add weather
This commit is contained in:
parent
2cc8b893f7
commit
2774069f8e
102
skills/caiyun-weather/SKILL.md
Normal file
102
skills/caiyun-weather/SKILL.md
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
name: caiyun-weather
|
||||||
|
description: "通过彩云天气 API 查询天气数据 — 实时天气、逐小时/一周预报、历史天气和天气预警。当用户询问任何城市的天气、温度、空气质量、天气预报、降雨概率、历史天气或天气预警时使用此技能。需要设置 CAIYUN_WEATHER_API_TOKEN 环境变量。Use when: user asks about current weather, temperature, air quality, forecast, rain, historical weather, or alerts for any city."
|
||||||
|
metadata:
|
||||||
|
{
|
||||||
|
"openclaw":
|
||||||
|
{
|
||||||
|
"requires":
|
||||||
|
{
|
||||||
|
"bins": ["python3"],
|
||||||
|
"env": ["CAIYUN_WEATHER_API_TOKEN"],
|
||||||
|
},
|
||||||
|
"primaryEnv": "CAIYUN_WEATHER_API_TOKEN",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
# 彩云天气 (Caiyun Weather)
|
||||||
|
|
||||||
|
通过彩云天气 API 查询天气数据。支持直接使用城市名称(中文或英文),无需提供经纬度。
|
||||||
|
|
||||||
|
## 前置条件
|
||||||
|
|
||||||
|
使用前需设置环境变量:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export CAIYUN_WEATHER_API_TOKEN="你的API密钥"
|
||||||
|
```
|
||||||
|
|
||||||
|
免费申请 API 密钥:https://docs.caiyunapp.com/weather-api/
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
✅ **使用此技能:**
|
||||||
|
- "北京现在天气怎么样?"
|
||||||
|
- "上海明天会下雨吗?"
|
||||||
|
- "深圳未来一周天气预报"
|
||||||
|
- "广州空气质量如何?"
|
||||||
|
- "杭州过去24小时的天气"
|
||||||
|
- "成都有没有天气预警?"
|
||||||
|
- "What's the weather in Beijing?"
|
||||||
|
- 用户询问任何城市的天气、温度、空气质量、预报或预警
|
||||||
|
|
||||||
|
❌ **不要使用此技能:**
|
||||||
|
- 气候趋势分析或长期历史数据
|
||||||
|
- 航空/航海专业气象(METAR、TAF)
|
||||||
|
- 用户未配置彩云天气 API Token
|
||||||
|
|
||||||
|
## 命令
|
||||||
|
|
||||||
|
使用 `--city` 加城市名称(中文或英文)。如需精确定位,可使用 `--lng` 和 `--lat`。
|
||||||
|
|
||||||
|
### 实时天气
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" realtime --city "北京"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 逐小时预报(72小时)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" hourly --city "上海"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一周预报(7天)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" weekly --city "深圳"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 历史天气(过去24小时)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" history --city "杭州"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 天气预警
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" alerts --city "成都"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用坐标(可选)
|
||||||
|
|
||||||
|
对于无法通过城市名识别的地点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 "{{skill_path}}/scripts/caiyun_weather.py" realtime --lng 116.4074 --lat 39.9042
|
||||||
|
```
|
||||||
|
|
||||||
|
## 内置城市(即时查询)
|
||||||
|
|
||||||
|
北京、上海、广州、深圳、杭州、成都、武汉、南京、重庆、西安、天津、苏州、郑州、长沙、青岛、大连、厦门、昆明、贵阳、哈尔滨、沈阳、长春、福州、合肥、济南、南昌、石家庄、太原、呼和浩特、南宁、海口、三亚、拉萨、乌鲁木齐、兰州、西宁、银川、香港、澳门、台北、珠海、东莞、佛山、无锡、宁波、温州
|
||||||
|
|
||||||
|
英文名和其他全球城市通过在线地理编码自动解析。
|
||||||
|
|
||||||
|
## 说明
|
||||||
|
|
||||||
|
- 脚本仅使用 Python 标准库,无需 pip 安装
|
||||||
|
- 内置城市即时解析,其他城市通过 OpenStreetMap 地理编码
|
||||||
|
- API 对中国地区数据最为精准
|
||||||
|
- 有频率限制,请避免短时间内频繁请求
|
||||||
6
skills/caiyun-weather/_meta.json
Normal file
6
skills/caiyun-weather/_meta.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn72jm08z8zg612dswg2c11sbs80w1w8",
|
||||||
|
"slug": "caiyun-weather",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"publishedAt": 1772199654265
|
||||||
|
}
|
||||||
270
skills/caiyun-weather/scripts/caiyun_weather.py
Normal file
270
skills/caiyun-weather/scripts/caiyun_weather.py
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Caiyun Weather API client for OpenClaw skill."""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
import urllib.parse
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
API_BASE = "https://api.caiyunapp.com/v2.6"
|
||||||
|
|
||||||
|
# Built-in city coordinates (lng, lat)
|
||||||
|
CITY_COORDS = {
|
||||||
|
"北京": (116.4074, 39.9042), "beijing": (116.4074, 39.9042),
|
||||||
|
"上海": (121.4737, 31.2304), "shanghai": (121.4737, 31.2304),
|
||||||
|
"广州": (113.2644, 23.1291), "guangzhou": (113.2644, 23.1291),
|
||||||
|
"深圳": (114.0579, 22.5431), "shenzhen": (114.0579, 22.5431),
|
||||||
|
"杭州": (120.1551, 30.2741), "hangzhou": (120.1551, 30.2741),
|
||||||
|
"成都": (104.0665, 30.5728), "chengdu": (104.0665, 30.5728),
|
||||||
|
"武汉": (114.3054, 30.5931), "wuhan": (114.3054, 30.5931),
|
||||||
|
"南京": (118.7969, 32.0603), "nanjing": (118.7969, 32.0603),
|
||||||
|
"重庆": (106.5516, 29.5630), "chongqing": (106.5516, 29.5630),
|
||||||
|
"西安": (108.9402, 34.2615), "xian": (108.9402, 34.2615),
|
||||||
|
"天津": (117.1901, 39.1256), "tianjin": (117.1901, 39.1256),
|
||||||
|
"苏州": (120.5853, 31.2989), "suzhou": (120.5853, 31.2989),
|
||||||
|
"郑州": (113.6254, 34.7466), "zhengzhou": (113.6254, 34.7466),
|
||||||
|
"长沙": (112.9388, 28.2282), "changsha": (112.9388, 28.2282),
|
||||||
|
"青岛": (120.3826, 36.0671), "qingdao": (120.3826, 36.0671),
|
||||||
|
"大连": (121.6147, 38.9140), "dalian": (121.6147, 38.9140),
|
||||||
|
"厦门": (118.0894, 24.4798), "xiamen": (118.0894, 24.4798),
|
||||||
|
"昆明": (102.8329, 25.0389), "kunming": (102.8329, 25.0389),
|
||||||
|
"贵阳": (106.6302, 26.6477), "guiyang": (106.6302, 26.6477),
|
||||||
|
"哈尔滨": (126.5350, 45.8038), "haerbin": (126.5350, 45.8038),
|
||||||
|
"沈阳": (123.4315, 41.8057), "shenyang": (123.4315, 41.8057),
|
||||||
|
"长春": (125.3235, 43.8171), "changchun": (125.3235, 43.8171),
|
||||||
|
"福州": (119.2965, 26.0745), "fuzhou": (119.2965, 26.0745),
|
||||||
|
"合肥": (117.2272, 31.8206), "hefei": (117.2272, 31.8206),
|
||||||
|
"济南": (117.0009, 36.6758), "jinan": (117.0009, 36.6758),
|
||||||
|
"南昌": (115.8581, 28.6820), "nanchang": (115.8581, 28.6820),
|
||||||
|
"石家庄": (114.5149, 38.0428), "shijiazhuang": (114.5149, 38.0428),
|
||||||
|
"太原": (112.5489, 37.8706), "taiyuan": (112.5489, 37.8706),
|
||||||
|
"呼和浩特": (111.7490, 40.8424), "huhehaote": (111.7490, 40.8424),
|
||||||
|
"南宁": (108.3200, 22.8240), "nanning": (108.3200, 22.8240),
|
||||||
|
"海口": (110.3494, 20.0174), "haikou": (110.3494, 20.0174),
|
||||||
|
"三亚": (109.5120, 18.2528), "sanya": (109.5120, 18.2528),
|
||||||
|
"拉萨": (91.1322, 29.6600), "lasa": (91.1322, 29.6600),
|
||||||
|
"乌鲁木齐": (87.6168, 43.8256), "wulumuqi": (87.6168, 43.8256),
|
||||||
|
"兰州": (103.8343, 36.0611), "lanzhou": (103.8343, 36.0611),
|
||||||
|
"西宁": (101.7782, 36.6171), "xining": (101.7782, 36.6171),
|
||||||
|
"银川": (106.2782, 38.4664), "yinchuan": (106.2782, 38.4664),
|
||||||
|
"香港": (114.1694, 22.3193), "hongkong": (114.1694, 22.3193),
|
||||||
|
"澳门": (113.5439, 22.1987), "macau": (113.5439, 22.1987),
|
||||||
|
"台北": (121.5654, 25.0330), "taipei": (121.5654, 25.0330),
|
||||||
|
"珠海": (113.5767, 22.2710), "zhuhai": (113.5767, 22.2710),
|
||||||
|
"东莞": (113.7518, 23.0208), "dongguan": (113.7518, 23.0208),
|
||||||
|
"佛山": (113.1214, 23.0218), "foshan": (113.1214, 23.0218),
|
||||||
|
"无锡": (120.3119, 31.4912), "wuxi": (120.3119, 31.4912),
|
||||||
|
"宁波": (121.5440, 29.8683), "ningbo": (121.5440, 29.8683),
|
||||||
|
"温州": (120.6994, 28.0015), "wenzhou": (120.6994, 28.0015),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_token():
|
||||||
|
token = os.environ.get("CAIYUN_WEATHER_API_TOKEN")
|
||||||
|
if not token:
|
||||||
|
print("Error: CAIYUN_WEATHER_API_TOKEN environment variable is not set.", file=sys.stderr)
|
||||||
|
print("Apply for a free API key at: https://docs.caiyunapp.com/weather-api/", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def geocode_city(city_name):
|
||||||
|
"""Look up city coordinates using Nominatim (OpenStreetMap) as fallback."""
|
||||||
|
encoded = urllib.parse.quote(city_name)
|
||||||
|
url = f"https://nominatim.openstreetmap.org/search?q={encoded}&format=json&limit=1"
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
req.add_header("User-Agent", "caiyun-weather-skill/1.0")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
|
results = json.loads(resp.read().decode("utf-8"))
|
||||||
|
if results:
|
||||||
|
lat = float(results[0]["lat"])
|
||||||
|
lng = float(results[0]["lon"])
|
||||||
|
name = results[0].get("display_name", city_name).split(",")[0]
|
||||||
|
return lng, lat, name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_location(args):
|
||||||
|
"""Resolve lng/lat from --city or --lng/--lat arguments."""
|
||||||
|
if args.city:
|
||||||
|
city_key = args.city.strip().lower()
|
||||||
|
if city_key in CITY_COORDS:
|
||||||
|
lng, lat = CITY_COORDS[city_key]
|
||||||
|
return lng, lat
|
||||||
|
# Also try the original case
|
||||||
|
if args.city.strip() in CITY_COORDS:
|
||||||
|
lng, lat = CITY_COORDS[args.city.strip()]
|
||||||
|
return lng, lat
|
||||||
|
# Fallback: geocode via Nominatim
|
||||||
|
lng, lat, name = geocode_city(args.city)
|
||||||
|
if lng is not None:
|
||||||
|
return lng, lat
|
||||||
|
print(f"Error: Cannot find coordinates for city '{args.city}'.", file=sys.stderr)
|
||||||
|
print("Please use --lng and --lat to specify coordinates manually.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
elif args.lng is not None and args.lat is not None:
|
||||||
|
return args.lng, args.lat
|
||||||
|
else:
|
||||||
|
print("Error: Please specify --city <name> or both --lng and --lat.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def make_request(url, params=None):
|
||||||
|
if params:
|
||||||
|
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||||
|
url = f"{url}?{query}"
|
||||||
|
req = urllib.request.Request(url)
|
||||||
|
req.add_header("User-Agent", "caiyun-weather-skill/1.0")
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||||
|
return json.loads(resp.read().decode("utf-8"))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(f"HTTP Error {e.code}: {e.reason}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except urllib.error.URLError as e:
|
||||||
|
print(f"Connection Error: {e.reason}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def format_percent(value):
|
||||||
|
return f"{value * 100:.0f}%"
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_realtime(args):
|
||||||
|
token = get_token()
|
||||||
|
lng, lat = resolve_location(args)
|
||||||
|
url = f"{API_BASE}/{token}/{lng},{lat}/realtime"
|
||||||
|
data = make_request(url, {"lang": "en_US", "unit": "metric:v2"})
|
||||||
|
r = data["result"]["realtime"]
|
||||||
|
print(f"""=== Realtime Weather ===
|
||||||
|
Temperature: {r['temperature']}°C
|
||||||
|
Humidity: {format_percent(r['humidity'])}
|
||||||
|
Wind: {r['wind']['speed']} km/h, Direction {r['wind']['direction']}°
|
||||||
|
Precipitation: {r['precipitation']['local']['intensity']} mm/hr
|
||||||
|
Air Quality:
|
||||||
|
PM2.5: {r['air_quality']['pm25']} μg/m³
|
||||||
|
PM10: {r['air_quality']['pm10']} μg/m³
|
||||||
|
O3: {r['air_quality']['o3']} μg/m³
|
||||||
|
SO2: {r['air_quality']['so2']} μg/m³
|
||||||
|
NO2: {r['air_quality']['no2']} μg/m³
|
||||||
|
CO: {r['air_quality']['co']} mg/m³
|
||||||
|
AQI (China): {r['air_quality']['aqi']['chn']}
|
||||||
|
AQI (USA): {r['air_quality']['aqi']['usa']}
|
||||||
|
Life Index:
|
||||||
|
UV: {r['life_index']['ultraviolet']['desc']}
|
||||||
|
Comfort: {r['life_index']['comfort']['desc']}""")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_hourly(args):
|
||||||
|
token = get_token()
|
||||||
|
lng, lat = resolve_location(args)
|
||||||
|
url = f"{API_BASE}/{token}/{lng},{lat}/hourly"
|
||||||
|
data = make_request(url, {"hourlysteps": "72", "lang": "en_US", "unit": "metric:v2"})
|
||||||
|
hourly = data["result"]["hourly"]
|
||||||
|
print("=== 72-Hour Forecast ===")
|
||||||
|
for i in range(len(hourly["temperature"])):
|
||||||
|
t = hourly["temperature"][i]
|
||||||
|
sky = hourly["skycon"][i]["value"]
|
||||||
|
rain = hourly["precipitation"][i]["probability"]
|
||||||
|
wind = hourly["wind"][i]
|
||||||
|
print(f"""
|
||||||
|
Time: {t['datetime']}
|
||||||
|
Temperature: {t['value']}°C
|
||||||
|
Weather: {sky}
|
||||||
|
Rain Probability: {rain}%
|
||||||
|
Wind: {wind['speed']} km/h, {wind['direction']}°
|
||||||
|
------------------------""")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_weekly(args):
|
||||||
|
token = get_token()
|
||||||
|
lng, lat = resolve_location(args)
|
||||||
|
url = f"{API_BASE}/{token}/{lng},{lat}/daily"
|
||||||
|
data = make_request(url, {"dailysteps": "7", "lang": "en_US", "unit": "metric:v2"})
|
||||||
|
daily = data["result"]["daily"]
|
||||||
|
print("=== 7-Day Forecast ===")
|
||||||
|
for i in range(len(daily["temperature"])):
|
||||||
|
temp = daily["temperature"][i]
|
||||||
|
date = temp["date"].split("T")[0]
|
||||||
|
sky = daily["skycon"][i]["value"]
|
||||||
|
rain = daily["precipitation"][i]["probability"]
|
||||||
|
print(f"""
|
||||||
|
Date: {date}
|
||||||
|
Temperature: {temp['min']}°C ~ {temp['max']}°C
|
||||||
|
Weather: {sky}
|
||||||
|
Rain Probability: {rain}%
|
||||||
|
------------------------""")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_history(args):
|
||||||
|
token = get_token()
|
||||||
|
lng, lat = resolve_location(args)
|
||||||
|
timestamp = int((datetime.now() - timedelta(hours=24)).timestamp())
|
||||||
|
url = f"{API_BASE}/{token}/{lng},{lat}/hourly"
|
||||||
|
data = make_request(url, {
|
||||||
|
"hourlysteps": "24",
|
||||||
|
"begin": str(timestamp),
|
||||||
|
"lang": "en_US",
|
||||||
|
"unit": "metric:v2",
|
||||||
|
})
|
||||||
|
hourly = data["result"]["hourly"]
|
||||||
|
print("=== Past 24-Hour Weather ===")
|
||||||
|
for i in range(len(hourly["temperature"])):
|
||||||
|
t = hourly["temperature"][i]
|
||||||
|
sky = hourly["skycon"][i]["value"]
|
||||||
|
print(f"""
|
||||||
|
Time: {t['datetime']}
|
||||||
|
Temperature: {t['value']}°C
|
||||||
|
Weather: {sky}
|
||||||
|
------------------------""")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_alerts(args):
|
||||||
|
token = get_token()
|
||||||
|
lng, lat = resolve_location(args)
|
||||||
|
url = f"{API_BASE}/{token}/{lng},{lat}/weather"
|
||||||
|
data = make_request(url, {"alert": "true", "lang": "en_US", "unit": "metric:v2"})
|
||||||
|
alerts = data["result"].get("alert", {}).get("content", [])
|
||||||
|
if not alerts:
|
||||||
|
print("No active weather alerts.")
|
||||||
|
return
|
||||||
|
print("=== Weather Alerts ===")
|
||||||
|
for alert in alerts:
|
||||||
|
print(f"""
|
||||||
|
Title: {alert.get('title', 'N/A')}
|
||||||
|
Code: {alert.get('code', 'N/A')}
|
||||||
|
Status: {alert.get('status', 'N/A')}
|
||||||
|
Description: {alert.get('description', 'N/A')}
|
||||||
|
------------------------""")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Caiyun Weather API client")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
for name, func in [
|
||||||
|
("realtime", cmd_realtime),
|
||||||
|
("hourly", cmd_hourly),
|
||||||
|
("weekly", cmd_weekly),
|
||||||
|
("history", cmd_history),
|
||||||
|
("alerts", cmd_alerts),
|
||||||
|
]:
|
||||||
|
sub = subparsers.add_parser(name)
|
||||||
|
sub.add_argument("--city", type=str, help="City name (Chinese or English, e.g. 北京 or beijing)")
|
||||||
|
sub.add_argument("--lng", type=float, help="Longitude (use with --lat)")
|
||||||
|
sub.add_argument("--lat", type=float, help="Latitude (use with --lng)")
|
||||||
|
sub.set_defaults(func=func)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
73
skills/weather-china/SKILL.md
Normal file
73
skills/weather-china/SKILL.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
---
|
||||||
|
name: weather-china
|
||||||
|
description: 中国天气预报查询 - 基于中国天气网(weather.com.cn)获取7天天气预报和生活指数数据。纯 Python 实现,无需 API Key。
|
||||||
|
version: 1.0.2
|
||||||
|
tags: [weather, china, forecast, chinese, weather-cn, life-index, 7day-forecast]
|
||||||
|
metadata: {"openclaw":{"emoji":"🌤️","requires":{"bins":["python3"]}}}
|
||||||
|
allowed-tools: [exec]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 中国天气预报查询 (China Weather)
|
||||||
|
|
||||||
|
基于 [中国天气网](https://www.weather.com.cn) 获取 7 天天气预报和生活指数数据。纯 Python 实现,无需 API Key。
|
||||||
|
|
||||||
|
## Quick Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查询天气(格式化文本输出)
|
||||||
|
python3 skills/weather-china/lib/weather_cn.py query 南京
|
||||||
|
python3 skills/weather-china/lib/weather_cn.py query 北京
|
||||||
|
python3 skills/weather-china/lib/weather_cn.py query 成都
|
||||||
|
|
||||||
|
# JSON 输出(结构化数据)
|
||||||
|
python3 skills/weather-china/lib/weather_cn.py json 上海
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Output
|
||||||
|
|
||||||
|
```text
|
||||||
|
城市: 南京 (代码: 101190101)
|
||||||
|
数据来源: weather.com.cn
|
||||||
|
查询时间: 2026-03-04 16:31:55
|
||||||
|
|
||||||
|
[4日(今天)] 阴转多云, 10℃/5℃, 东风 4-5级转3-4级
|
||||||
|
感冒指数: 易发 - 风较大,易发生感冒,注意防护。
|
||||||
|
运动指数: 较适宜 - 风力较强且气温较低,请进行室内运动。
|
||||||
|
过敏指数: 较易发 - 外出需远离过敏源,适当采取防护措施。
|
||||||
|
穿衣指数: 冷 - 建议着棉衣加羊毛衫等冬季服装。
|
||||||
|
洗车指数: 较不宜 - 风力较大,洗车后会蒙上灰尘。
|
||||||
|
紫外线指数: 最弱 - 辐射弱,涂擦SPF8-12防晒护肤品。
|
||||||
|
|
||||||
|
[5日(明天)] 阴转多云, 11℃/4℃, 北风 3-4级
|
||||||
|
感冒指数: 少发 - 无明显降温,感冒机率较低。
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Cities
|
||||||
|
|
||||||
|
支持查询中国天气网覆盖的所有城市和地区。输入城市名称即可自动搜索匹配,无需手动配置。
|
||||||
|
|
||||||
|
常见城市(如北京、上海、广州、深圳、成都、杭州、南京等 60+ 城市)已内置代码,查询更快。其他城市会通过搜索接口自动查找城市代码。
|
||||||
|
|
||||||
|
## Data Available
|
||||||
|
|
||||||
|
- **7天预报**: 日期、天气状况、高/低温度、风向风力
|
||||||
|
- **生活指数**: 感冒、运动、过敏、穿衣、洗车、紫外线等
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
当用户询问以下问题时使用本 skill:
|
||||||
|
|
||||||
|
- "今天天气怎么样"
|
||||||
|
- "明天会下雨吗"
|
||||||
|
- "[城市名]天气预报"
|
||||||
|
- "南京这周天气如何"
|
||||||
|
- "出门需要带伞吗"
|
||||||
|
- "穿什么衣服合适"
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **数据来源**: 中国天气网,数据可能略有延迟
|
||||||
|
2. **城市名称**: 使用标准城市名,如"成都"、"南京"
|
||||||
|
3. **网络依赖**: 需要能访问 <www.weather.com.cn>
|
||||||
|
4. **无需 API Key**: 直接解析天气网页面数据
|
||||||
6
skills/weather-china/_meta.json
Normal file
6
skills/weather-china/_meta.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ownerId": "kn73tn1djj79kf3zsca8gm8tw182bhke",
|
||||||
|
"slug": "weather-china",
|
||||||
|
"version": "1.0.2",
|
||||||
|
"publishedAt": 1772678191581
|
||||||
|
}
|
||||||
425
skills/weather-china/lib/weather_cn.py
Normal file
425
skills/weather-china/lib/weather_cn.py
Normal file
@ -0,0 +1,425 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
中国天气网天气预报查询工具 (weather.com.cn)
|
||||||
|
查询7天天气预报和生活指数数据,输出结构化数据供AI模型使用。
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
import urllib.request
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class WeatherHTMLParser(HTMLParser):
|
||||||
|
"""解析天气页面HTML,提取7天预报和生活指数数据。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._capture = False
|
||||||
|
self._target_id = None
|
||||||
|
self._depth = 0
|
||||||
|
self._data_parts = []
|
||||||
|
self._result = ""
|
||||||
|
|
||||||
|
def extract(self, html, target_id):
|
||||||
|
"""提取指定id的div内容。"""
|
||||||
|
self._capture = False
|
||||||
|
self._target_id = target_id
|
||||||
|
self._depth = 0
|
||||||
|
self._data_parts = []
|
||||||
|
self._result = ""
|
||||||
|
self.feed(html)
|
||||||
|
return self._result
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
attrs_dict = dict(attrs)
|
||||||
|
if tag == "div" and attrs_dict.get("id") == self._target_id:
|
||||||
|
self._capture = True
|
||||||
|
self._depth = 1
|
||||||
|
return
|
||||||
|
if self._capture and tag == "div":
|
||||||
|
self._depth += 1
|
||||||
|
if self._capture:
|
||||||
|
attr_str = " ".join(f'{k}="{v}"' for k, v in attrs)
|
||||||
|
self._data_parts.append(f"<{tag} {attr_str}>" if attr_str else f"<{tag}>")
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if self._capture:
|
||||||
|
self._data_parts.append(f"</{tag}>")
|
||||||
|
if tag == "div":
|
||||||
|
self._depth -= 1
|
||||||
|
if self._depth <= 0:
|
||||||
|
self._capture = False
|
||||||
|
self._result = "".join(self._data_parts)
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if self._capture:
|
||||||
|
self._data_parts.append(data)
|
||||||
|
|
||||||
|
|
||||||
|
class ChinaWeather:
|
||||||
|
"""中国天气网天气查询。"""
|
||||||
|
|
||||||
|
# 城市搜索API
|
||||||
|
SEARCH_URL = "https://toy1.weather.com.cn/search?cityname={query}&callback=success_jsonpCallback&_={ts}"
|
||||||
|
|
||||||
|
# 天气预报页面
|
||||||
|
WEATHER_URL = "https://www.weather.com.cn/weather/{code}.shtml"
|
||||||
|
|
||||||
|
# 请求超时(秒)
|
||||||
|
REQUEST_TIMEOUT = 15
|
||||||
|
|
||||||
|
# 请求头
|
||||||
|
REQUEST_HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36",
|
||||||
|
"Accept": "*/*",
|
||||||
|
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
||||||
|
"Referer": "https://www.weather.com.cn/",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 预置常用城市代码
|
||||||
|
PRESET_CITIES = {
|
||||||
|
"北京": "101010100", "上海": "101020100", "广州": "101280101",
|
||||||
|
"深圳": "101280601", "成都": "101270101", "杭州": "101210101",
|
||||||
|
"南京": "101190101", "武汉": "101200101", "西安": "101110101",
|
||||||
|
"重庆": "101040100", "天津": "101030100", "沈阳": "101070101",
|
||||||
|
"哈尔滨": "101050101", "长春": "101060101", "济南": "101120101",
|
||||||
|
"青岛": "101120201", "郑州": "101180101", "长沙": "101250101",
|
||||||
|
"南昌": "101240101", "福州": "101230101", "厦门": "101230201",
|
||||||
|
"南宁": "101300101", "海口": "101310101", "三亚": "101310201",
|
||||||
|
"贵阳": "101260101", "昆明": "101290101", "兰州": "101160101",
|
||||||
|
"银川": "101170101", "西宁": "101150101", "拉萨": "101140101",
|
||||||
|
"乌鲁木齐": "101130101", "石家庄": "101090101", "太原": "101100101",
|
||||||
|
"呼和浩特": "101080101", "大连": "101070201", "苏州": "101190401",
|
||||||
|
"无锡": "101190201", "宁波": "101210401", "温州": "101210701",
|
||||||
|
"绍兴": "101210601", "金华": "101210901", "台州": "101211101",
|
||||||
|
"嘉兴": "101210301", "湖州": "101210201", "衢州": "101211001",
|
||||||
|
"丽水": "101211201", "舟山": "101211301", "东莞": "101281901",
|
||||||
|
"佛山": "101281701", "珠海": "101280701", "中山": "101281801",
|
||||||
|
"惠州": "101280301", "江门": "101280603", "肇庆": "101280901",
|
||||||
|
"湛江": "101281001", "茂名": "101281101", "汕头": "101280401",
|
||||||
|
"合肥": "101220101", "常州": "101191101", "徐州": "101190801",
|
||||||
|
"烟台": "101120501", "潍坊": "101120601", "临沂": "101120901",
|
||||||
|
"洛阳": "101180901", "襄阳": "101200201", "宜昌": "101200901",
|
||||||
|
"芜湖": "101220301", "泉州": "101230501", "漳州": "101230601",
|
||||||
|
"桂林": "101300501", "柳州": "101300301", "遵义": "101260201",
|
||||||
|
"大理": "101290601", "丽江": "101291401", "西双版纳": "101291601",
|
||||||
|
"延安": "101110600", "宝鸡": "101110901", "咸阳": "101110200",
|
||||||
|
"香港": "101320101", "澳门": "101330101",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 生活指数名称映射
|
||||||
|
LIFE_INDEX_NAMES = {
|
||||||
|
"感冒": "cold",
|
||||||
|
"运动": "exercise",
|
||||||
|
"过敏": "allergy",
|
||||||
|
"穿衣": "clothing",
|
||||||
|
"洗车": "carwash",
|
||||||
|
"紫外线": "uv",
|
||||||
|
"钓鱼": "fishing",
|
||||||
|
"旅游": "travel",
|
||||||
|
"晾晒": "drying",
|
||||||
|
"交通": "traffic",
|
||||||
|
"防晒": "sunscreen",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._parser = WeatherHTMLParser()
|
||||||
|
|
||||||
|
def _http_get(self, url, encoding="utf-8"):
|
||||||
|
"""发送HTTP GET请求。"""
|
||||||
|
req = urllib.request.Request(url, headers=self.REQUEST_HEADERS)
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=self.REQUEST_TIMEOUT) as resp:
|
||||||
|
return resp.read().decode(encoding, errors="replace")
|
||||||
|
except Exception as e:
|
||||||
|
raise ConnectionError(f"HTTP请求失败: {url} - {e}")
|
||||||
|
|
||||||
|
def search_city(self, city_name):
|
||||||
|
"""通过搜索API查询城市代码。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (城市代码, 城市显示名) 或 None
|
||||||
|
"""
|
||||||
|
encoded = urllib.parse.quote(city_name)
|
||||||
|
ts = int(datetime.now().timestamp() * 1000)
|
||||||
|
url = self.SEARCH_URL.format(query=encoded, ts=ts)
|
||||||
|
|
||||||
|
try:
|
||||||
|
text = self._http_get(url)
|
||||||
|
except ConnectionError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 解析JSONP: success_jsonpCallback([...])
|
||||||
|
match = re.search(r"success_jsonpCallback\((\[.*\])\)", text, re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = json.loads(match.group(1))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 取第一个结果,解析格式: "代码~省份~城市名~英文名~..."
|
||||||
|
ref = results[0].get("ref", "")
|
||||||
|
parts = ref.split("~")
|
||||||
|
if len(parts) >= 3:
|
||||||
|
code = parts[0]
|
||||||
|
display_name = parts[2]
|
||||||
|
# 确保是有效的城市代码(以101开头的9位数字)
|
||||||
|
if re.match(r"^101\d{6}$", code):
|
||||||
|
return (code, display_name)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_city_code(self, city_name):
|
||||||
|
"""获取城市代码,优先使用预置数据。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (城市代码, 城市显示名)
|
||||||
|
"""
|
||||||
|
city_name = city_name.strip()
|
||||||
|
|
||||||
|
# 优先查预置城市
|
||||||
|
if city_name in self.PRESET_CITIES:
|
||||||
|
return (self.PRESET_CITIES[city_name], city_name)
|
||||||
|
|
||||||
|
# 实时搜索
|
||||||
|
result = self.search_city(city_name)
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
raise ValueError(f"未找到城市: {city_name}")
|
||||||
|
|
||||||
|
def fetch_weather_html(self, city_code):
|
||||||
|
"""获取天气预报页面HTML。"""
|
||||||
|
url = self.WEATHER_URL.format(code=city_code)
|
||||||
|
return self._http_get(url)
|
||||||
|
|
||||||
|
def parse_7day_forecast(self, html):
|
||||||
|
"""解析7天天气预报数据。
|
||||||
|
|
||||||
|
从 <div id="7d"> 区域提取每天的天气信息。
|
||||||
|
实际HTML结构 (per <li>):
|
||||||
|
<h1>日期</h1>
|
||||||
|
<p title="天气" class="wea">天气</p>
|
||||||
|
<p class="tem"><span>高温</span>/<i>低温℃</i></p>
|
||||||
|
<p class="win">
|
||||||
|
<em><span title="风向"></span>...</em>
|
||||||
|
<i>风力等级</i>
|
||||||
|
</p>
|
||||||
|
"""
|
||||||
|
block = self._parser.extract(html, "7d")
|
||||||
|
if not block:
|
||||||
|
return []
|
||||||
|
|
||||||
|
forecasts = []
|
||||||
|
|
||||||
|
# 按 <li> 拆分,逐个解析
|
||||||
|
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', block, re.DOTALL)
|
||||||
|
|
||||||
|
for li in li_blocks:
|
||||||
|
# 跳过没有 <h1> 的空 li 元素
|
||||||
|
m_date = re.search(r'<h1>([^<]+)</h1>', li)
|
||||||
|
if not m_date:
|
||||||
|
continue
|
||||||
|
|
||||||
|
day = {
|
||||||
|
"date": m_date.group(1).strip(),
|
||||||
|
"weather": "",
|
||||||
|
"temp_high": "",
|
||||||
|
"temp_low": "",
|
||||||
|
"wind": "",
|
||||||
|
"wind_level": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 天气状况 - 优先取title属性
|
||||||
|
m = re.search(r'<p\s+title="([^"]*)"[^>]*class="wea"', li)
|
||||||
|
if m:
|
||||||
|
day["weather"] = m.group(1).strip()
|
||||||
|
else:
|
||||||
|
m = re.search(r'class="wea"[^>]*>([^<]+)<', li)
|
||||||
|
if m:
|
||||||
|
day["weather"] = m.group(1).strip()
|
||||||
|
|
||||||
|
# 温度 - <p class="tem"><span>高温℃</span>/<i>低温℃</i></p>
|
||||||
|
# 注意:晚间"今天"可能无<span>高温,仅有<i>低温℃</i>
|
||||||
|
m = re.search(r'<span>(\d+)℃?</span>', li)
|
||||||
|
if m:
|
||||||
|
day["temp_high"] = m.group(1) + "℃"
|
||||||
|
m = re.search(r'<i>(\d+)℃</i>', li)
|
||||||
|
if m:
|
||||||
|
day["temp_low"] = m.group(1) + "℃"
|
||||||
|
|
||||||
|
# 风向 - <em> 内第一个 <span title="风向">
|
||||||
|
m = re.search(r'<span\s+title="([^"]*)"[^>]*class="[A-Z]', li)
|
||||||
|
if m:
|
||||||
|
day["wind"] = m.group(1).strip()
|
||||||
|
|
||||||
|
# 风力等级 - <p class="win"> 内紧跟 </em> 后的 <i>等级</i>
|
||||||
|
m = re.search(r'</em>\s*<i>([^<]+)</i>', li)
|
||||||
|
if m:
|
||||||
|
day["wind_level"] = m.group(1).strip()
|
||||||
|
|
||||||
|
forecasts.append(day)
|
||||||
|
|
||||||
|
return forecasts
|
||||||
|
|
||||||
|
def parse_life_indices(self, html):
|
||||||
|
"""解析生活指数数据。
|
||||||
|
|
||||||
|
从 <div id="livezs"> 区域提取生活指数。
|
||||||
|
实际HTML结构 (per <li>):
|
||||||
|
<li>
|
||||||
|
<i class="gmi"></i>
|
||||||
|
<span>级别</span>
|
||||||
|
<em>类型指数</em>
|
||||||
|
<p>描述</p>
|
||||||
|
</li>
|
||||||
|
"""
|
||||||
|
block = self._parser.extract(html, "livezs")
|
||||||
|
if not block:
|
||||||
|
return []
|
||||||
|
|
||||||
|
indices = []
|
||||||
|
|
||||||
|
# 按 <li> 拆分解析
|
||||||
|
li_blocks = re.findall(r'<li[^>]*>(.*?)</li>', block, re.DOTALL)
|
||||||
|
|
||||||
|
for li in li_blocks:
|
||||||
|
# 级别 - <span>易发</span>
|
||||||
|
m_level = re.search(r'<span>([^<]+)</span>', li)
|
||||||
|
# 指数名 - <em>感冒指数</em>
|
||||||
|
m_name = re.search(r'<em>([^<]+)</em>', li)
|
||||||
|
# 描述 - <p>描述内容</p>
|
||||||
|
m_desc = re.search(r'<p>([^<]+)</p>', li)
|
||||||
|
|
||||||
|
if not (m_level and m_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
level = m_level.group(1).strip()
|
||||||
|
name = m_name.group(1).strip()
|
||||||
|
desc = m_desc.group(1).strip() if m_desc else ""
|
||||||
|
|
||||||
|
# 提取指数类型关键词(去掉"指数"后缀)
|
||||||
|
index_type = name.replace("指数", "").strip()
|
||||||
|
key = self.LIFE_INDEX_NAMES.get(index_type, index_type)
|
||||||
|
|
||||||
|
indices.append({
|
||||||
|
"name": name,
|
||||||
|
"key": key,
|
||||||
|
"level": level,
|
||||||
|
"description": desc,
|
||||||
|
})
|
||||||
|
|
||||||
|
return indices
|
||||||
|
|
||||||
|
def query(self, city_name):
|
||||||
|
"""查询指定城市的天气预报。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
city_name: 城市名称,如"南京"、"北京"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含城市信息、7天预报(含每日生活指数)的结构化数据
|
||||||
|
"""
|
||||||
|
code, display_name = self.get_city_code(city_name)
|
||||||
|
html = self.fetch_weather_html(code)
|
||||||
|
|
||||||
|
forecast = self.parse_7day_forecast(html)
|
||||||
|
life_indices = self.parse_life_indices(html)
|
||||||
|
|
||||||
|
# 将生活指数按天分组后合并到对应天的forecast中
|
||||||
|
# livezs中的指数是连续排列的,每天的指数类型数量相同
|
||||||
|
if forecast and life_indices:
|
||||||
|
num_days = len(forecast)
|
||||||
|
indices_per_day = len(life_indices) // num_days if num_days > 0 else 0
|
||||||
|
if indices_per_day > 0:
|
||||||
|
for i, day in enumerate(forecast):
|
||||||
|
start = i * indices_per_day
|
||||||
|
end = start + indices_per_day
|
||||||
|
day["life_indices"] = life_indices[start:end]
|
||||||
|
else:
|
||||||
|
# 无法均分时,全部放到第一天
|
||||||
|
forecast[0]["life_indices"] = life_indices
|
||||||
|
|
||||||
|
return {
|
||||||
|
"city": display_name,
|
||||||
|
"city_code": code,
|
||||||
|
"query_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"source": "weather.com.cn",
|
||||||
|
"forecast": forecast,
|
||||||
|
}
|
||||||
|
|
||||||
|
def format_output(self, data):
|
||||||
|
"""格式化天气数据为文本输出(供AI模型读取)。"""
|
||||||
|
if "error" in data:
|
||||||
|
return f"错误: {data['error']}"
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
f"城市: {data['city']} (代码: {data['city_code']})",
|
||||||
|
f"数据来源: {data['source']}",
|
||||||
|
f"查询时间: {data['query_time']}",
|
||||||
|
]
|
||||||
|
|
||||||
|
for day in data.get("forecast", []):
|
||||||
|
lines.append("")
|
||||||
|
temp = f"{day['temp_high']}/{day['temp_low']}" if day.get("temp_high") else day.get("temp_low", "")
|
||||||
|
wind_info = f"{day['wind']} {day['wind_level']}" if day.get("wind") else ""
|
||||||
|
lines.append(f"[{day['date']}] {day['weather']}, {temp}, {wind_info}".rstrip(", "))
|
||||||
|
|
||||||
|
# 每天的生活指数
|
||||||
|
for idx in day.get("life_indices", []):
|
||||||
|
lines.append(f" {idx['name']}: {idx['level']} - {idx['description']}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: weather_cn.py <command> <city>")
|
||||||
|
print("Commands:")
|
||||||
|
print(" query <city> - 查询天气(格式化文本输出)")
|
||||||
|
print(" json <city> - 查询天气(JSON输出)")
|
||||||
|
print()
|
||||||
|
print("Examples:")
|
||||||
|
print(" weather_cn.py query 南京")
|
||||||
|
print(" weather_cn.py json 北京")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
weather = ChinaWeather()
|
||||||
|
command = sys.argv[1].lower()
|
||||||
|
city = " ".join(sys.argv[2:]) if len(sys.argv) > 2 else ""
|
||||||
|
|
||||||
|
if not city:
|
||||||
|
print("错误: 请指定城市名称", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if command == "query":
|
||||||
|
data = weather.query(city)
|
||||||
|
print(weather.format_output(data))
|
||||||
|
elif command == "json":
|
||||||
|
data = weather.query(city)
|
||||||
|
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||||
|
else:
|
||||||
|
print(f"未知命令: {command}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"错误: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except ConnectionError as e:
|
||||||
|
print(f"网络错误: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"未知错误: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@ -1,93 +0,0 @@
|
|||||||
---
|
|
||||||
name: weather-forecasts
|
|
||||||
description: Get current weather and forecasts (no API key required).
|
|
||||||
homepage: https://wttr.in/:help
|
|
||||||
metadata: {"clawdbot":{"emoji":"🌤️","requires":{"bins":["curl"]}}}
|
|
||||||
---
|
|
||||||
|
|
||||||
# Weather
|
|
||||||
|
|
||||||
Two free services, no API keys needed.
|
|
||||||
|
|
||||||
## wttr.in (primary)
|
|
||||||
|
|
||||||
Quick one-liner:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?format=3"
|
|
||||||
# Output: London: ⛅️ +8°C
|
|
||||||
```
|
|
||||||
|
|
||||||
Compact format:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?format=%l:+%c+%t+%h+%w"
|
|
||||||
# Output: London: ⛅️ +8°C 71% ↙5km/h
|
|
||||||
```
|
|
||||||
|
|
||||||
Full forecast (3 days):
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?T"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Querying by time range
|
|
||||||
|
|
||||||
Current weather only:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?0&format=3"
|
|
||||||
```
|
|
||||||
|
|
||||||
Today's forecast:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?1&T"
|
|
||||||
```
|
|
||||||
|
|
||||||
Tomorrow's forecast:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?2&T"
|
|
||||||
# Shows today + tomorrow; extract the second day
|
|
||||||
```
|
|
||||||
|
|
||||||
Day after tomorrow:
|
|
||||||
```bash
|
|
||||||
curl -s "wttr.in/London?T"
|
|
||||||
# Default output shows 3 days (today, tomorrow, day after tomorrow)
|
|
||||||
```
|
|
||||||
|
|
||||||
Next 7 days (weekly forecast) — use Open-Meteo:
|
|
||||||
```bash
|
|
||||||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto&forecast_days=7"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Format codes
|
|
||||||
|
|
||||||
`%c` condition · `%t` temp · `%h` humidity · `%w` wind · `%l` location · `%m` moon
|
|
||||||
|
|
||||||
### Tips
|
|
||||||
|
|
||||||
- URL-encode spaces: `wttr.in/New+York`
|
|
||||||
- Airport codes: `wttr.in/JFK`
|
|
||||||
- Units: `?m` (metric) `?u` (USCS)
|
|
||||||
- Language: `?lang=ja` (Japanese), `?lang=zh` (Chinese), etc.
|
|
||||||
- PNG: `curl -s "wttr.in/Berlin.png" -o /tmp/weather.png`
|
|
||||||
|
|
||||||
## Open-Meteo (fallback / extended forecasts, JSON)
|
|
||||||
|
|
||||||
Free, no key, good for programmatic use and longer-range forecasts.
|
|
||||||
|
|
||||||
Current weather:
|
|
||||||
```bash
|
|
||||||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12¤t_weather=true"
|
|
||||||
```
|
|
||||||
|
|
||||||
7-day daily forecast (min/max temp, weather code):
|
|
||||||
```bash
|
|
||||||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto&forecast_days=7"
|
|
||||||
```
|
|
||||||
|
|
||||||
Hourly forecast for the next 2 days:
|
|
||||||
```bash
|
|
||||||
curl -s "https://api.open-meteo.com/v1/forecast?latitude=51.5&longitude=-0.12&hourly=temperature_2m,weathercode,precipitation&timezone=auto&forecast_days=2"
|
|
||||||
```
|
|
||||||
|
|
||||||
Find coordinates for a city, then query. Returns JSON with temp, windspeed, weathercode.
|
|
||||||
|
|
||||||
Docs: https://open-meteo.com/en/docs
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"ownerId": "kn70pywhg0fyz996kpa8xj89s57yhv26",
|
|
||||||
"slug": "weather",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"publishedAt": 1767545394459
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user