{
  "openapi": "3.1.1",
  "info": {
    "title": "Laundry Drying Forecast API",
    "version": "1.0.0",
    "summary": "Which days dry laundry outside: rain, humidity, wind and temperature from MET Norway.",
    "description": "Every endpoint performs the same calculation as the human page and returns inputs, result, units, assumptions, sources, warnings and computedAt. MCP endpoint: /mcp."
  },
  "servers": [
    {
      "url": "https://hearthdryingforecast.usefulweb.app"
    }
  ],
  "paths": {
    "/api/v1/calculate": {
      "post": {
        "operationId": "calculate",
        "summary": "Which days dry laundry outside: rain, humidity, wind and temperature from MET Norway.",
        "description": "Rates the next days for drying laundry outdoors at your place. It reads the MET Norway forecast hour by hour, finds the longest dry stretch in your drying hours and estimates how fast washing dries from temperature, humidity and wind (Penman evaporation), with a verdict per day.\n\nMethodology:\n- Fetches the MET Norway Locationforecast 2.0 for the place rounded to 0.01° (about 1 km) and caches it until the time in MET’s Expires header; stale copies are revalidated with If-Modified-Since, as MET’s terms of service ask.\n- Uses hourly steps where MET provides them (the first two to three days) and splits later 6-hour steps into hours with linearly interpolated temperature, humidity and wind.\n- An hour is wet with at least 0.1 mm of rain in a 1-hour step, at least 0.2 mm in a 6-hour step, or a stated rain chance of 40 % or more. The longest run of dry hours inside your drying hours is the day’s drying window.\n- Drying speed is the Penman (1948) open-water evaporation from wind and vapour-pressure deficit: E = 2.6 (1 + 0.536 u2) (es − ea) mm/day, with es from FAO-56 eq. 11, ea = RH · es and the 10 m wind reduced to 2 m (FAO-56 eq. 47). Sunshine is not added, so the estimate is for drying in the shade.\n- A light load counts as dry after 1.2 mm of evaporation potential, a heavy load after 2.4 mm (rules of thumb: a warm, breezy afternoon dries shirts in about three hours). Verdict: great ≥ 1.5 × the threshold, good ≥ 1 ×, partly ≥ 0.6 × (finish indoors), else dry indoors.\n\nForecasts can change quickly, especially showers. The drying times are estimates for shade and average fabrics; sunshine dries faster, thick seams slower.",
        "tags": [
          "apps"
        ],
        "x-mcp-tool": "hearth_drying_forecast",
        "x-owner-app": "hearth",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HearthDryingForecastInput"
              },
              "examples": {
                "example1": {
                  "summary": "Berlin, light load, 8:00–19:00",
                  "value": {
                    "place": "52.52, 13.40",
                    "timeZone": "Europe/Berlin",
                    "load": "light",
                    "fromHour": 8,
                    "toHour": 19,
                    "days": 7
                  }
                },
                "example2": {
                  "summary": "Manchester, towels, 9:00–17:00",
                  "value": {
                    "place": "53.48, -2.24",
                    "timeZone": "Europe/London",
                    "load": "heavy",
                    "fromHour": 9,
                    "toHour": 17,
                    "days": 5
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calculation result envelope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HearthDryingForecastResult"
                }
              }
            }
          },
          "400": {
            "description": "Malformed JSON body.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input (see errors[]).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (see Retry-After).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/tool": {
      "get": {
        "operationId": "getToolDefinition",
        "summary": "Inputs, output schema and examples for this tool",
        "responses": {
          "200": {
            "description": "Tool definition.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 problem details. 422 responses include per-field `errors`.",
        "required": [
          "type",
          "title",
          "status",
          "detail"
        ],
        "properties": {
          "type": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "code": {
            "type": "string"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "field",
                "message"
              ],
              "properties": {
                "field": {
                  "type": "string"
                },
                "message": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "HearthDryingForecastInput": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "place": {
            "title": "Place (latitude, longitude)",
            "description": "Decimal degrees such as “52.52, 13.40”, or degrees-minutes-seconds. Rounded to 0.01° (about 1 km) before the request.",
            "type": "string",
            "maxLength": 80,
            "default": "52.52, 13.40"
          },
          "timeZone": {
            "title": "Time zone of the place",
            "description": "IANA name such as Europe/Berlin, Europe/London or America/New_York. Hours and dates are shown in this zone.",
            "type": "string",
            "maxLength": 40,
            "default": "Europe/Berlin"
          },
          "load": {
            "title": "Load",
            "type": "string",
            "enum": [
              "light",
              "heavy"
            ],
            "default": "light"
          },
          "fromHour": {
            "title": "Hang out from (hour)",
            "type": "integer",
            "minimum": 0,
            "maximum": 22,
            "default": 8
          },
          "toHour": {
            "title": "Bring in by (hour)",
            "type": "integer",
            "minimum": 1,
            "maximum": 24,
            "default": 19
          },
          "days": {
            "title": "Days ahead",
            "description": "MET Norway forecasts about nine days ahead: hourly for the first two to three days, then in 6-hour steps.",
            "type": "integer",
            "minimum": 1,
            "maximum": 9,
            "default": 7
          }
        },
        "required": [
          "place"
        ]
      },
      "HearthDryingForecastResult": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schemaVersion",
          "tool",
          "computedAt",
          "inputs",
          "summary",
          "result",
          "units",
          "assumptions",
          "sources",
          "warnings",
          "presentation",
          "links"
        ],
        "properties": {
          "schemaVersion": {
            "type": "string",
            "const": "1.0"
          },
          "tool": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "id",
              "name",
              "version"
            ],
            "properties": {
              "id": {
                "type": "string"
              },
              "name": {
                "type": "string"
              },
              "version": {
                "type": "string"
              }
            }
          },
          "computedAt": {
            "type": "string",
            "format": "date-time"
          },
          "inputs": {
            "type": "object",
            "description": "Normalised inputs actually used, including applied defaults."
          },
          "summary": {
            "type": "string"
          },
          "result": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "place",
              "timeZone",
              "load",
              "thresholdMm",
              "forecastUpdated",
              "days",
              "bestDay",
              "attribution"
            ],
            "properties": {
              "place": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "lat",
                  "lon"
                ],
                "properties": {
                  "lat": {
                    "type": "number"
                  },
                  "lon": {
                    "type": "number"
                  }
                }
              },
              "timeZone": {
                "type": "string"
              },
              "load": {
                "type": "string"
              },
              "thresholdMm": {
                "type": "number"
              },
              "forecastUpdated": {
                "type": "string"
              },
              "days": {
                "type": "array",
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "date",
                    "verdict",
                    "score",
                    "bestStart",
                    "bestEnd",
                    "dryHours",
                    "hoursToDry",
                    "evaporationMm",
                    "rainMm",
                    "rainHours",
                    "maxRainChance",
                    "tempMin",
                    "tempMax",
                    "humidityMean",
                    "windMean",
                    "frost",
                    "coarse",
                    "complete"
                  ],
                  "properties": {
                    "date": {
                      "type": "string"
                    },
                    "verdict": {
                      "enum": [
                        "great",
                        "good",
                        "partly",
                        "indoors"
                      ]
                    },
                    "score": {
                      "type": "integer"
                    },
                    "bestStart": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "bestEnd": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "dryHours": {
                      "type": "integer"
                    },
                    "hoursToDry": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "evaporationMm": {
                      "type": "number"
                    },
                    "rainMm": {
                      "type": "number"
                    },
                    "rainHours": {
                      "type": "integer"
                    },
                    "maxRainChance": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "tempMin": {
                      "type": "number"
                    },
                    "tempMax": {
                      "type": "number"
                    },
                    "humidityMean": {
                      "type": "number"
                    },
                    "windMean": {
                      "type": "number"
                    },
                    "frost": {
                      "type": "boolean"
                    },
                    "coarse": {
                      "type": "boolean"
                    },
                    "complete": {
                      "type": "boolean"
                    }
                  }
                }
              },
              "bestDay": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "attribution": {
                "type": "string"
              }
            }
          },
          "units": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "assumptions": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "title"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string"
                },
                "publisher": {
                  "type": "string"
                },
                "retrievedAt": {
                  "type": "string"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "presentation": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Display blocks used by the human page (same data)."
          },
          "links": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "human",
              "api",
              "openapi",
              "methodology"
            ],
            "properties": {
              "human": {
                "type": "string",
                "format": "uri"
              },
              "api": {
                "type": "string",
                "format": "uri"
              },
              "openapi": {
                "type": "string",
                "format": "uri"
              },
              "methodology": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      }
    }
  }
}