{
 "$schema": "https://json-schema.org/draft/2020-12/schema",
 "$id": "https://heatandtimber.com/data/schema/product.schema.json",
 "title": "Heat & Timber product record",
 "type": "object",
 "description": "One product model as listed by one shop: a record in products (collections) or product (single files) of the Heat & Timber data files. Values from shop data carry evidence; computed values are calculated by Heat & Timber.",
 "required": [
  "id",
  "url",
  "name",
  "full_name",
  "brand",
  "shop",
  "country",
  "currency",
  "category",
  "type",
  "type_label",
  "price",
  "in_stock",
  "specs",
  "evidence",
  "variants",
  "computed",
  "source",
  "shop_link_paid",
  "links"
 ],
 "properties": {
  "id": {
   "type": "string",
   "description": "Identifier of the listing, unique, assigned by Heat & Timber."
  },
  "url": {
   "type": "string",
   "pattern": "^https://heatandtimber\\.com/(us|uk)/product/[^/?#]+$",
   "description": "Product page on heatandtimber.com, unique, by Heat & Timber. It quotes the shop listing, links to the shop and asks the shop for the current price."
  },
  "name": {
   "type": "string",
   "description": "Product name checked by Heat & Timber, without the brand unless the brand carries the model; not unique (two listings can share a name)."
  },
  "full_name": {
   "type": "string",
   "description": "name with \" by <brand>\" where the name does not start with the brand, composed by Heat & Timber."
  },
  "brand": {
   "type": [
    "string",
    "null"
   ],
   "description": "Brand confirmed from shop data (vendor field or the shop's own statement). null: not confirmed."
  },
  "shop": {
   "type": "object",
   "description": "Shop that lists the product.",
   "required": [
    "id",
    "name"
   ],
   "properties": {
    "id": {
     "type": "string",
     "description": "Shop identifier, assigned by Heat & Timber."
    },
    "name": {
     "type": "string",
     "description": "Shop name as the shop writes it (shop data)."
    }
   }
  },
  "country": {
   "type": "string",
   "enum": [
    "us",
    "uk"
   ],
   "description": "Country of the shop and of the page, set by Heat & Timber."
  },
  "currency": {
   "type": "string",
   "enum": [
    "USD",
    "GBP"
   ],
   "description": "Currency of all amounts in the record (ISO 4217), as listed by the shop."
  },
  "category": {
   "type": "string",
   "enum": [
    "sauna",
    "cold-plunge",
    "hot-tub",
    "shed",
    "log-cabin",
    "summerhouse",
    "garden-room",
    "pergola"
   ],
   "description": "Category, classified by Heat & Timber."
  },
  "type": {
   "type": [
    "string",
    "null"
   ],
   "enum": [
    "barrel",
    "cabin",
    "indoor",
    "infrared",
    "hybrid",
    "tent",
    "tub-only",
    "with-chiller",
    "chiller-only",
    "hard-shell",
    "inflatable",
    "portable",
    "wood-fired",
    "swim-spa",
    "wood",
    "metal",
    "plastic",
    "louvred",
    "fixed-roof",
    null
   ],
   "description": "Type within the category, classified by Heat & Timber from the shop listing. null: not classified."
  },
  "type_label": {
   "type": "string",
   "description": "Type in words, as on the product page, by Heat & Timber."
  },
  "price": {
   "type": "object",
   "description": "Price as listed by the shop at the last reading.",
   "required": [
    "amount",
    "amount_max",
    "installed",
    "kind",
    "text",
    "checked_at"
   ],
   "properties": {
    "amount": {
     "type": "number",
     "description": "Lowest price in currency at checked_at (shop data)."
    },
    "amount_max": {
     "type": "number",
     "description": "Highest price in currency at checked_at (shop data)."
    },
    "installed": {
     "type": [
      "number",
      "null"
     ],
     "description": "Price with installation at checked_at (shop data). null: not offered."
    },
    "kind": {
     "type": "string",
     "enum": [
      "single",
      "from",
      "range"
     ],
     "description": "How text shows the price: one amount, from the lowest variant, or a range; set by Heat & Timber from the shop prices."
    },
    "text": {
     "type": "string",
     "description": "The price in currency as shown on the product page, formatted by Heat & Timber from amount and amount_max."
    },
    "checked_at": {
     "type": "string",
     "pattern": "^\\d{4}-\\d{2}-\\d{2}(T\\d{2}:\\d{2}:\\d{2}Z)?$",
     "description": "Time of our last reading of the shop in UTC (RFC 3339), recorded by Heat & Timber; a date only for older data."
    }
   }
  },
  "in_stock": {
   "type": "boolean",
   "description": "Stock at price.checked_at (shop data)."
  },
  "specs": {
   "type": "object",
   "description": "Specifications of the category, read from shop data (see evidence); keys depend on category."
  },
  "evidence": {
   "type": "object",
   "description": "Excerpt from shop data for each key of specs that has a value, and for brand.",
   "required": [],
   "properties": {
    "seats": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "seats_min": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "power_kw": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "voltage_v": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "current_a": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "plug_in": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "wood": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "jets": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "width_m": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "depth_m": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "height_m": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "eaves_height_m": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "wall_thickness_mm": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "wall_thickness_options_mm": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    },
    "brand": {
     "type": "object",
     "description": "Excerpt from the shop listing that the value was read from.",
     "required": [
      "quote",
      "from"
     ],
     "properties": {
      "quote": {
       "type": "string",
       "description": "Excerpt as shown on the product page, quoted from the shop; prices and web addresses replaced by [...]."
      },
      "from": {
       "type": "string",
       "enum": [
        "title",
        "description",
        "size_option",
        "attribute",
        "vendor",
        "shop_statement",
        "product_data"
       ],
       "description": "Part of the listing the excerpt comes from (shop data)."
      },
      "note": {
       "type": "string",
       "description": "Remark by Heat & Timber on how the value relates to the excerpt."
      }
     }
    }
   }
  },
  "variants": {
   "type": "array",
   "description": "Variants as listed by the shop when there are at least two (shop data); else empty.",
   "items": {
    "type": "object",
    "description": "One variant (size or option) as listed by the shop.",
    "required": [
     "name",
     "amount",
     "in_stock",
     "width_m",
     "depth_m",
     "wall_thickness_mm",
     "floor_area_m2",
     "price_per_m2",
     "page"
    ],
    "properties": {
     "name": {
      "type": "string",
      "description": "Variant name as listed by the shop (shop data), web addresses and prices replaced by [...]."
     },
     "amount": {
      "type": "number",
      "description": "Price of the variant in the record currency at price.checked_at (shop data)."
     },
     "in_stock": {
      "type": "boolean",
      "description": "Stock of the variant at price.checked_at (shop data)."
     },
     "width_m": {
      "type": [
       "number",
       "null"
      ],
      "description": "Width in metres from the variant name (shop data)."
     },
     "depth_m": {
      "type": [
       "number",
       "null"
      ],
      "description": "Depth in metres from the variant name (shop data)."
     },
     "wall_thickness_mm": {
      "type": [
       "number",
       "null"
      ],
      "description": "Wall thickness in millimetres from the variant name (shop data)."
     },
     "floor_area_m2": {
      "type": [
       "number",
       "null"
      ],
      "description": "Width times depth in m², computed by Heat & Timber."
     },
     "price_per_m2": {
      "type": [
       "number",
       "null"
      ],
      "description": "amount divided by floor_area_m2, rounded, computed by Heat & Timber."
     },
     "page": {
      "type": [
       "string",
       "null"
      ],
      "pattern": "^https://heatandtimber\\.com/.*$",
      "description": "Row of this size in the size table of the product page (unique anchor). null: no size."
     }
    }
   }
  },
  "computed": {
   "type": "object",
   "description": "Values computed by Heat & Timber; null where not applicable.",
   "required": [
    "size_basis",
    "floor_area_m2",
    "price_per_m2",
    "running_cost",
    "running_cost_reason",
    "planning_uk",
    "permit_us"
   ],
   "properties": {
    "size_basis": {
     "type": [
      "string",
      "null"
     ],
     "enum": [
      "cheapest_size",
      null
     ],
     "description": "cheapest_size: width_m, depth_m, wall_thickness_mm, floor_area_m2, price_per_m2 and the links refer to the cheapest of several sizes. null: one size."
    },
    "floor_area_m2": {
     "type": [
      "number",
      "null"
     ],
     "description": "Width times depth in m², computed by Heat & Timber. null: footprint not stated."
    },
    "price_per_m2": {
     "type": [
      "number",
      "null"
     ],
     "description": "Price of that size divided by floor_area_m2, rounded, computed by Heat & Timber."
    },
    "running_cost": {
     "type": [
      "object",
      "null"
     ],
     "description": "Cost of one hour at full power, computed by Heat & Timber. null: no electric heating or cooling, power not stated, or no calculator for the category.",
     "required": [
      "per_hour",
      "text",
      "hour_of",
      "power_kw",
      "rate_per_kwh",
      "rate_basis",
      "rate_data"
     ],
     "properties": {
      "per_hour": {
       "type": "number",
       "description": "Cost of one hour at full power in the record currency, computed by Heat & Timber."
      },
      "text": {
       "type": "string",
       "description": "per_hour in words as shown on the product page (\"£0.57 per hour of use\"), by Heat & Timber."
      },
      "hour_of": {
       "type": "string",
       "enum": [
        "use",
        "heating",
        "cooling"
       ],
       "description": "What the hour is, set by Heat & Timber from the category: use (sauna), heating (hot tub), cooling (cold plunge)."
      },
      "power_kw": {
       "type": "number",
       "description": "Power in kW used, from specs.power_kw (shop data)."
      },
      "rate_per_kwh": {
       "type": "number",
       "description": "Electricity price per kWh in pounds or dollars (four decimals), from Ofgem (UK) or the EIA (US)."
      },
      "rate_basis": {
       "type": "string",
       "description": "Source (Ofgem price cap or EIA average) and period of the electricity price, from rate_data."
      },
      "rate_data": {
       "type": "string",
       "pattern": "^https://heatandtimber\\.com/.*$",
       "description": "Data file of Heat & Timber with the electricity prices and their sources."
      }
     }
    },
    "running_cost_reason": {
     "type": [
      "string",
      "null"
     ],
     "enum": [
      "power_not_stated",
      "no_electric_heating",
      "no_calculator",
      null
     ],
     "description": "Why running_cost is null for saunas, hot tubs and cold plunges. null: running_cost is set or not applicable."
    },
    "planning_uk": {
     "type": [
      "object",
      "null"
     ],
     "description": "Planning permission estimate for UK garden buildings with a stated height, computed by Heat & Timber from national permitted development rules. Not legal advice; see caveat. null: not a UK garden building or height not stated.",
     "required": [
      "summary",
      "status",
      "nations",
      "inputs",
      "caveat",
      "checker"
     ],
     "properties": {
      "summary": {
       "type": "string",
       "description": "Summary over the four nations, as on the product page."
      },
      "status": {
       "type": [
        "string",
        "null"
       ],
       "enum": [
        "likely_not_needed",
        "likely_needed",
        null
       ],
       "description": "likely_not_needed everywhere, likely_needed everywhere, null when mixed."
      },
      "nations": {
       "type": "object",
       "description": "Estimate per nation.",
       "required": [
        "england",
        "wales",
        "scotland",
        "northern_ireland"
       ],
       "properties": {
        "england": {
         "type": "object",
         "description": "Estimate for one nation, computed by Heat & Timber.",
         "required": [
          "result",
          "boundary_distance_m"
         ],
         "properties": {
          "result": {
           "type": "string",
           "enum": [
            "likely_not_needed",
            "likely_not_needed_away_from_boundary",
            "likely_needed"
           ],
           "description": "likely_not_needed (even close to the boundary), likely_not_needed_away_from_boundary (only more than boundary_distance_m from the boundary), likely_needed."
          },
          "boundary_distance_m": {
           "type": "number",
           "description": "Distance from the boundary in metres that the national rule uses."
          }
         }
        },
        "wales": {
         "type": "object",
         "description": "Estimate for one nation, computed by Heat & Timber.",
         "required": [
          "result",
          "boundary_distance_m"
         ],
         "properties": {
          "result": {
           "type": "string",
           "enum": [
            "likely_not_needed",
            "likely_not_needed_away_from_boundary",
            "likely_needed"
           ],
           "description": "likely_not_needed (even close to the boundary), likely_not_needed_away_from_boundary (only more than boundary_distance_m from the boundary), likely_needed."
          },
          "boundary_distance_m": {
           "type": "number",
           "description": "Distance from the boundary in metres that the national rule uses."
          }
         }
        },
        "scotland": {
         "type": "object",
         "description": "Estimate for one nation, computed by Heat & Timber.",
         "required": [
          "result",
          "boundary_distance_m"
         ],
         "properties": {
          "result": {
           "type": "string",
           "enum": [
            "likely_not_needed",
            "likely_not_needed_away_from_boundary",
            "likely_needed"
           ],
           "description": "likely_not_needed (even close to the boundary), likely_not_needed_away_from_boundary (only more than boundary_distance_m from the boundary), likely_needed."
          },
          "boundary_distance_m": {
           "type": "number",
           "description": "Distance from the boundary in metres that the national rule uses."
          }
         }
        },
        "northern_ireland": {
         "type": "object",
         "description": "Estimate for one nation, computed by Heat & Timber.",
         "required": [
          "result",
          "boundary_distance_m"
         ],
         "properties": {
          "result": {
           "type": "string",
           "enum": [
            "likely_not_needed",
            "likely_not_needed_away_from_boundary",
            "likely_needed"
           ],
           "description": "likely_not_needed (even close to the boundary), likely_not_needed_away_from_boundary (only more than boundary_distance_m from the boundary), likely_needed."
          },
          "boundary_distance_m": {
           "type": "number",
           "description": "Distance from the boundary in metres that the national rule uses."
          }
         }
        }
       }
      },
      "inputs": {
       "type": "object",
       "description": "Inputs of the estimate.",
       "required": [
        "height_m",
        "eaves_height_m",
        "eaves_assumed",
        "floor_area_m2",
        "dual_pitched_roof",
        "other_inputs"
       ],
       "properties": {
        "height_m": {
         "type": "number",
         "description": "Height in metres from specs.height_m."
        },
        "eaves_height_m": {
         "type": "number",
         "description": "Eaves height in metres used; the height when eaves_assumed."
        },
        "eaves_assumed": {
         "type": "boolean",
         "description": "true: the shop does not state the eaves height, the height is used."
        },
        "floor_area_m2": {
         "type": [
          "number",
          "null"
         ],
         "description": "Floor area in m² from computed.floor_area_m2."
        },
        "dual_pitched_roof": {
         "type": "boolean",
         "const": false,
         "description": "Roof shape is not stated by the shops; the stricter case is used."
        },
        "other_inputs": {
         "type": "string",
         "description": "All other inputs as the defaults of the planning checker."
        }
       }
      },
      "caveat": {
       "type": "string",
       "description": "What the estimate is based on and what it is not, as on the product page."
      },
      "checker": {
       "type": "string",
       "pattern": "^https://heatandtimber\\.com/.*$",
       "description": "Planning permission checker with these inputs."
      }
     }
    },
    "permit_us": {
     "type": [
      "object",
      "null"
     ],
     "description": "Reading of the US model codes for saunas, hot tubs and cold plunges, by Heat & Timber. Not a ruling; see caveat. null: not a US sauna, hot tub or cold plunge.",
     "required": [
      "class",
      "summary",
      "status",
      "note",
      "caveat",
      "guide"
     ],
     "properties": {
      "class": {
       "type": "string",
       "enum": [
        "wood_stove",
        "pool_spa_code",
        "unclear",
        "electrical",
        "plug_in",
        "power_not_stated"
       ],
       "description": "Main point of the permit guide for this product."
      },
      "summary": {
       "type": [
        "string",
        "null"
       ],
       "description": "Short reading as on the product page. null: power supply not stated, no estimate."
      },
      "status": {
       "type": [
        "string",
        "null"
       ],
       "enum": [
        "no_permit_likely",
        "permit_likely",
        null
       ],
       "description": "no_permit_likely, permit_likely, or null without a clear answer."
      },
      "note": {
       "type": [
        "string",
        "null"
       ],
       "description": "\"rules vary locally\"; null without an estimate."
      },
      "caveat": {
       "type": "string",
       "description": "What the reading is and what it is not, as on the product page."
      },
      "guide": {
       "type": "string",
       "pattern": "^https://heatandtimber\\.com/.*$",
       "description": "Permit guide with the inputs of this product."
      }
     }
    }
   }
  },
  "source": {
   "type": "object",
   "description": "Where the data comes from, recorded by Heat & Timber.",
   "required": [
    "shop",
    "feed",
    "checked_on"
   ],
   "properties": {
    "shop": {
     "type": "string",
     "description": "Shop identifier, as in shop.id."
    },
    "feed": {
     "type": "string",
     "description": "Public product data read: shopify_products_json or woocommerce_store_api."
    },
    "checked_on": {
     "type": "string",
     "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
     "description": "Date of the reading (UTC), recorded by Heat & Timber."
    }
   }
  },
  "shop_link_paid": {
   "type": "boolean",
   "description": "true: the shop link on the product page is a paid link, marked Ad (Heat & Timber partner programmes). The data files contain no paid links."
  },
  "links": {
   "type": "object",
   "description": "Pages of Heat & Timber for this product, addresses on heatandtimber.com.",
   "required": [
    "finder",
    "running_cost_calculator",
    "planning_checker",
    "permit_guide"
   ],
   "properties": {
    "finder": {
     "type": [
      "string",
      "null"
     ],
     "pattern": "^https://heatandtimber\\.com/.*$",
     "description": "Finder of Heat & Timber showing this product. null: no finder for the category in the country."
    },
    "running_cost_calculator": {
     "type": [
      "string",
      "null"
     ],
     "pattern": "^https://heatandtimber\\.com/.*$",
     "description": "Running cost calculator of Heat & Timber for this product or category. null: not applicable."
    },
    "planning_checker": {
     "type": [
      "string",
      "null"
     ],
     "pattern": "^https://heatandtimber\\.com/.*$",
     "description": "UK planning permission checker of Heat & Timber. null: not applicable."
    },
    "permit_guide": {
     "type": [
      "string",
      "null"
     ],
     "pattern": "^https://heatandtimber\\.com/.*$",
     "description": "US permit guide of Heat & Timber. null: not applicable."
    }
   }
  }
 },
 "allOf": [
  {
   "if": {
    "required": [
     "category"
    ],
    "properties": {
     "category": {
      "enum": [
       "sauna",
       "cold-plunge"
      ]
     }
    }
   },
   "then": {
    "properties": {
     "specs": {
      "type": "object",
      "description": "Specifications of the category, each quoted from the shop listing (see evidence); null means not stated.",
      "required": [
       "seats",
       "seats_min",
       "power_kw",
       "voltage_v",
       "current_a",
       "plug_in",
       "wood"
      ],
      "properties": {
       "seats": {
        "type": [
         "integer",
         "null"
        ],
        "description": "Seats or persons, quoted from the shop; with a range (\"4-6 Person\") the upper end. null: not stated."
       },
       "seats_min": {
        "type": [
         "integer",
         "null"
        ],
        "description": "Lower end of a range of seats, quoted from the shop. null: no range."
       },
       "power_kw": {
        "type": [
         "number",
         "null"
        ],
        "description": "Heater power in kW (for cold plunges the chiller), quoted from the shop. null: not stated."
       },
       "voltage_v": {
        "type": [
         "integer",
         "null"
        ],
        "enum": [
         120,
         240,
         null
        ],
        "description": "Supply voltage class in volts (120 or 240), quoted from the shop; read for US listings only. A shop figure of 110 V counts as 120 V, 220 V as 240 V. null: not stated."
       },
       "current_a": {
        "type": [
         "number",
         "null"
        ],
        "description": "Supply current in amps, quoted from the shop. null: not stated."
       },
       "plug_in": {
        "type": [
         "boolean",
         "null"
        ],
        "enum": [
         true,
         null
        ],
        "description": "true: plugs into a standard socket (120 V stated, or 13 A with a stated plug), from shop data. null: not stated or hard-wired; never false."
       },
       "wood": {
        "type": [
         "string",
         "null"
        ],
        "description": "Main wood in lower case as stated by the shop (\"cedar\", \"thermowood\"). null: not stated."
       }
      }
     }
    }
   }
  },
  {
   "if": {
    "required": [
     "category"
    ],
    "properties": {
     "category": {
      "enum": [
       "hot-tub"
      ]
     }
    }
   },
   "then": {
    "properties": {
     "specs": {
      "type": "object",
      "description": "Specifications of the category, each quoted from the shop listing (see evidence); null means not stated.",
      "required": [
       "seats",
       "seats_min",
       "power_kw",
       "voltage_v",
       "current_a",
       "plug_in",
       "jets",
       "wood"
      ],
      "properties": {
       "seats": {
        "type": [
         "integer",
         "null"
        ],
        "description": "Seats or persons, quoted from the shop; with a range (\"4-6 Person\") the upper end. null: not stated."
       },
       "seats_min": {
        "type": [
         "integer",
         "null"
        ],
        "description": "Lower end of a range of seats, quoted from the shop. null: no range."
       },
       "power_kw": {
        "type": [
         "number",
         "null"
        ],
        "description": "Heater power in kW (for cold plunges the chiller), quoted from the shop. null: not stated."
       },
       "voltage_v": {
        "type": [
         "integer",
         "null"
        ],
        "enum": [
         120,
         240,
         null
        ],
        "description": "Supply voltage class in volts (120 or 240), quoted from the shop; read for US listings only. A shop figure of 110 V counts as 120 V, 220 V as 240 V. null: not stated."
       },
       "current_a": {
        "type": [
         "number",
         "null"
        ],
        "description": "Supply current in amps, quoted from the shop. null: not stated."
       },
       "plug_in": {
        "type": [
         "boolean",
         "null"
        ],
        "enum": [
         true,
         null
        ],
        "description": "true: plugs into a standard socket (120 V stated, or 13 A with a stated plug), from shop data. null: not stated or hard-wired; never false."
       },
       "jets": {
        "type": [
         "integer",
         "null"
        ],
        "description": "Number of jets, quoted from the shop. null: not stated."
       },
       "wood": {
        "type": [
         "string",
         "null"
        ],
        "description": "Main wood in lower case as stated by the shop (\"cedar\", \"thermowood\"). null: not stated."
       }
      }
     }
    }
   }
  },
  {
   "if": {
    "required": [
     "category"
    ],
    "properties": {
     "category": {
      "enum": [
       "shed",
       "log-cabin",
       "summerhouse",
       "garden-room",
       "pergola"
      ]
     }
    }
   },
   "then": {
    "properties": {
     "specs": {
      "type": "object",
      "description": "Specifications of the category, each quoted from the shop listing (see evidence); null means not stated.",
      "required": [
       "width_m",
       "depth_m",
       "height_m",
       "eaves_height_m",
       "wall_thickness_mm",
       "wall_thickness_options_mm",
       "wood"
      ],
      "properties": {
       "width_m": {
        "type": [
         "number",
         "null"
        ],
        "description": "Width of the footprint in metres, quoted from the shop; for models with several sizes the cheapest size (computed.size_basis). null: not stated."
       },
       "depth_m": {
        "type": [
         "number",
         "null"
        ],
        "description": "Depth of the footprint in metres, quoted from the shop; same size as width_m. null: not stated."
       },
       "height_m": {
        "type": [
         "number",
         "null"
        ],
        "description": "Ridge or overall height in metres, quoted from the shop. null: not stated."
       },
       "eaves_height_m": {
        "type": [
         "number",
         "null"
        ],
        "description": "Eaves height in metres, quoted from the shop. null: not stated."
       },
       "wall_thickness_mm": {
        "type": [
         "number",
         "null"
        ],
        "description": "Wall thickness in millimetres of the cheapest size, quoted from the shop. null: not stated."
       },
       "wall_thickness_options_mm": {
        "type": "array",
        "items": {
         "type": "number"
        },
        "description": "Wall thicknesses in millimetres the shop offers for this model, ascending, from shop data; empty with one or no choice."
       },
       "wood": {
        "type": [
         "string",
         "null"
        ],
        "description": "Main wood in lower case as stated by the shop (\"cedar\", \"thermowood\"). null: not stated."
       }
      }
     }
    }
   }
  }
 ]
}
