Skip to main content
Glama

Improve a roof plan (solar roof check)

edit_roof
Read-onlyIdempotent

Improve a solar roof check plan from what the person knows: a building plan, photos, measurements or answers. Read the numbers yourself (tilt, overhang, sizes, where a chimney is) and pass them as edits: slope (tilt and direction of a face), overhang (move an eave or end in or out), split a face, remove or restore a face, add_face (an extension or garage against a wall), add, remove or confirm obstacles (with height_m), change_kind (e.g. a found dormer to a flat-roofed or hipped one, or a chimney to a vent), colour (sheet metal, window frames, …), covering (tiles, Falzblech, Trapezblech, rack tilt 5/10/15°, flat-roof racks; on a flat dormer roof it decides racks or flush), dormer_modules, fill_dormer and clear_dormer (one dormer's roof full of modules, or none), modules (arrangement as the page names it: „Ästhetisch“ design, „Ordentlich“ tidy, „Maximal“ most; orientation incl. mixed; module type), add_part or remove_part (an attached building part from "building_parts", e.g. the other half of a semi-detached house), remove_tree, restore_tree or tree_height (trees from "shade", the worst shade casters first), edit_obstacle (move, resize, raise, turn or give a free four-cornered outline to any obstacle, found or own), face_outline (any outline by corners, as the editor drags them), reset_face („Wie gemessen“), unsplit, add_face also with corners_m (any shape), drawn_face (eave height) and delete_face, turn_racks (flat roofs and racked dormer roofs), find_similar („Ähnliche finden“ on the aerial photo), check_outline (the AI outline check), search_again, module fields by hand (lay_by_hand, add_modules as a block of rows and columns, remove_modules, move_field, turn_field; also on dormer roofs from "dormer_roofs"; on flat roofs the modules keep standing on their racks), the modules in an area together as the page selects them (move_modules, turn_modules, set_module_type for a module type of their own; remove_modules takes an area too) and household (incl. offer_eur, the price on an offer the person has, for the payback the link shows; roof_step, optimizers and backup_power for the price guide). Own obstacle ids (u:) keep their numbers for the whole call. To rebuild a layout the person drew, lay the face by hand and place its fields. Faces are named by the ids in plan_roof's "faces", which give facing, building (house or attached part), size and eave_overhang_m to tell them apart; when two could fit, ask the person. Places on a face are metres up from the eave (horizontal, as on a drawing from above) and from the face's left end seen from below, standing in front of the roof. Each applied sentence says how many modules that edit added or took away. Returns the new link and figures, one sentence per change to confirm with the person, and the edits it turned down with the reason. The old link keeps working; nothing is stored.

Tool call template

Replace each placeholder with a value matching the input schema. Only required parameters are included; add optional parameters as needed.

{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {
      "link": "<link>"
    },
    "name": "edit_roof"
  }
}

Input Schema

NameRequiredDescriptionDefault
linkYesA solar roof check link (https://pv.skysolar.de/dachcheck/#d=…), as plan_roof, edit_roof or the solar roof check page gave it.
editsNo
answersNoThe person's answers to the last answer's "questions".
geometryNotrue: add a complete geometry snapshot to the answer (the default stays short): for every face and dormer roof its outline, size, usable box and the eave and slope axes (with compass bearings), every module with its face, position in the face's own metres and orientation, the module fields (rows × columns), and the obstacles with their outlines and clearances. Everything in the same metres the edits take.

Schema Changelog

Changes observed during successful MCP inspections.

  1. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, ton (Tonziegel, a narrow interlocking clay tile; mounted like tiles), biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "ton",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. Hand-laid modules are laid again for the new size, rows and columns kept; \"applied\" says how many no longer fit.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Wieder automatisch belegen“: faces laid by hand go back to the automatic layout, so orientation and arrangement count there again. Their hand-laid modules go; their outline, slope and turned racks stay (reset_face takes those back too).",
      -    "properties": {
      -      "face": {
      -        "description": "One face laid by hand; without it, all of them.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_automatically",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "backup_power": {
      -        "description": "The person wants backup power (Notstrom): it is in both prices of the price guide.",
      -        "type": "boolean"
      -      },
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "battery_type": {
      -        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      -        "enum": [
      -          "nv5",
      -          "hv1c"
      -        ],
      -        "type": "string"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the payback then counts it. Without one the payback counts the price guide's recommendation (summary.price_guide). Never pass a price of Sky Solar's as an offer.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "optimizers": {
      -        "description": "The person wants module optimizers: they are in both prices of the price guide. Without them the recommendation adds them anyway where the plan shows shade or several directions.",
      -        "type": "boolean"
      -      },
      -      "roof_step": {
      -        "description": "How demanding the roof is for the price guide: \"simple\" (one field, plain tiles), \"normal\", \"complex\" (several directions, many small fields, Biberschwanz tiles); \"auto\" (the default) works it out from the plan. Changes the price guide only.",
      -        "enum": [
      -          "auto",
      -          "simple",
      -          "normal",
      -          "complex"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "bivalence_c": {
      -        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      -        "maximum": 5,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery silver, white, anthracite, black (default silver); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "heating_rod",
      -          "battery",
      -          "dishwasher",
      -          "washer",
      -          "dryer"
      -        ],
      -        "type": "string"
      -      },
      -      "hot_water_percent": {
      -        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      -        "maximum": 100,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_run": {
      -        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      -        "maximum": 6,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "peak_kw": {
      -        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "peak_minutes": {
      -        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      -        "maximum": 240,
      -        "minimum": 5,
      -        "type": "number"
      -      },
      -      "rod_kw": {
      -        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      -        "maximum": 12,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "runs_per_week": {
      -        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      -        "maximum": 14,
      -        "minimum": 0.5,
      -        "type": "number"
      -      },
      -      "start": {
      -        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      -        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; module: black, blue; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "module",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: pfanne (Doppel-S-Pfanne, the common concrete tile, the standard of a new plan), tiles (Geradschnitt, a straight-cut tile, the standard of older plans), ton (Tonziegel, a narrow interlocking clay tile; mounted like tiles), biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), reform (Reformziegel, one broad flat trough), doppelmulde (Doppelmuldenfalzziegel, two narrow troughs), hohlfalz (Hohlfalzziegel, an S-wave; these four mounted like tiles), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "ton",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez",
      +          "pfanne",
      +          "reform",
      +          "doppelmulde",
      +          "hohlfalz"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm; m410s = 410 W silver · 1.762 × 1.134 mm; m410g = 410 W green · 1.762 × 1.134 mm; m400w = 400 W wine red · 1.762 × 1.134 mm; m400t = 400 W terracotta · 1.762 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520",
      +          "m410s",
      +          "m410g",
      +          "m400w",
      +          "m400t"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm; m410s = 410 W silver · 1.762 × 1.134 mm; m410g = 410 W green · 1.762 × 1.134 mm; m400w = 400 W wine red · 1.762 × 1.134 mm; m400t = 400 W terracotta · 1.762 × 1.134 mm. Hand-laid modules are laid again for the new size, rows and columns kept; \"applied\" says how many no longer fit.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520",
      +          "m410s",
      +          "m410g",
      +          "m400w",
      +          "m400t"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Wieder automatisch belegen“: faces laid by hand go back to the automatic layout, so orientation and arrangement count there again. Their hand-laid modules go; their outline, slope and turned racks stay (reset_face takes those back too).",
      +    "properties": {
      +      "face": {
      +        "description": "One face laid by hand; without it, all of them.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_automatically",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "backup_power": {
      +        "description": "The person wants backup power (Notstrom): it is in both prices of the price guide.",
      +        "type": "boolean"
      +      },
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the payback then counts it. Without one the payback counts the price guide's recommendation (summary.price_guide). Never pass a price of Sky Solar's as an offer.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "optimizers": {
      +        "description": "The person wants module optimizers: they are in both prices of the price guide. Without them the recommendation adds them anyway where the plan shows shade or several directions.",
      +        "type": "boolean"
      +      },
      +      "roof_step": {
      +        "description": "How demanding the roof is for the price guide: \"simple\" (one field, plain tiles), \"normal\", \"complex\" (several directions, many small fields, Biberschwanz tiles); \"auto\" (the default) works it out from the plan. Changes the price guide only.",
      +        "enum": [
      +          "auto",
      +          "simple",
      +          "normal",
      +          "complex"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "How big the system is (the page's \"− 24 Module +\"). One fixed order through the modules: the sun sets it, only whole rows and columns count, east and west stay mixed, north comes last. Hand modules, modules taken out and faces switched off stay as they are. A first plan shows the whole roof; when the household's use is given the plan jumps to the target by itself, unless the person chose a size. Give modules or to, not both.",
      +    "properties": {
      +      "modules": {
      +        "description": "How many modules: the nearest step of the plan's size ladder (summary.size.steps lists them). Each step is a tidy field, never modules scattered about.",
      +        "maximum": 600,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "to": {
      +        "description": "\"target\": the size that suits the household (a year's yield 1.5 to 2 times its use, e-car and heat pump in), and it follows the household from then on; \"whole\": the whole roof; \"smaller\", \"larger\": one step along the ladder, as the page's − and +.",
      +        "enum": [
      +          "target",
      +          "whole",
      +          "smaller",
      +          "larger"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "size",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "bivalence_c": {
      +        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      +        "maximum": 5,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery silver, white, anthracite, black (default silver); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "heating_rod",
      +          "battery",
      +          "dishwasher",
      +          "washer",
      +          "dryer"
      +        ],
      +        "type": "string"
      +      },
      +      "hot_water_percent": {
      +        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      +        "maximum": 100,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_run": {
      +        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      +        "maximum": 6,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "peak_kw": {
      +        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "peak_minutes": {
      +        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      +        "maximum": 240,
      +        "minimum": 5,
      +        "type": "number"
      +      },
      +      "rod_kw": {
      +        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      +        "maximum": 12,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "runs_per_week": {
      +        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      +        "maximum": 14,
      +        "minimum": 0.5,
      +        "type": "number"
      +      },
      +      "start": {
      +        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      +        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  2. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "battery_type": {
      -        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      -        "enum": [
      -          "nv5",
      -          "hv1c"
      -        ],
      -        "type": "string"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "bivalence_c": {
      -        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      -        "maximum": 5,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery silver, white, anthracite, black (default silver); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "heating_rod",
      -          "battery",
      -          "dishwasher",
      -          "washer",
      -          "dryer"
      -        ],
      -        "type": "string"
      -      },
      -      "hot_water_percent": {
      -        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      -        "maximum": 100,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_run": {
      -        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      -        "maximum": 6,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "peak_kw": {
      -        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "peak_minutes": {
      -        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      -        "maximum": 240,
      -        "minimum": 5,
      -        "type": "number"
      -      },
      -      "rod_kw": {
      -        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      -        "maximum": 12,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "runs_per_week": {
      -        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      -        "maximum": 14,
      -        "minimum": 0.5,
      -        "type": "number"
      -      },
      -      "start": {
      -        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      -        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, ton (Tonziegel, a narrow interlocking clay tile; mounted like tiles), biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "ton",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. Hand-laid modules are laid again for the new size, rows and columns kept; \"applied\" says how many no longer fit.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Wieder automatisch belegen“: faces laid by hand go back to the automatic layout, so orientation and arrangement count there again. Their hand-laid modules go; their outline, slope and turned racks stay (reset_face takes those back too).",
      +    "properties": {
      +      "face": {
      +        "description": "One face laid by hand; without it, all of them.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_automatically",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "backup_power": {
      +        "description": "The person wants backup power (Notstrom): it is in both prices of the price guide.",
      +        "type": "boolean"
      +      },
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the payback then counts it. Without one the payback counts the price guide's recommendation (summary.price_guide). Never pass a price of Sky Solar's as an offer.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "optimizers": {
      +        "description": "The person wants module optimizers: they are in both prices of the price guide. Without them the recommendation adds them anyway where the plan shows shade or several directions.",
      +        "type": "boolean"
      +      },
      +      "roof_step": {
      +        "description": "How demanding the roof is for the price guide: \"simple\" (one field, plain tiles), \"normal\", \"complex\" (several directions, many small fields, Biberschwanz tiles); \"auto\" (the default) works it out from the plan. Changes the price guide only.",
      +        "enum": [
      +          "auto",
      +          "simple",
      +          "normal",
      +          "complex"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "bivalence_c": {
      +        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      +        "maximum": 5,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery silver, white, anthracite, black (default silver); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "heating_rod",
      +          "battery",
      +          "dishwasher",
      +          "washer",
      +          "dryer"
      +        ],
      +        "type": "string"
      +      },
      +      "hot_water_percent": {
      +        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      +        "maximum": 100,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_run": {
      +        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      +        "maximum": 6,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "peak_kw": {
      +        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "peak_minutes": {
      +        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      +        "maximum": 240,
      +        "minimum": 5,
      +        "type": "number"
      +      },
      +      "rod_kw": {
      +        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      +        "maximum": 12,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "runs_per_week": {
      +        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      +        "maximum": 14,
      +        "minimum": 0.5,
      +        "type": "number"
      +      },
      +      "start": {
      +        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      +        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  3. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "battery_type": {
      -        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      -        "enum": [
      -          "nv5",
      -          "hv1c"
      -        ],
      -        "type": "string"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "bivalence_c": {
      -        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      -        "maximum": 5,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "heating_rod",
      -          "battery",
      -          "dishwasher",
      -          "washer",
      -          "dryer"
      -        ],
      -        "type": "string"
      -      },
      -      "hot_water_percent": {
      -        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      -        "maximum": 100,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_run": {
      -        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      -        "maximum": 6,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "peak_kw": {
      -        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "peak_minutes": {
      -        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      -        "maximum": 240,
      -        "minimum": 5,
      -        "type": "number"
      -      },
      -      "rod_kw": {
      -        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      -        "maximum": 12,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "runs_per_week": {
      -        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      -        "maximum": 14,
      -        "minimum": 0.5,
      -        "type": "number"
      -      },
      -      "start": {
      -        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      -        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "bivalence_c": {
      +        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      +        "maximum": 5,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery silver, white, anthracite, black (default silver); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "heating_rod",
      +          "battery",
      +          "dishwasher",
      +          "washer",
      +          "dryer"
      +        ],
      +        "type": "string"
      +      },
      +      "hot_water_percent": {
      +        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      +        "maximum": 100,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_run": {
      +        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      +        "maximum": 6,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "peak_kw": {
      +        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "peak_minutes": {
      +        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      +        "maximum": 240,
      +        "minimum": 5,
      +        "type": "number"
      +      },
      +      "rod_kw": {
      +        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      +        "maximum": 12,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "runs_per_week": {
      +        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      +        "maximum": 14,
      +        "minimum": 0.5,
      +        "type": "number"
      +      },
      +      "start": {
      +        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      +        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  4. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "battery_type": {
      -        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      -        "enum": [
      -          "nv5",
      -          "hv1c"
      -        ],
      -        "type": "string"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type).",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "battery"
      -        ],
      -        "type": "string"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car, a heat pump, its heating rod or a shiftable appliance with its own load curve: on or off, its settings and its colour (also the battery's colour). The car and the heat pump add to the household's day; appliances and the rod add no kWh, they give a share of the year a shape of its own. The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "bivalence_c": {
      +        "description": "heating_rod: the outdoor temperature below which it helps, °C (default -5).",
      +        "maximum": 5,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"heating_rod\": the heat pump's electric rod (needs the heat pump; off by default); \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type); \"dishwasher\", \"washer\", \"dryer\": a shiftable appliance run at a set time. An appliance adds nothing to the household's kWh a year: it is already inside them and moves out of the smooth curve into a timed shape. Fridge, freezer and TV stay in the base load.",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "heating_rod",
      +          "battery",
      +          "dishwasher",
      +          "washer",
      +          "dryer"
      +        ],
      +        "type": "string"
      +      },
      +      "hot_water_percent": {
      +        "description": "heating_rod: share of the hot water it heats instead of the heat pump, % (default 0), each morning. The rod's kWh stay inside the heat pump's kWh a year.",
      +        "maximum": 100,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_run": {
      +        "description": "appliance: kWh per run (defaults: dishwasher 0.85, washer 0.8, dryer 1.5; EU label and Verbraucherzentrale figures; a dryer without a heat pump about 3.4).",
      +        "maximum": 6,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "peak_kw": {
      +        "description": "appliance: kW of its heating peak (defaults: dishwasher 2, washer 2, dryer 0.8).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "peak_minutes": {
      +        "description": "appliance: how long the peak lasts, minutes, all of it together (defaults: dishwasher 20, washer 15, dryer 100).",
      +        "maximum": 240,
      +        "minimum": 5,
      +        "type": "number"
      +      },
      +      "rod_kw": {
      +        "description": "heating_rod: its power, kW (default 6; rods have 3 to 9 kW).",
      +        "maximum": 12,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "runs_per_week": {
      +        "description": "appliance: runs a week, evenly over the week (defaults: dishwasher 5.4, washer 4.2, dryer 2).",
      +        "maximum": 14,
      +        "minimum": 0.5,
      +        "type": "number"
      +      },
      +      "start": {
      +        "description": "appliance: when it starts, \"HH:MM\" (to ten minutes) or \"sun\" for the midday window, a fixed time as a delay timer sets it (dishwasher 12:00, washer 11:00, dryer 14:00). Defaults are evening: dishwasher 20:00, washer 18:00, dryer 20:00.",
      +        "pattern": "^(sun|([01]?\\d|2[0-3]):[0-5]\\d)$",
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  5. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "battery_type": {
      -        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      -        "enum": [
      -          "nv5",
      -          "hv1c"
      -        ],
      -        "type": "string"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type).",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "battery"
      -        ],
      -        "type": "string"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call. plan_roof runs it by itself on a new plan when the photo speaks against the outline; call it when a note says the outline may be wrong, or the outline looks wrong to you, before placing modules: it replaces the outline of each face it moves, your own changes to that face included.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type).",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "battery"
      +        ],
      +        "type": "string"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  6. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      -    "properties": {
      -      "charge_kw": {
      -        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      -        "type": "number"
      -      },
      -      "charges_per_week": {
      -        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      -        "maximum": 7,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "charging": {
      -        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      -        "enum": [
      -          "evening",
      -          "daytime",
      -          "weekend"
      -        ],
      -        "type": "string"
      -      },
      -      "colour": {
      -        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      -        "enum": [
      -          "silver",
      -          "white",
      -          "anthracite",
      -          "black",
      -          "blue",
      -          "red"
      -        ],
      -        "type": "string"
      -      },
      -      "device": {
      -        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh).",
      -        "enum": [
      -          "ev",
      -          "heat_pump",
      -          "battery"
      -        ],
      -        "type": "string"
      -      },
      -      "km_per_year": {
      -        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      -        "maximum": 100000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "kwh_per_year": {
      -        "anyOf": [
      -          {
      -            "maximum": 30000,
      -            "minimum": 500,
      -            "type": "number"
      -          },
      -          {
      -            "const": "estimate",
      -            "type": "string"
      -          }
      -        ],
      -        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      -      },
      -      "on": {
      -        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "device",
      -        "type": "string"
      -      },
      -      "wallbox_colour": {
      -        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      -        "enum": [
      -          "white",
      -          "silver",
      -          "anthracite",
      -          "black"
      -        ],
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "device"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "battery_type": {
      +        "description": "The battery's type, both DC-coupled hybrid systems: \"nv5\" (low voltage, the default) charges and discharges at most 5 kW whatever its size; \"hv1c\" (high voltage) at most 1C, as many kW as it has kWh (10 kWh: 10 kW, 4 kWh: only 4 kW). Only with a battery. It matters at peaks: an e-car charging at more than 5 kW; a household on its own comes out nearly the same with both, with a heat pump too.",
      +        "enum": [
      +          "nv5",
      +          "hv1c"
      +        ],
      +        "type": "string"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh, its type household.battery_type).",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "battery"
      +        ],
      +        "type": "string"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  7. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      -    "properties": {
      -      "anchor": {
      -        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      -        "enum": [
      -          "lower_left",
      -          "centre"
      -        ],
      -        "type": "string"
      -      },
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      -        "type": "boolean"
      -      },
      -      "exact": {
      -        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The anchor: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "snap_to_grid": {
      -        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "place_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      -    "properties": {
      -      "count": {
      -        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "integer"
      -      },
      -      "direction": {
      -        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      -        "enum": [
      -          "right",
      -          "left",
      -          "up",
      -          "down"
      -        ],
      -        "type": "string"
      -      },
      -      "dry_run": {
      -        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      -        "type": "boolean"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module of the row (or column) to continue.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "continue_row",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "direction"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_dormer",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: by default 15,000 km a year, about 2,955 kWh, charged in the evening (edit_roof device sets km, when and how fast).",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: by default about 4,500 kWh a year, mostly in winter (edit_roof device sets the person's own figure).",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "An e-car or a heat pump with its own load curve (the household's day stays as it is): on or off, its settings and its colour (also the battery's colour). The answer's summary.devices shows each one's kWh a year and how much of it the roof covers; summary.device_colours the colours.",
      +    "properties": {
      +      "charge_kw": {
      +        "description": "ev: charging power, kW: 2.3, 3.7, 7.4, 11, 22 (default 11, a usual wallbox).",
      +        "type": "number"
      +      },
      +      "charges_per_week": {
      +        "description": "ev: charging sessions a week, 1 to 7 (default 7, every day); each takes what built up since the last. Not for \"weekend\".",
      +        "maximum": 7,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "charging": {
      +        "description": "ev: when the car is at home to charge: \"evening\" (default, from 18:00), \"daytime\" (home office or retired, from 11:00), \"weekend\" (Saturday and Sunday from 11:00). It charges at full power until done; no smart charging yet.",
      +        "enum": [
      +          "evening",
      +          "daytime",
      +          "weekend"
      +        ],
      +        "type": "string"
      +      },
      +      "colour": {
      +        "description": "The device's colour as the 3D view draws it by the house (changes no figure): heat_pump and battery white, silver, anthracite, black (default white); ev (the car's paint) silver, white, anthracite, black, blue, red (default silver). A colour alone does not count as an answer about km or kWh.",
      +        "enum": [
      +          "silver",
      +          "white",
      +          "anthracite",
      +          "black",
      +          "blue",
      +          "red"
      +        ],
      +        "type": "string"
      +      },
      +      "device": {
      +        "description": "\"ev\": an electric car charged at home (with its wallbox); \"heat_pump\": an air-to-water heat pump heating the house and its water; \"battery\": the home battery, only its colour (its size is household.battery_kwh).",
      +        "enum": [
      +          "ev",
      +          "heat_pump",
      +          "battery"
      +        ],
      +        "type": "string"
      +      },
      +      "km_per_year": {
      +        "description": "ev: km a year charged at home (default 15000); at 19.7 kWh per 100 km (ADAC Ecotest 2023 average, at the socket).",
      +        "maximum": 100000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "kwh_per_year": {
      +        "anyOf": [
      +          {
      +            "maximum": 30000,
      +            "minimum": 500,
      +            "type": "number"
      +          },
      +          {
      +            "const": "estimate",
      +            "type": "string"
      +          }
      +        ],
      +        "description": "heat_pump: its electricity a year in kWh (its meter, the bill or the installer's figure), or \"estimate\" for ours (4,500 kWh). Spread over the year by the measured outdoor temperature at the house."
      +      },
      +      "on": {
      +        "description": "false takes the device out (its settings go with it); true or left out puts it in. Sending a device edit, even with no settings, records that the person answered for it (the defaults are right), so ask_the_person stops asking.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "device",
      +        "type": "string"
      +      },
      +      "wallbox_colour": {
      +        "description": "ev: the wallbox's colour, white, silver, anthracite, black (default white).",
      +        "enum": [
      +          "white",
      +          "silver",
      +          "anthracite",
      +          "black"
      +        ],
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "device"
      +    ],
      +    "type": "object"
      +  }
      +]
  8. Changed2 schema fields changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape).",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_modules",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "module": {
      -        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "set_module_type",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m",
      -      "module"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "fill_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      -    "properties": {
      -      "id": {
      -        "description": "A dormer's obstacle id.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "clear_dormer",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      -        "maximum": 20,
      -        "minimum": 0,
      -        "type": "integer"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found). A found one becomes one of your obstacles (u:<n>), but its id keeps working: later edits that name it (edit_obstacle, change_kind, fill_dormer, clear_dormer, find_similar, remove_obstacle, confirm_obstacle) and its dormer roofs (g:<id>:<n>) reach the new one, in this call and later ones, while it stays on the found one's spot; the answer's \"id_map\" lists them ({ found id: u:<n> }). A dormer's modules and roof settings move to its new roofs where they are the same, and the answer says what did not.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Place an R × C field of modules exactly where the person's drawing shows it, in the face's own metres. Use dry_run first when unsure.",
      +    "properties": {
      +      "anchor": {
      +        "description": "What from_eave_m and from_left_m name: the field's lower left corner (default) or its middle.",
      +        "enum": [
      +          "lower_left",
      +          "centre"
      +        ],
      +        "type": "string"
      +      },
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; the answer's \"dry_run\" lists the modules that would be placed (row, column, centre, orientation) and every collision with its reason.",
      +        "type": "boolean"
      +      },
      +      "exact": {
      +        "description": "Default true: all or nothing. If any module of the R × C field would collide (an obstacle, the edge margin, another module) nothing is placed and every collision is listed. false: place the ones that fit (as add_modules).",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's anchor: horizontal metres up from the eave (the lower left corner, or the middle with anchor \"centre\").",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The anchor: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "snap_to_grid": {
      +        "description": "true: the field's first module is moved onto the module grid of the nearest module on this face (same pitch, same rows and columns, its orientation unless given), so a new field lines up with an existing one. The answer says by how many cm it moved.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "place_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Continue a row (or column) of modules in a direction, on the row's own grid: the same pitch, orientation, type and line, one clamp gap on from its last module. A neighbouring field on the same grid is met exactly.",
      +    "properties": {
      +      "count": {
      +        "description": "How many modules to add. Without it: as many as fit, up to the next obstacle, module or edge (the answer says what stopped the row).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "integer"
      +      },
      +      "direction": {
      +        "description": "Which way: right or left seen from below along the eave; up or down the slope.",
      +        "enum": [
      +          "right",
      +          "left",
      +          "up",
      +          "down"
      +        ],
      +        "type": "string"
      +      },
      +      "dry_run": {
      +        "description": "true: change nothing; \"dry_run\" lists what would be added.",
      +        "type": "boolean"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module of the row (or column) to continue.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "continue_row",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "direction"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move a dormer and keep its modules: the dormer moves as with edit_obstacle, its automatically laid modules follow, and the modules laid by hand on its roofs are carried the same distance.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (\"obstacles\" or \"your_obstacles\"; a replaced dormer's old id works).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_dormer",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape). portrait: the module's long side up the slope (across the eave); landscape: long side along the eave. The same on every face, house roof or dormer roof: \"up the slope\" and \"along the eave\" are the face's own axes (geometry.faces[].slope_axis / eave_axis; on a flat face its longest edge). On racks every module stands landscape and this does not apply.",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  }
      +]
    • addedInput schema / properties / geometry
      Added value: +{
      +  "description": "true: add a complete geometry snapshot to the answer (the default stays short): for every face and dormer roof its outline, size, usable box and the eave and slope axes (with compass bearings), every module with its face, position in the face's own metres and orientation, the module fields (rows × columns), and the obstacles with their outlines and clearances. Everything in the same metres the edits take.",
      +  "type": "boolean"
      +}
  9. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape).",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: clockwise).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      -        "type": "number"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say), from \"faces\" or \"dormer_roofs\"; without it the whole roof. On a flat dormer roof it decides the mounting: tiles (a plain flat roof) or trapez puts the modules on racks, falz lays them flush (without a choice: racks where it stands on a flat roof or measures 20 m² or more; see its \"mounting\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape).",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Move the modules in an area together, as the solar roof check moves a selection (press, hold and drag over them, then drag the middle). All or none: blocked by the face's edge, another module or an obstacle.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_modules",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Turn the modules in an area portrait ↔ landscape, as the page's selection does: laid afresh from their corner at the eave, as wide as they were, at most as many as there were. Modules on racks always stand landscape.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Another module type for the modules in an area (the page's selection, \"Modultyp\"): each keeps its row and column from the group's corner; one that no longer fits goes.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "One corner of the area (horizontal metres up from the eave), or a spot on one module.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "module": {
      +        "description": "The module type for these modules: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm. The plan's own type (edit \"modules\") stays for the rest.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner: every module whose middle lies in the area is picked.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "set_module_type",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m",
      +      "module"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Gaube ganz belegen“: as many modules as fit on the dormer's roof, laid as any roof (on racks where its flat top is big enough or stands on a flat roof). A flat or shed dormer is switched on alone.",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id (from \"obstacles\", \"u:<n>\" for one added).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "fill_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "„Alle Module von der Gaube nehmen“: the dormer's roof switched off (fill_dormer lays it again).",
      +    "properties": {
      +      "id": {
      +        "description": "A dormer's obstacle id.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "clear_dormer",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: anticlockwise, seen from above).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face, or a flat dormer roof from \"dormer_roofs\" whose \"mounting\" is racks.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: 0 for none, or a whole number from 4 to 20 (default 8).",
      +        "maximum": 20,
      +        "minimum": 0,
      +        "type": "integer"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  }
      +]
  10. Changed2 schema fields changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners_m": {
      -        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      -        "items": {
      -          "items": false,
      -          "maxItems": 2,
      -          "minItems": 2,
      -          "prefixItems": [
      -            {
      -              "type": "number"
      -            },
      -            {
      -              "type": "number"
      -            }
      -          ],
      -          "type": "array"
      -        },
      -        "maxItems": 24,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "roof",
      -          "metal",
      -          "dormerBarrel",
      -          "dormerShed",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape).",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Put modules taken out of the automatic layout back.",
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "restore_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "As the Dachcheck names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "module": {
      -        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      -        "enum": [
      -          "m470",
      -          "m490",
      -          "m520"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape",
      -          "mixed"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A building part from \"building_parts\" to plan without again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_part",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "A tree id taken out before.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_tree",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "change_m": {
      -        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "A tree id (\"t:…\") from \"shade\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "tree_height",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "change_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "A free four-cornered outline instead of a rectangle (as the Dachcheck's corner handles): its corners on its face, the front (eave side) first.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 4,
      -        "minItems": 4,
      -        "type": "array"
      -      },
      -      "depth_m": {
      -        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      -        "maximum": 8,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "right_m": {
      -        "description": "Move it to the right seen from below (negative: left), metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "turn_deg": {
      -        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the Dachcheck.",
      -        "maximum": 180,
      -        "minimum": -180,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "edit_obstacle",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Move it up the roof (negative: down), horizontal metres.",
      -        "maximum": 10,
      -        "minimum": -10,
      -        "type": "number"
      -      },
      -      "width_m": {
      -        "description": "New width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "corners": {
      -        "description": "The face's new outline, corner by corner (as the Dachcheck's editor drags them); \"faces[].corners\" gives the present ones.",
      -        "items": {
      -          "additionalProperties": false,
      -          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      -          "properties": {
      -            "from_eave_m": {
      -              "maximum": 40,
      -              "minimum": -40,
      -              "type": "number"
      -            },
      -            "from_left_m": {
      -              "maximum": 50,
      -              "minimum": -50,
      -              "type": "number"
      -            }
      -          },
      -          "required": [
      -            "from_eave_m",
      -            "from_left_m"
      -          ],
      -          "type": "object"
      -        },
      -        "maxItems": 30,
      -        "minItems": 3,
      -        "type": "array"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "face_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "corners"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "reset_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "unsplit",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "eave_height_m": {
      -        "description": "Its lowest edge above the ground, metres.",
      -        "maximum": 30,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\").",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "drawn_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "delete_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "find_similar",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      -    "properties": {
      -      "type": {
      -        "const": "check_outline",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      -    "properties": {
      -      "type": {
      -        "const": "search_again",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "degrees": {
      -        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: clockwise).",
      -        "maximum": 45,
      -        "minimum": -45,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A flat face.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "turn_racks",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "degrees"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      -        "type": "number"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "offer_eur": {
      -        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the Dachcheck link then shows the payback. Never a price of Sky Solar's.",
      -        "maximum": 300000,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape).",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the solar roof check names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the solar roof check's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the solar roof check.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the solar roof check's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: clockwise).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      +        "type": "number"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the solar roof check link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  }
      +]
    • changedInput schema / properties / link / description
      Previous value: -"A Dachcheck link (https://pv.skysolar.de/dachcheck/#d=…), as plan_roof, edit_roof or the Dachcheck page gave it."New value: +"A solar roof check link (https://pv.skysolar.de/dachcheck/#d=…), as plan_roof, edit_roof or the solar roof check page gave it."
  11. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch).",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "wall",
      -      "width_m",
      -      "depth_m",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "A new width along the eave, metres in the slope.",
      -        "maximum": 12,
      -        "minimum": 0.1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "metal",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "biber",
      -          "flach",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape).",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "tidy: even fields (default); most: as many as fit; design: clean rectangles only.",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      -        "type": "number"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners_m": {
      +        "description": "Any shape instead: its corners as [east, north] metres in the same frame as \"house.footprint_m\" (from a building plan).",
      +        "items": {
      +          "items": false,
      +          "maxItems": 2,
      +          "minItems": 2,
      +          "prefixItems": [
      +            {
      +              "type": "number"
      +            },
      +            {
      +              "type": "number"
      +            }
      +          ],
      +          "type": "array"
      +        },
      +        "maxItems": 24,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch). With width_m and depth_m; or give corners_m instead.",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; dormerBarrel: tiles, anthracite, aluminium, zinc, copper, brown; dormerShed: tiles, anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: roof (a tiled roof, for the building part of face, the house without face; colours: photo, clay, red, darkRed, copper, brown, black, anthracite, slate, grey), metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "roof",
      +          "metal",
      +          "dormerBarrel",
      +          "dormerShed",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape).",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Put modules taken out of the automatic layout back.",
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on a module taken out before, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "restore_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "As the Dachcheck names them: design = „Ästhetisch“ (whole rectangles only, the same height, left and right the same width), tidy = „Ordentlich“ (rows in one line, no small single fields; default), most = „Maximal“ (as many as fit, landscape in gaps too).",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "module": {
      +        "description": "The module type: m470 = 470 W · 1.762 × 1.134 mm; m490 = 490 W · 1.800 × 1.134 mm; m520 = 520 W · 1.960 × 1.134 mm.",
      +        "enum": [
      +          "m470",
      +          "m490",
      +          "m520"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "description": "auto (default): portrait, or landscape where that fits 2+ more modules; mixed: portrait and landscape on one roof, wherever each fits.",
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape",
      +          "mixed"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" (an extension, a garage, the other half of a semi-detached house) to plan with the house.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A building part from \"building_parts\" to plan without again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_part",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\": felled, or wrong in the data; its shade no longer counts.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "A tree id taken out before.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_tree",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "change_m": {
      +        "description": "Metres higher (positive) or lower (negative) than the laser scan measured it (the scan is from earlier years).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "A tree id (\"t:…\") from \"shade\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "tree_height",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "change_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "A free four-cornered outline instead of a rectangle (as the Dachcheck's corner handles): its corners on its face, the front (eave side) first.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 4,
      +        "minItems": 4,
      +        "type": "array"
      +      },
      +      "depth_m": {
      +        "description": "New depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a chimney taller than measured casts more shade).",
      +        "maximum": 8,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" (found by scan or photo) or \"your_obstacles\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "right_m": {
      +        "description": "Move it to the right seen from below (negative: left), metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "turn_deg": {
      +        "description": "Turn it round its centre, degrees (positive: clockwise seen from above), as „Drehen“ in the Dachcheck.",
      +        "maximum": 180,
      +        "minimum": -180,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "edit_obstacle",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Move it up the roof (negative: down), horizontal metres.",
      +        "maximum": 10,
      +        "minimum": -10,
      +        "type": "number"
      +      },
      +      "width_m": {
      +        "description": "New width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "corners": {
      +        "description": "The face's new outline, corner by corner (as the Dachcheck's editor drags them); \"faces[].corners\" gives the present ones.",
      +        "items": {
      +          "additionalProperties": false,
      +          "description": "A corner on the face: horizontal metres up from the eave and from the left end seen from below.",
      +          "properties": {
      +            "from_eave_m": {
      +              "maximum": 40,
      +              "minimum": -40,
      +              "type": "number"
      +            },
      +            "from_left_m": {
      +              "maximum": 50,
      +              "minimum": -50,
      +              "type": "number"
      +            }
      +          },
      +          "required": [
      +            "from_eave_m",
      +            "from_left_m"
      +          ],
      +          "type": "object"
      +        },
      +        "maxItems": 30,
      +        "minItems": 3,
      +        "type": "array"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "face_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "corners"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face back to the measured roof („Wie gemessen“): its outline, slope, turned racks and hand-laid modules go.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "reset_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A piece of a split face (\"<id>|1\" or \"|2\"): both pieces become one face again.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "unsplit",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "eave_height_m": {
      +        "description": "Its lowest edge above the ground, metres.",
      +        "maximum": 30,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\").",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "drawn_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face added with add_face (\"d:<n>\"): deleted; later ones move down by one.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "delete_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (a chimney, a skylight dome, a vent …): the aerial photo is searched for more of the same on this roof (as „Ähnliche finden“), and what it finds is added as your obstacles. One AI call each; at most 3 a call.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "find_similar",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "The AI outline check (as „Umriss passt nicht?“): the roof's edges are compared with the aerial photo and moved where they clearly lie elsewhere. One AI call; use it when a note says the photo shows more roof.",
      +    "properties": {
      +      "type": {
      +        "const": "check_outline",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "description": "Ask the aerial photo again for chimneys and roof windows when \"obstacles_checked\" was false (as „Nochmal suchen“).",
      +    "properties": {
      +      "type": {
      +        "const": "search_again",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "degrees": {
      +        "description": "Turn the module racks from the roof's own edges by this much (0: along the edges again; positive: clockwise).",
      +        "maximum": 45,
      +        "minimum": -45,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A flat face.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "turn_racks",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "degrees"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      +        "type": "number"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "offer_eur": {
      +        "description": "The total price on an offer the person already has, euros before any subsidy (0 clears it): the Dachcheck link then shows the payback. Never a price of Sky Solar's.",
      +        "maximum": 300000,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  }
      +]
  12. Changed1 schema field changed
    • changedInput schema / properties / edits / items / oneOf
      Previous value: -[
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "direction_deg": {
      -        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "tilt_deg": {
      -        "description": "Tilt, degrees (0 flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "slope",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "edge": {
      -        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      -        "enum": [
      -          "eave",
      -          "left",
      -          "right"
      -        ],
      -        "type": "string"
      -      },
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "metres": {
      -        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      -        "maximum": 1.5,
      -        "minimum": -1.5,
      -        "type": "number"
      -      },
      -      "to_m": {
      -        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      -        "maximum": 1.5,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "overhang",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "edge"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A cut along the eave, this far up from it.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "A cut up the slope, this far from the left end.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "split",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "restore_face",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "depth_m": {
      -        "description": "Out from the wall, metres.",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "direction_deg": {
      -        "description": "Way the roof slopes down to (default: away from the wall).",
      -        "maximum": 360,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "eave_height_m": {
      -        "description": "Height of the roof's lowest edge above the ground, metres.",
      -        "maximum": 15,
      -        "minimum": 1.5,
      -        "type": "number"
      -      },
      -      "offset_m": {
      -        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      -        "maximum": 30,
      -        "minimum": -30,
      -        "type": "number"
      -      },
      -      "tilt_deg": {
      -        "description": "Roof tilt, degrees (default 0, flat).",
      -        "maximum": 60,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_face",
      -        "type": "string"
      -      },
      -      "wall": {
      -        "description": "The house wall the new part stands against (an extension, a garage, a porch).",
      -        "enum": [
      -          "north",
      -          "east",
      -          "south",
      -          "west"
      -        ],
      -        "type": "string"
      -      },
      -      "width_m": {
      -        "description": "Along the wall, metres.",
      -        "maximum": 30,
      -        "minimum": 1,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "wall",
      -      "width_m",
      -      "depth_m",
      -      "eave_height_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from the last answer's \"faces\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "height_m": {
      -        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      -        "maximum": 4,
      -        "minimum": 0.1,
      -        "type": "number"
      -      },
      -      "kind": {
      -        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "add_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "kind",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "kind": {
      -        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      -        "enum": [
      -          "chimney",
      -          "window",
      -          "vent",
      -          "flue",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome",
      -          "dormerShed",
      -          "dormerFlat",
      -          "dormerGable",
      -          "dormerTwin",
      -          "dormerHip",
      -          "dormerPoint",
      -          "dormerTrapez",
      -          "dormerBarrel",
      -          "dormerBat",
      -          "dormerWall",
      -          "block"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "change_kind",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id",
      -      "kind"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "colour": {
      -        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      -        "maxLength": 30,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "item": {
      -        "description": "What gets the colour: metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      -        "enum": [
      -          "metal",
      -          "block",
      -          "window",
      -          "chimney",
      -          "flue",
      -          "vent",
      -          "dish",
      -          "antenna",
      -          "ladder",
      -          "snowWood",
      -          "snowGrid",
      -          "steps",
      -          "solarThermal",
      -          "pv",
      -          "dome"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "colour",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "item",
      -      "colour"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "covering": {
      -        "description": "The roof's covering: tiles, falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      -        "enum": [
      -          "tiles",
      -          "falz",
      -          "trapez"
      -        ],
      -        "type": "string"
      -      },
      -      "flat_rack": {
      -        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      -        "enum": [
      -          "ew",
      -          "south",
      -          "ns"
      -        ],
      -        "type": "string"
      -      },
      -      "rack_tilt_deg": {
      -        "anyOf": [
      -          {
      -            "const": 5,
      -            "type": "number"
      -          },
      -          {
      -            "const": 10,
      -            "type": "number"
      -          },
      -          {
      -            "const": 15,
      -            "type": "number"
      -          }
      -        ],
      -        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      -      },
      -      "trapez_racks": {
      -        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "covering",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "lay_by_hand",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "columns": {
      -        "description": "Modules side by side (default 1).",
      -        "maximum": 20,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "The field's lower left corner: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "orientation": {
      -        "description": "Default portrait (landscape if the plan is set to landscape).",
      -        "enum": [
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "rows": {
      -        "description": "Rows up the roof (default 1).",
      -        "maximum": 10,
      -        "minimum": 1,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "add_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_eave_m": {
      -        "description": "The area's other corner (every module whose centre lies inside goes).",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "to_left_m": {
      -        "description": "The area's other corner.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "remove_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "right_m": {
      -        "description": "Metres to the right seen from below (negative: left).",
      -        "maximum": 40,
      -        "minimum": -40,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "move_field",
      -        "type": "string"
      -      },
      -      "up_m": {
      -        "description": "Metres up the roof, in its slope (negative: down).",
      -        "maximum": 20,
      -        "minimum": -20,
      -        "type": "number"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "face": {
      -        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "from_eave_m": {
      -        "description": "A spot on (or next to) the field.",
      -        "maximum": 30,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "from_left_m": {
      -        "description": "Metres from the face's left end, seen from below.",
      -        "maximum": 40,
      -        "minimum": 0,
      -        "type": "number"
      -      },
      -      "type": {
      -        "const": "turn_field",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "face",
      -      "from_eave_m",
      -      "from_left_m"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "description": "An obstacle id (\"u:<n>\" for one added before).",
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "remove_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "id": {
      -        "maxLength": 80,
      -        "minLength": 1,
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "confirm_obstacle",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "id"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "on": {
      -        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "dormer_modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type",
      -      "on"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "arrange": {
      -        "description": "tidy: even fields (default); most: as many as fit; design: clean rectangles only.",
      -        "enum": [
      -          "tidy",
      -          "most",
      -          "design"
      -        ],
      -        "type": "string"
      -      },
      -      "orientation": {
      -        "enum": [
      -          "auto",
      -          "portrait",
      -          "landscape"
      -        ],
      -        "type": "string"
      -      },
      -      "type": {
      -        "const": "modules",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  },
      -  {
      -    "additionalProperties": false,
      -    "properties": {
      -      "battery_kwh": {
      -        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      -        "type": "number"
      -      },
      -      "consumption_kwh": {
      -        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      -        "maximum": 30000,
      -        "minimum": 1000,
      -        "type": "number"
      -      },
      -      "ev": {
      -        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "heat_pump": {
      -        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      -        "type": "boolean"
      -      },
      -      "type": {
      -        "const": "household",
      -        "type": "string"
      -      }
      -    },
      -    "required": [
      -      "type"
      -    ],
      -    "type": "object"
      -  }
      -]New value: +[
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "direction_deg": {
      +        "description": "Compass direction the face looks to: 0 north, 90 east, 180 south, 270 west.",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "tilt_deg": {
      +        "description": "Tilt, degrees (0 flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "slope",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "edge": {
      +        "description": "Which edge moves: the eave, or the left or right end seen from below.",
      +        "enum": [
      +          "eave",
      +          "left",
      +          "right"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "metres": {
      +        "description": "Or a change: metres out from where the edge is now (negative: in), horizontal.",
      +        "maximum": 1.5,
      +        "minimum": -1.5,
      +        "type": "number"
      +      },
      +      "to_m": {
      +        "description": "Eave only: how far the eave should reach past the wall, horizontal metres (\"Dachüberstand 50 cm\" → 0.5). The face list shows the current eave_overhang_m.",
      +        "maximum": 1.5,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "overhang",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "edge"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A cut along the eave, this far up from it.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "A cut up the slope, this far from the left end.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "split",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "restore_face",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Out from the wall, metres.",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "direction_deg": {
      +        "description": "Way the roof slopes down to (default: away from the wall).",
      +        "maximum": 360,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "eave_height_m": {
      +        "description": "Height of the roof's lowest edge above the ground, metres.",
      +        "maximum": 15,
      +        "minimum": 1.5,
      +        "type": "number"
      +      },
      +      "offset_m": {
      +        "description": "From the middle of the wall: positive to the east (north and south walls) or north (east and west walls).",
      +        "maximum": 30,
      +        "minimum": -30,
      +        "type": "number"
      +      },
      +      "tilt_deg": {
      +        "description": "Roof tilt, degrees (default 0, flat).",
      +        "maximum": 60,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_face",
      +        "type": "string"
      +      },
      +      "wall": {
      +        "description": "The house wall the new part stands against (an extension, a garage, a porch).",
      +        "enum": [
      +          "north",
      +          "east",
      +          "south",
      +          "west"
      +        ],
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Along the wall, metres.",
      +        "maximum": 30,
      +        "minimum": 1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "wall",
      +      "width_m",
      +      "depth_m",
      +      "eave_height_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "Its depth up the roof, metres in the slope (a roof window \"78 × 140\": 1.4); a dormer's horizontally, as on a drawing from above. A ladder starts at from_eave_m and runs this far up; without it a ladder runs from the eave to the ridge.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from the last answer's \"faces\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "Horizontal metres up from the eave (as on a drawing seen from above).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "height_m": {
      +        "description": "How high it stands above the roof, metres (a dormer: its front wall; default: the kind's usual height).",
      +        "maximum": 4,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "kind": {
      +        "description": "What it is: chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall, or block (anything else, 60 cm square).",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "add_obstacle",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "Its width along the eave, metres in the roof's slope (a dormer, a roof window \"78 × 140\": 0.78). Default: the kind's usual size.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "kind",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "depth_m": {
      +        "description": "A new depth up the roof, metres in the slope (a dormer's horizontally).",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      },
      +      "id": {
      +        "description": "An obstacle id from \"obstacles\" or \"your_obstacles\" (e.g. a dormer the scan found).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "kind": {
      +        "description": "The new kind. Dormers: dormerShed, dormerFlat, dormerGable, dormerTwin, dormerHip, dormerPoint, dormerTrapez, dormerBarrel, dormerBat, dormerWall (a dormer stays its size). Or chimney, window, vent, flue, dish, antenna, ladder, snowWood, snowGrid, steps, solarThermal, pv, dome, block.",
      +        "enum": [
      +          "chimney",
      +          "window",
      +          "vent",
      +          "flue",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome",
      +          "dormerShed",
      +          "dormerFlat",
      +          "dormerGable",
      +          "dormerTwin",
      +          "dormerHip",
      +          "dormerPoint",
      +          "dormerTrapez",
      +          "dormerBarrel",
      +          "dormerBat",
      +          "dormerWall",
      +          "block"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "change_kind",
      +        "type": "string"
      +      },
      +      "width_m": {
      +        "description": "A new width along the eave, metres in the slope.",
      +        "maximum": 12,
      +        "minimum": 0.1,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id",
      +      "kind"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "colour": {
      +        "description": "A colour id. metal: anthracite, aluminium, zinc, copper, brown; block: wall, grey, anthracite, red, brown; window: silver, anthracite, white, brown; chimney: render, brick, slate, steel, copper; flue: steel, anthracite, red; vent: anthracite, red, brown, grey; dish: light, anthracite, red; antenna: silver, anthracite; ladder: anthracite, red, brown, galvanised; snowWood: natural, dark, weathered; snowGrid: anthracite, red, brown, galvanised; steps: anthracite, red, brown, galvanised; solarThermal: silver, black; pv: silver, black; dome: opal, clear",
      +        "maxLength": 30,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "With item \"metal\": only this face's sheet (a copper garage beside a zinc roof).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "item": {
      +        "description": "What gets the colour: metal (sheet-metal dormer tops and Falz- or Trapezblech roofs), or a kind of item.",
      +        "enum": [
      +          "metal",
      +          "block",
      +          "window",
      +          "chimney",
      +          "flue",
      +          "vent",
      +          "dish",
      +          "antenna",
      +          "ladder",
      +          "snowWood",
      +          "snowGrid",
      +          "steps",
      +          "solarThermal",
      +          "pv",
      +          "dome"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "colour",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "item",
      +      "colour"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "covering": {
      +        "description": "The roof's covering: tiles, biber (Biberschwanz plain tiles), flach (flat tiles such as Tegalit), falz (standing-seam sheet, modules flush) or trapez (trapezoidal sheet).",
      +        "enum": [
      +          "tiles",
      +          "biber",
      +          "flach",
      +          "falz",
      +          "trapez"
      +        ],
      +        "type": "string"
      +      },
      +      "face": {
      +        "description": "Only this face (a garage in Falzblech, say); without it the whole roof.",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "flat_rack": {
      +        "description": "Racks on flat roofs: ew (east-west pairs back to back, default), south (rows facing south), ns (along the roof).",
      +        "enum": [
      +          "ew",
      +          "south",
      +          "ns"
      +        ],
      +        "type": "string"
      +      },
      +      "rack_tilt_deg": {
      +        "anyOf": [
      +          {
      +            "const": 5,
      +            "type": "number"
      +          },
      +          {
      +            "const": 10,
      +            "type": "number"
      +          },
      +          {
      +            "const": 15,
      +            "type": "number"
      +          }
      +        ],
      +        "description": "Tilt of the racks on flat roofs and racked trapezoidal sheet: 5, 10 (default) or 15 degrees."
      +      },
      +      "trapez_racks": {
      +        "description": "Trapezoidal sheet sloping 15° or less: modules on racks instead of flush.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "covering",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\". Its modules stay exactly where they are from now on (the other module edits do this by themselves).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "lay_by_hand",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "columns": {
      +        "description": "Modules side by side (default 1).",
      +        "maximum": 20,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "The field's lower left corner: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "The field's lower left corner: metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "orientation": {
      +        "description": "Default portrait (landscape if the plan is set to landscape).",
      +        "enum": [
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "rows": {
      +        "description": "Rows up the roof (default 1).",
      +        "maximum": 10,
      +        "minimum": 1,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "add_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on the module to take off, or one corner of an area: horizontal metres up from the eave.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_eave_m": {
      +        "description": "The area's other corner (every module whose centre lies inside goes).",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "to_left_m": {
      +        "description": "The area's other corner.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "remove_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field: the modules joined to the nearest one move together.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "right_m": {
      +        "description": "Metres to the right seen from below (negative: left).",
      +        "maximum": 40,
      +        "minimum": -40,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "move_field",
      +        "type": "string"
      +      },
      +      "up_m": {
      +        "description": "Metres up the roof, in its slope (negative: down).",
      +        "maximum": 20,
      +        "minimum": -20,
      +        "type": "number"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "face": {
      +        "description": "A face id from \"faces\" or \"dormer_roofs\".",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "from_eave_m": {
      +        "description": "A spot on (or next to) the field.",
      +        "maximum": 30,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "from_left_m": {
      +        "description": "Metres from the face's left end, seen from below.",
      +        "maximum": 40,
      +        "minimum": 0,
      +        "type": "number"
      +      },
      +      "type": {
      +        "const": "turn_field",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "face",
      +      "from_eave_m",
      +      "from_left_m"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "description": "An obstacle id (\"u:<n>\" for one added before).",
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "remove_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "id": {
      +        "maxLength": 80,
      +        "minLength": 1,
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "confirm_obstacle",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "id"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "on": {
      +        "description": "Modules on flat and shed dormers too (gable dormers always carry them).",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "dormer_modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type",
      +      "on"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "arrange": {
      +        "description": "tidy: even fields (default); most: as many as fit; design: clean rectangles only.",
      +        "enum": [
      +          "tidy",
      +          "most",
      +          "design"
      +        ],
      +        "type": "string"
      +      },
      +      "orientation": {
      +        "enum": [
      +          "auto",
      +          "portrait",
      +          "landscape"
      +        ],
      +        "type": "string"
      +      },
      +      "type": {
      +        "const": "modules",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  },
      +  {
      +    "additionalProperties": false,
      +    "properties": {
      +      "battery_kwh": {
      +        "description": "Battery size, kWh: one of 0, 5, 8, 10, 15 (default 8).",
      +        "type": "number"
      +      },
      +      "consumption_kwh": {
      +        "description": "The household's electricity use a year, kWh, without car or heat pump (default 4,500).",
      +        "maximum": 30000,
      +        "minimum": 1000,
      +        "type": "number"
      +      },
      +      "ev": {
      +        "description": "An electric car is (or will be) charged at home: about 2,700 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "heat_pump": {
      +        "description": "A heat pump heats the house: about 4,500 kWh a year more.",
      +        "type": "boolean"
      +      },
      +      "type": {
      +        "const": "household",
      +        "type": "string"
      +      }
      +    },
      +    "required": [
      +      "type"
      +    ],
      +    "type": "object"
      +  }
      +]
  13. Added

TDQS

A3.6/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly, idempotent and non-destructive, and the description reinforces this with a concrete guarantee: 'The old link keeps working; nothing is stored.' Beyond that it discloses return behaviour ('Returns the new link and figures, one sentence per change to confirm with the person, and the edits it turned down with the reason'), id stability ('Own obstacle ids (u:<n>) keep their numbers for the whole call'), and that each applied sentence reports module deltas. That is substantive context on top of the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness2/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The first sentence is well front-loaded, but the body is a single enormous run-on sentence enumerating roughly forty edit types in comma-separated prose with no grouping or line breaks. Nearly every clause carries information, yet the density makes it hard to scan for the one edit an agent needs. Structure, not content, is the failure here.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a tool with this many edit variants, the description covers what an agent needs before calling: how to source numbers, how faces are identified and disambiguated, the coordinate frame, id stability across a call, and what the answer returns. The remaining detail (ranges, enums, defaults, dry_run) lives in a very rich schema. Only the absence of explicit ordering versus plan_roof keeps it from full marks.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

At 75% schema coverage the schema carries most per-field meaning, but the description adds a coordinate convention not obvious from any single field: 'Places on a face are metres up from the eave (horizontal, as on a drawing from above) and from the face's left end seen from below, standing in front of the roof.' It also explains how to disambiguate faces using facing, building, size and eave_overhang_m from plan_roof's "faces". The edit-type prose largely mirrors the oneOf branches, so the value is in the conventions rather than the enumeration.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The opening states a specific verb and resource: 'Improve a solar roof check plan from what the person knows: a building plan, photos, measurements or answers.' The edit catalogue that follows makes clear it applies structured changes rather than creating a plan, and it names plan_roof's "faces" as the id source, so the plan_roof → edit_roof relationship is inferable. It stops short of an explicit statement that plan_roof must run first, so sibling separation is implied rather than declared.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

There is real guidance on how to use it — 'Read the numbers yourself (tilt, overhang, sizes, where a chimney is) and pass them as edits', 'when two could fit, ask the person', 'To rebuild a layout the person drew, lay the face by hand and place its fields'. But there is no explicit when-to-use/when-not versus plan_roof, size_system or area_figures, and no statement of prerequisites beyond the link. Usage is implied by the edit list rather than routed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Try in Browser

Glama MCP Gateway

Add one secure layer between your agents and this server.

Resources