本文介绍如何使用 Open-Meteo API 打造一个 专业级天气数据工具类,支持降雨、温度、湿度、风速、气压、辐射、土壤含水量等十几种气象要素查询,并包含 ET₀(蒸散量)、有效降雨、日累计雨量等灌区或农业常用指标的计算。
提供完整代码,开箱即用。

✨一、背景介绍

很多农业、水利、智慧灌区系统,都需要实时或短期(3–7天)天气预报数据,例如:

  • 降雨量

  • 风速风向

  • 土壤含水量

  • 气温、湿度

  • 太阳辐射

  • 蒸散量 ET₀(FAO56)

  • 有效降雨

传统商业气象 API 费用较高,而 Open-Meteo 是一个质量很高、功能丰富、免费可商用的气象服务,非常适合数据科学、物联网、农业模型等应用。


✨ 二、Open-Meteo API 优势

  • ✔ 免费,无需 Token

  • ✔ 支持全世界任意经纬度

  • ✔ 支持小时级天气(1h 颗粒度)

  • ✔ 支持未来 7–16 天预报

  • ✔ 支持几十种气象要素

  • ✔ JSON 数据结构清晰稳定

  • ✔ 适合模型计算(ET₀、供需水分析等)


✨ 三、本文目标

为了方便在 Java 项目中直接使用 Open-Meteo,我写了一个增强版工具类 OpenMeteoUtils,支持:

🔧 功能能力升级

功能说明
获取常规天气降雨、湿度、温度、风速、风向、辐射、气压
获取土壤含水量0–1cm、1–3cm、3–9cm、9–27cm、27–81cm
灌区综合接口一次性请求全部关键要素
ET₀(小时级)FAO56 简化公式
ET₀(日级)自动按天累计
日降雨量自动汇总
有效降雨P × 系数
小时级天气封装适合前端列表和图表

并提供了 缓存机制(30分钟),避免 API 请求过于频繁。

下面进入正文。


✨ 四、完整工具类代码(可直接使用)

代码全部经过整理、重构、注释,适用于任何 Spring Boot / Java 项目。。



import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zhy.irrigation.siyu.domain.OpenMeteoResponse;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Open-Meteo 天气工具类
 */
public class OpenMeteoUtils {

    private static final String BASE_URL = "https://api.open-meteo.com/v1/forecast";

    private static final HttpClient httpClient = HttpClient.newHttpClient();

    private static final ObjectMapper objectMapper = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

    /** 缓存数据 */
    private static final Map<String, OpenMeteoResponse> CACHE = new ConcurrentHashMap<>();
    private static final Map<String, Long> CACHE_TIME = new ConcurrentHashMap<>();
    private static final long CACHE_EXPIRE_MS = 30 * 60 * 1000; // 30分钟

    /** 开关:是否开启缓存 */
    private static final boolean CACHE_ENABLED = true;

    /**
     * 核心:请求 Open Meteo API
     */
    public static OpenMeteoResponse getForecast(
            double lat,
            double lon,
            LocalDateTime startTime,
            LocalDateTime endTime,
            List<String> hourlyVars
    ) throws Exception {

        LocalDate startDate = startTime.toLocalDate();
        LocalDate endDate = endTime.toLocalDate();

        String key = lat + "_" + lon + "_" + startDate + "_" + endDate + "_" + hourlyVars;

        /** 缓存逻辑 */
        if (CACHE_ENABLED && CACHE.containsKey(key)) {
            if (System.currentTimeMillis() - CACHE_TIME.get(key) < CACHE_EXPIRE_MS) {
                return CACHE.get(key);
            }
        }

        String url = buildUrl(lat, lon, startDate, endDate, hourlyVars);

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(url))
                .timeout(java.time.Duration.ofSeconds(10))
                .build();

        HttpResponse<String> resp = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        OpenMeteoResponse respObj = objectMapper.readValue(resp.body(), OpenMeteoResponse.class);

        if (CACHE_ENABLED) {
            CACHE.put(key, respObj);
            CACHE_TIME.put(key, System.currentTimeMillis());
        }

        return respObj;
    }

    /**
     * 构建 URL
     */
    private static String buildUrl(
            double lat,
            double lon,
            LocalDate start,
            LocalDate end,
            List<String> hourlyVars) {

        StringBuilder sb = new StringBuilder(BASE_URL)
                .append("?latitude=").append(lat)
                .append("&longitude=").append(lon)
                .append("&timezone=auto")
                .append("&start_date=").append(start)
                .append("&end_date=").append(end);

        if (hourlyVars != null && !hourlyVars.isEmpty()) {
            sb.append("&hourly=").append(String.join(",", hourlyVars));
        }

        return sb.toString();
    }

    // ============================================================
    //              封装的快捷获取方法
    // ============================================================

    /** 降雨 */
    public static OpenMeteoResponse getRainfall(
            double lat, double lon,
            LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("precipitation"));
    }

    /** 风速风向 */
    public static OpenMeteoResponse getWind(
            double lat, double lon,
            LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("wind_speed_10m", "wind_direction_10m"));
    }

    /** 温度 */
    public static OpenMeteoResponse getTemperatureForecast(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("temperature_2m"));
    }

    /** 湿度 */
    public static OpenMeteoResponse getHumidityForecast(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("relative_humidity_2m"));
    }

    /** 气压 */
    public static OpenMeteoResponse getPressureForecast(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("surface_pressure"));
    }

    /** 辐射(太阳短波辐射) */
    public static OpenMeteoResponse getSolarRadiation(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {
        return getForecast(lat, lon, start, end, List.of("shortwave_radiation"));
    }

    /** 灌区模型所需全部字段 */
    public static OpenMeteoResponse getIrrigationWeather(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {

        List<String> vars = List.of(
                "temperature_2m",
                "precipitation",
                "relative_humidity_2m",
                "wind_speed_10m",
                "wind_direction_10m",
                "surface_pressure",
                "shortwave_radiation"
        );

        return getForecast(lat, lon, start, end, vars);
    }

    /** 土壤含水量 */
    public static OpenMeteoResponse getSoilMoistureForecast(
            double lat, double lon, LocalDateTime start, LocalDateTime end) throws Exception {

        List<String> vars = List.of(
                "soil_moisture_0_1cm",
                "soil_moisture_1_3cm",
                "soil_moisture_3_9cm",
                "soil_moisture_9_27cm",
                "soil_moisture_27_81cm"
        );

        return getForecast(lat, lon, start, end, vars);
    }

    // ============================================================
    //              数据计算能力扩展
    // ============================================================

    /** 小时级 ET0(FAO56 简化) */
    public static List<Double> calculateET0(OpenMeteoResponse resp) {
        List<Double> list = new ArrayList<>();
        if (resp.getHourly() == null) return list;

        var temp = resp.getHourly().getTemperature_2m();
        var wind = resp.getHourly().getWind_speed_10m();
        var rh = resp.getHourly().getRelative_humidity_2m();

        for (int i = 0; i < temp.size(); i++) {
            double t = temp.get(i);
            double u = wind.get(i);
            double h = rh.get(i);

            double et0 = 0.0023 * (t + 17.8) * Math.sqrt(Math.max(u, 0.1)) * (1 - h / 100.0);

            list.add(et0);
        }
        return list;
    }

    /** 逐日 ET0(按小时求和) */
    public static Map<String, Double> calculateDailyET0(OpenMeteoResponse resp) {
        Map<String, Double> map = new LinkedHashMap<>();
        if (resp.getHourly() == null) return map;

        List<String> times = resp.getHourly().getTime();
        List<Double> et0s = calculateET0(resp);

        for (int i = 0; i < times.size(); i++) {
            String date = times.get(i).substring(0, 10);
            map.merge(date, et0s.get(i), Double::sum);
        }

        return map;
    }

    /** 有效降雨 */
    public static List<Double> calculateEffectiveRain(OpenMeteoResponse resp, double coef) {
        List<Double> result = new ArrayList<>();
        if (resp.getHourly() == null) return result;

        for (Double r : resp.getHourly().getPrecipitation()) {
            result.add(r * coef);
        }
        return result;
    }

    /** 逐日降雨累计 */
    public static Map<String, Double> calculateDailyRain(OpenMeteoResponse resp) {
        Map<String, Double> map = new LinkedHashMap<>();
        if (resp.getHourly() == null) return map;

        List<String> times = resp.getHourly().getTime();
        List<Double> rain = resp.getHourly().getPrecipitation();

        for (int i = 0; i < times.size(); i++) {
            String date = times.get(i).substring(0, 10);
            map.merge(date, rain.get(i), Double::sum);
        }

        return map;
    }

    /**
     * 组装小时级天气对象
     */
    public static List<Map<String, Object>> assembleHourlyWeather(OpenMeteoResponse resp) {
        List<Map<String, Object>> list = new ArrayList<>();
        if (resp.getHourly() == null) return list;

        List<String> times = resp.getHourly().getTime();
        var temp = resp.getHourly().getTemperature_2m();
        var rain = resp.getHourly().getPrecipitation();
        var wind = resp.getHourly().getWind_speed_10m();

        for (int i = 0; i < times.size(); i++) {
            Map<String, Object> item = new HashMap<>();
            item.put("time", times.get(i));
            item.put("temp", temp != null ? temp.get(i) : null);
            item.put("rain", rain != null ? rain.get(i) : null);
            item.put("wind", wind != null ? wind.get(i) : null);

            list.add(item);
        }

        return list;
    }

    public static void main(String[] args) {
        try {
            OpenMeteoResponse soil = OpenMeteoUtils.getSoilMoistureForecast(
                    31.0, 121.0,
                    LocalDateTime.now(),
                    LocalDateTime.now().plusDays(1)
            );

            System.out.println(soil.getHourly());

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

✨ 五、关键方法介绍

1. 获取降雨预报

OpenMeteoResponse resp = OpenMeteoUtils.getRainfall( 31.0, 121.0, LocalDateTime.now(), LocalDateTime.now().plusDays(3) );

2. 获取温度、湿度、气压

OpenMeteoUtils.getTemperatureForecast(...); OpenMeteoUtils.getHumidityForecast(...); OpenMeteoUtils.getPressureForecast(...);

3. 获取灌区模型所需全部气象要素

OpenMeteoResponse resp = OpenMeteoUtils.getIrrigationWeather( lat, lon, start, end );

此接口包括:

  • 温度

  • 相对湿度

  • 降雨量

  • 风速

  • 风向

  • 气压

  • 短波辐射(太阳辐射)

非常适合模型类项目。


✨ 六、计算类方法

1. ET₀(小时级,FAO56 简化)

List<Double> et0List = OpenMeteoUtils.calculateET0(resp);

2. 逐日 ET₀ 累计

Map<String, Double> et0Daily = OpenMeteoUtils.calculateDailyET0(resp);

3. 日降雨累计

Map<String, Double> rainDaily = OpenMeteoUtils.calculateDailyRain(resp);

4. 有效降雨

List<Double> effRain = OpenMeteoUtils.calculateEffectiveRain(resp, 0.8);

✨ 七、应用场景

✔ 智慧灌区 / 水利大屏

支持制定用水方案、灌溉调度等。

✔ 农业模型(需水量、肥水管理)

ET₀ + 降雨 + 土壤湿度 = 完整作物模型数据源。

✔ 物联网气象站补足

本地站点 + 云端预报效果更稳。

✔ 决策支持系统

降雨预报 + 风速风向 = 施工安全、户外作业预警等。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐