Skill: etmnb/weather-pulse (version 1.3.6)

--- name: weather-pulse description: "Weather + air quality tool. (1) Current: temp, feels-like, wind, humidity, precipitation, pressure, visibility. (2) Daily 3-30d: sun/moon, max/min temp, day/night weather, UV. (3) Hourly 24-168h: temp, weather, wind, humidity, precipitation probability. (4) 19 lifestyle indices (5 overseas). (5) AQI 6 levels, PM2.5/PM10/CO/NO2/O3/SO2, 7-day forecast. Input: city name (CN/EN), coordinates, CityId, or WAQI ID. Endpoints: now, 3d-30d, 24h-168h, indices, aqi. Free: QWeather 50k/mo, WAQI 1k/hr.", summary: "Query real-time weather, 3-30 day daily forecasts, 24-168h hourly forecasts, AQI/PM2.5/PM10 with 7-day pollutant forecast, and 19 lifestyle indices for any city by name (CN/EN), coordinates, or CityId." tags: weather: "1.3.6" forecast: "1.3.6" air-quality: "1.3.6" aqi: "1.3.6" qweather: "1.3.6" temperature: "1.3.6" chinese: "1.3.6" trigger_patterns: - "天气" - "查天气" - "天气预报" - "气温" - "温度" - "下雨" - "湿度" - "风力" - "空气质量" - "PM25" - "天气怎么样" - "weather" - "forecast" - "temperature" - "humidity" - "aqi" - "air quality" - "rain" - "wind" - "uv" - "current weather" - "weather forecast" - "what is the weather" homepage: "https://dev.qweather.com/docs" requiredEnvs: - name: QWEATHER_API_HOST description: "QWeather API Host from console settings (e.g. xxxxxxx.re.qweatherapi.com). Required for weather endpoints." required: false - name: QWEATHER_API_KEY description: "QWeather API Key from project credentials. Required for weather endpoints." required: false - name: WAQI_API_TOKEN description: "WAQI token from https://aqicn.org/data-platform/token/. Required for AQI endpoint." required: false --- # weather-pulse Weather and air quality query tool. Uses two free APIs. Both require separate registration. > **Two APIs, Two Registrations Required:** > - **Weather Data** - QWeather: https://console.qweather.com > - **Air Quality Data** - WAQI: https://aqicn.org/api/ > > You need both for full features. Weather-only works without WAQI token. AQI-only works without QWeather credentials. ## Quick Start ### 1. Get API Credentials #### QWeather - Free 50,000 calls/month Register to get API Host and API Key. 1. Open https://console.qweather.com and register or log in. 2. Create a project with any name (e.g. "Demo"). 3. Add credentials: go to Project > Credentials > Create Credential > select API KEY. 4. Get API Host: go to Settings in the left menu > copy the value like `xxxxxxx.re.qweatherapi.com`. | Config Item | Required For | Where to Get | Example | |-------------|-------------|--------------|---------| | `QWEATHER_API_HOST` | Weather | QWeather Console > Settings > API Host | `xxxxxxx.re.qweatherapi.com` | | `QWEATHER_API_KEY` | Weather | QWeather Console > Project > Credentials > API KEY | [REDACTED_SECRET] | Docs: https://dev.qweather.com/docs/start/ | Pricing: https://dev.qweather.com/docs/finance/pricing/ #### WAQI - Free 1,000 calls/hour Register to get Token. 1. Open https://aqicn.org/data-platform/token/#/ 2. Check the agreement box, fill in email and name, submit. 3. Check your email, click the confirmation link, get the token. | Config Item | Required For | Where to Get | Example | |-------------|-------------|--------------|---------| | `WAQI_API_TOKEN` | Air Quality | WAQI email confirmation page | [REDACTED_SECRET] | Docs: https://aqicn.org/api/ | City lookup: https://aqicn.org/city/ ### 2. Configure Credentials **Recommended: use environment variables. Do not put API keys in script files.** Storing API keys directly in code can cause leaks (e.g. accidental Git commits, shared devices). Use environment variables or a `.env` file. #### Option A: Environment Variables (Recommended) Only configure credentials for the features you need. ```bash # Weather (Linux/macOS) export QWEATHER_API_HOST="your_api_host" export QWEATHER_API_KEY="your_api_key" # Weather (Windows PowerShell) $env:QWEATHER_API_HOST = "your_api_host" $env:QWEATHER_API_KEY = "your_api_key" # Air Quality (all platforms) export WAQI_API_TOKEN="your_waqi_token" # Windows PowerShell: $env:WAQI_API_TOKEN = "your_waqi_token" ``` #### Option B: .env File Create a `.env` file in the same directory as `weather.py`, and load it before running: ```bash # .env file contents QWEATHER_API_HOST=your_api_host QWEATHER_API_KEY=your_api_key WAQI_API_TOKEN=your_waqi_token ``` ```bash # Linux/macOS set -a && source .env && set +a && python scripts/weather.py ... # Windows PowerShell Get-Content .env | ForEach-Object { $k,$v = $_.Split('='); Set-Item "env:$k" $v } python scripts/weather.py ... ``` ### 3. Test ```bash # Current weather (by CityId) python scripts/weather.py 101180301 # Air quality (by city name) python scripts/weather.py xinxiang --endpoint aqi ``` --- ## Call Method ```bash python scripts/weather.py <location> [--endpoint ENDPOINT] [--json] ``` If `--endpoint` is omitted, defaults to `now` (current weather). ### Location Format (Auto-Parsed) | Type | Format | Supported Endpoints | Example | |------|--------|---------------------|---------| | CityId | digits only | All | `101180301` | | English city name | any | All (auto-resolved to CityId) | `Shanghai` | | Chinese city name | any | All (auto-resolved to CityId) | `上海` | | Coordinates | `lng,lat` or `lat,lng` (auto-detected) | All | `113.92,35.30` | | geo: format | `"geo:lat;lng"` (quotes required) | AQI only | `"geo:35.30;113.92"` | | WAQI ID | `@number` | AQI only | `@5789` | | Current location | `here` | AQI only | `here` | City name input is automatically resolved to CityId via QWeather GeoAPI. No need to look up IDs manually. Coordinate input auto-detects the order (lng,lat or lat,lng both work). The `geo:` format uses semicolons which require quotes in shell to avoid parsing as command separator. ### Available Endpoints | Endpoint | Source | Free Quota | Description | |----------|--------|------------|-------------| | `now` | QWeather | 50k/mo | Current weather | | `3d` | QWeather | 50k/mo | 3-day daily forecast | | `7d` | QWeather | 50k/mo | 7-day daily forecast | | `10d` | QWeather | 50k/mo | 10-day daily forecast | | `15d` | QWeather | 50k/mo | 15-day daily forecast | | `30d` | QWeather | 50k/mo | 30-day daily forecast | | `24h` | QWeather | 50k/mo | 24-hour hourly forecast | | `72h` | QWeather | 50k/mo | 72-hour hourly forecast | | `168h` | QWeather | 50k/mo | 168-hour hourly forecast | | `indices` | QWeather | 50k/mo | Weather lifestyle indices (19 types) | | `aqi` | WAQI | 1k/hr | Air quality + PM2.5/PM10 forecast | --- ## Data Reference ### Current Weather `now` | Field | Description | Unit | |-------|-------------|------| | `temp` | Temperature | °C | | `feelsLike` | Feels-like temperature | °C | | `text` / `icon` | Weather description / icon | — | | `windDir` / `windScale` | Wind direction / wind scale | Beaufort | | `windSpeed` | Wind speed | km/h | | `humidity` | Humidity | % | | `precip` | Precipitation in last 1 hour | mm | | `pressure` | Air pressure | hPa | | `vis` | Visibility | km | | `cloud` | Cloud cover | % | | `dew` | Dew point temperature | °C | ### Daily Forecast `3d/7d/10d/15d/30d` | Field | Description | Unit | |-------|-------------|------| | `fxDate` | Date | — | | `sunrise` / `sunset` | Sunrise / Sunset | HH:MM | | `moonrise` / `moonset` | Moonrise / Moonset | HH:MM | | `moonPhase` | Moon phase | — | | `tempMax` / `tempMin` | Max temp / Min temp | °C | | `textDay` / `textNight` | Daytime / Nighttime weather | — | | `windDirDay` / `windDirNight` | Daytime / Nighttime wind direction | — | | `windScaleDay` / `windScaleNight` | Daytime / Nighttime wind scale | Beaufort | | `humidity` | Humidity | % | | `precip` | Total precipitation for the day | mm | | `uvIndex` | UV index | — | | `pressure` | Air pressure | hPa | | `vis` | Visibility | km | ### Hourly Forecast `24h/72h/168h` | Field | Description | Unit | |-------|-------------|------| | `fxTime` | Forecast time (ISO8601) | — | | `temp` | Temperature | °C | | `text` | Weather description | — | | `windDir` / `windScale` | Wind direction / wind scale | Beaufort | | `humidity` | Humidity | % | | `pop` | Probability of precipitation | % | | `precip` | Precipitation | mm | | `pressure` | Air pressure | hPa | | `cloud` | Cloud cover | % | | `dew` | Dew point temperature | °C | ### Lifestyle Indices `indices` 19 index types available in China: exercise, car washing, dressing, fishing, UV, travel, allergy, comfort, **cold/flu**, pollution diffusion, air conditioning, sunglasses, makeup, clothes drying, traffic, sunscreen, etc. Overseas locations support only 5 types: exercise, car washing, dressing, fishing, UV. | Field | Description | |-------|-------------| | `name` | Index type name | | `category` | Level label | | `text` | Detailed suggestion | ### Air Quality `aqi` Source: WAQI (aqicn.org) | Field | Description | Unit | |-------|-------------|------| | `aqi` | Air Quality Index | — | | `dominentpol` | Dominant pollutant | pm25/pm10/o3/no2 | | `pm25` | PM2.5 | μg/m³ | | `pm10` | PM10 | μg/m³ | | `co` | CO | mg/m³ | | `no2` / `o3` / `so2` | NO₂ / O₃ / SO₂ | μg/m³ | | `t` / `h` | Temperature / Humidity | °C / % | | `p` / `w` | Air pressure / Wind speed | hPa / km/h | | `forecast.pm25[]` | 7-day PM2.5 forecast | avg/min/max | | `forecast.pm10[]` | 7-day PM10 forecast | avg/min/max | AQI levels: Good (≤50) · Moderate (≤100) · Unhealthy for Sensitive (≤150) · Unhealthy (≤200) · Very Unhealthy (≤300) · Hazardous (>300) --- ## Examples ```bash # Current weather by CityId python scripts/weather.py 101180301 # Current weather by English city name (auto-resolved) python scripts/weather.py Shanghai # Current weather by Chinese city name (auto-resolved) python scripts/weather.py 上海 # 7-day forecast python scripts/weather.py 101180301 --endpoint 7d # 24-hour hourly forecast python scripts/weather.py 101180301 --endpoint 24h # 15-day forecast by coordinates (auto-detect order) python scripts/weather.py 113.92,35.30 --endpoint 15d python scripts/weather.py 35.30,113.92 --endpoint 15d # Lifestyle indices python scripts/weather.py 101180301 --endpoint indices # Air quality by city name python scripts/weather.py xinxiang --endpoint aqi python scripts/weather.py 新乡 --endpoint aqi # Air quality by coordinates python scripts/weather.py "geo:35.30;113.92" --endpoint aqi # Raw JSON output python scripts/weather.py 101180301 --json ``` ## Error Codes | QWeather code | WAQI status | Meaning | |---------------|-------------|---------| | 200 | ok | Success | | 400 | error | Bad parameters | | 401 | error | Authentication failed (check key or token) | | 402 | — | Quota exceeded | | 403 | — | Wrong API Host | ## FAQ **Q: "Configuration item not set" error?** A: You need to set the corresponding environment variables or use a `.env` file. **Q: How to find CityId?** A: No need! Just enter the city name (Chinese or English), the script will automatically resolve it. **Q: What cities are supported?** A: Beijing, Shanghai, Guangzhou, Shenzhen, Chengdu, Hangzhou, Nanjing, Zhengzhou, Xi'an, Chongqing, Wuhan, Kunming, Dubai, Singapore, Moscow, Rio de Janeiro, New York, Los Angeles, Chicago, London, Manchester, Edinburgh, Paris, Lyon, Marseille, Tokyo, Osaka, Kyoto, Berlin, Munich, Frankfurt, Sydney, Melbourne, Brisbane, all cities recognized by QWeather GeoAPI are supported.

path: _meta.json
content: ownerId: kn75s192k98dtwr83bft4myd0582ytyg
slug: weather-pulse
version: 1.3.6
publishedAt: 1775385826626

path: scripts/weather.py
content: #!/usr/bin/env python3
"""QWeather + WAQI 天气空气质量查询 - 发布版

QWeather 和风天气: now/3d/7d/10d/15d/30d/24h/72h/168h/indices
WAQI 空气质量: aqi (PM2.5/PM10/CO/NO2/O3/SO2/温湿度/气压/预报)

使用方法:
    python weather.py <location> [--endpoint ENDPOINT] [--json]

Location: CityId (101010100) / 经纬度 (116.41,39.92) / 城市名 (xinxiang)
Endpoints: now / 3d / 7d / 10d / 15d / 30d / 24h / 72h / 168h / indices / aqi

使用前配置:
    1. 设置 QW_HOST 和 QW_KEY (和风天气 API)
    2. 设置 WAQI_TOKEN (WAQI 空气质量 API)
    见下方 CONFIG 区域
"""

import argparse
import gzip
import io
import json
import sys
import os
import time
import urllib.error
import urllib.parse
import urllib.request

# ============================================================
# CONFIG - 使用前请填入你的 API 凭证
# ============================================================
# 和风天气: 注册 https://console.qweather.com
#   API Host: 控制台 -> 设置 -> API Host (格式: xxxxxxx.re.qweatherapi.com)
#   API Key : 控制台 -> 项目管理 -> 凭据 -> API KEY (32位字符串)
#   文档: https://dev.qweather.com/docs/start/
# ============================================================
# 免费额度: 50,000 次/月 (天气 + 指数)
# 环境变量名 → 内部变量名:
#   QWEATHER_API_HOST → QW_HOST
#   QWEATHER_API_KEY  → QW_KEY
# ============================================================
QW_HOST = os.environ.get("QWEATHER_API_HOST", "")
QW_KEY  = os.environ.get("QWEATHER_API_KEY", "")

# ============================================================
# 空气质量数据 (WAQI - World Air Quality Index)
#   [REDACTED_SECRET]
#   免费额度: 1,000 次/小时
#   文档: https://aqicn.org/api/
# 环境变量名 → 内部变量名:
#   WAQI_API_TOKEN → WAQI_TOKEN
# ============================================================
WAQI_TOKEN = os.environ.get("WAQI_API_TOKEN", "")


# Windows console encoding fix
if sys.platform == 'win32':
    if hasattr(sys.stdout, 'buffer'):
        sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')


QW_ENDPOINTS = {
    'now':   'v7/weather/now',
    '3d':    'v7/weather/3d',
    '7d':    'v7/weather/7d',
    '10d':   'v7/weather/10d',
    '15d':   'v7/weather/15d',
    '30d':   'v7/weather/30d',
    '24h':   'v7/weather/24h',
    '72h':   'v7/weather/72h',
    '168h':  'v7/weather/168h',
}


def _check_config(endpoint_key):
    """检查该端点需要的配置是否填写。"""
    # aqi 端点只需要 WAQI_TOKEN
    if endpoint_key == 'aqi':
        if not WAQI_TOKEN:
            print("错误: WAQI 空气质量 API 需要 WAQI_TOKEN")
            print("请设置环境变量 WAQI_API_TOKEN 或在 CONFIG 区域填写 WAQI_TOKEN")
            print("获取 [REDACTED_SECRET]")
            sys.exit(1)
        return
    # 其他端点（天气相关）只需要和风天气凭证
    if not QW_HOST:
        print("错误: 和风天气 API 需要 QWEATHER_API_HOST")
        print("请设置环境变量 QWEATHER_API_HOST 或在 CONFIG 区域填写 QW_HOST")
        print("获取 API Host+Key: https://console.qweather.com")
        sys.exit(1)
    if not QW_KEY:
        print("错误: 和风天气 API 需要 QWEATHER_API_KEY")
        print("请设置环境变量 QWEATHER_API_KEY 或在 CONFIG 区域填写 QW_KEY")
        sys.exit(1)


MAX_RETRIES = 3
RETRY_DELAY = 2  # seconds


def _resolve_city_name(name):
    """通过 QWeather Geo API 将城市名解析为 CityId."""
    url = f'https://{QW_HOST}/geo/v2/city/lookup'
    params = {'key': QW_KEY, 'location': name}
    data = _json_fetch(url + '?' + urllib.parse.urlencode(params))
    if data.get('code') == '200' and data.get('location'):
        city_id = data['location'][0].get('id')
        if city_id:
            return city_id
    return None


def _normalize_location(loc, endpoint_key):
    """智能标准化 location 输入。

    - "101020100" (纯数字) → CityId，直接返回
    - "116.41,39.92" (经纬度) → 对天气端点转为 CityId，对 AQI 端点转为 geo: 格式
    - "geo:39.92;116.41" (AQI 端点) → 对 AQI 直接返回
    - "Shanghai" (英文/中文城市名) → 对天气端点通过 Geo API 解析
      对 AQI 端点保留原名（WAQI 支持英文城市名）
    - "@5789" (AQI 站ID) → 保留原名
    - "here" (AQI 当前位置) → 保留原名
    """
    # 纯数字 → CityId
    if loc.isdigit():
        return loc

    # AQI 端点专有格式 (@ID, here)
    if endpoint_key == 'aqi':
        return loc  # WAQI 支持 "xinxiang", "@5789", "geo:39.92;116.41", "here"

    # 经纬度格式 "116.41,39.92" 或 "39.92,116.41"（自动检测顺序）
    parts = loc.split(',')
    if len(parts) == 2:
        try:
            v1 = float(parts[0].strip())
            v2 = float(parts[1].strip())
            # QWeather 要求 经度,纬度
            # 如果 v1 > 135 说明是纬度在前（纬度不可能>135），交换一下
            if v1 > 135:
                return f"{v2},{v1}"
            # 中国境内范围验证：经度 73-138，纬度 3-54
            if 73 <= v1 <= 138 and 3 <= v2 <= 54:
                return loc  # 已经是 经度,纬度
            if 3 <= v1 <= 54 and 73 <= v2 <= 138:
                return f"{v2},{v1}"  # 原来是 纬度,经度，交换
            return loc  # 其他区域，假设已经是 经度,纬度
        except ValueError:
            pass

    # geo: 格式 (经纬度另一种写法) → 转为逗号分隔 (经度,纬度)
    if loc.startswith('geo:'):
        geo = loc[4:].replace(';', ',')
        return geo

    # 城市名 → 通过 Geo API 解析为 CityId
    city_id = _resolve_city_name(loc)
    if city_id:
        return city_id

    # 解析失败 → 保持原样（让 API 自己报错）
    return loc


def _json_fetch(url, headers=None):
    """通用 JSON GET (gzip 解压)，支持自动重试 3 次。"""
    req = urllib.request.Request(url)
    req.add_header('Accept-Encoding', 'gzip, deflate')
    if headers:
        for k, v in headers.items():
            req.add_header(k, v)
    last_err = None
    for attempt in range(1, MAX_RETRIES + 1):
        try:
            with urllib.request.urlopen(req, timeout=10) as resp:
                raw = resp.read()
                ce = resp.headers.get('Content-Encoding', '')
                if 'gzip' in ce:
                    raw = gzip.decompress(raw)
                return json.loads(raw.decode('utf-8'))
        except (TimeoutError, urllib.error.URLError) as e:
            last_err = str(e)
            if attempt < MAX_RETRIES:
                print(f"[警告] 请求失败 (尝试 {attempt}/{MAX_RETRIES})，{RETRY_DELAY}秒后重试... ({last_err})")
                time.sleep(RETRY_DELAY)
            else:
                print(f"[错误] 请求失败，已重试 {MAX_RETRIES} 次，放弃。({last_err})")
        except urllib.error.HTTPError as e:
            body = e.read().decode('utf-8', errors='replace')
            try:
                return {'error': json.loads(body)}
            except json.JSONDecodeError:
                return {'error': {'status': e.code, 'body': body}}
        except Exception as e:
            last_err = str(e)
            if attempt < MAX_RETRIES:
                print(f"[警告] 请求异常 (尝试 {attempt}/{MAX_RETRIES})，{RETRY_DELAY}秒后重试... ({last_err})")
                time.sleep(RETRY_DELAY)
            else:
                print(f"[错误] 请求异常，已重试 {MAX_RETRIES} 次，放弃。({last_err})")
    return {'error': {'status': 0, 'message': last_err}}


# ============================================================
# QWeather (和风天气) API
# ============================================================
def fetch_qweather(endpoint_key, location, lang='zh', unit='m'):
    """请求和风天气 API."""
    ep = QW_ENDPOINTS.get(endpoint_key, f'v7/{endpoint_key}')
    url = f'https://{QW_HOST}/{ep}'
    params = {'key': QW_KEY, 'location': location, 'lang': lang, 'unit': unit}
    return _json_fetch(url + '?' + urllib.parse.urlencode(params))


# ============================================================
# WAQI (世界空气质量) API
# ============================================================
def fetch_aqi(location):
    """请求 WAQI 空气质量 API.
    location: 城市名(xinxiang) / @ID(5789) / geo:35.30;113.92 / here
    """
    url = f'https://api.waqi.info/feed/{urllib.parse.quote(location)}/?[REDACTED_SECRET]'
    return _json_fetch(url)


# ============================================================
# 格式化输出
# ============================================================
def format_now(d):
    """实况天气."""
    if 'error' in d:
        return f"Error: {json.dumps(d['error'], ensure_ascii=False)}"
    if d.get('code') != '200':
        return f"API Error: code={d.get('code')} {d.get('status', '')}"
    n = d.get('now', {})
    cloud = n.get('cloud', '?')
    dew = n.get('dew', '?')
    cloud_str = f"  Cloud: {cloud}%" if cloud != '?' else ""
    dew_str = f"  Dew: {dew}C" if dew != '?' else ""
    return (
        f"Obs: {n.get('obsTime', '?')}\n"
        f"Temp: {n.get('temp', '?')}C  Feels: {n.get('feelsLike', '?')}C\n"
        f"Weather: {n.get('text', '?')} [{n.get('icon', '?')}]\n"
        f"Wind: {n.get('windDir', '?')} {n.get('windScale', '?')}  {n.get('windSpeed', '?')}km/h ({n.get('wind360', '?')}deg)\n"
        f"Humidity: {n.get('humidity', '?')}%\n"
        f"Precip(1h): {n.get('precip', '?')}mm\n"
        f"Pressure: {n.get('pressure', '?')}hPa\n"
        f"Visibility: {n.get('vis', '?')}km{cloud_str}{dew_str}"
    )


def format_daily(d):
    """逐日预报."""
    if 'error' in d:
        return f"Error: {json.dumps(d['error'], ensure_ascii=False)}"
    if d.get('code') != '200':
        return f"API Error: code={d.get('code')}"
    lines = []
    for day in d.get('daily', []):
        t_range = f"{day.get('tempMin', '?')} ~ {day.get('tempMax', '?')}C"
        day_w = f"{day.get('textDay', '?')} -> {day.get('textNight', '?')}"
        wind = f"{day.get('windDirDay', '?')}{day.get('windScaleDay', '?')} / {day.get('windDirNight', '?')}{day.get('windScaleNight', '?')}"
        moon = day.get('moonPhase', '')
        moon_str = f"  Moon: {moon}" if moon else ""
        lines.append(
            f"  {day.get('fxDate', '?')} | {t_range} | {day_w} "
            f"| Hum {day.get('humidity', '?')}% | UV {day.get('uvIndex', '?')} "
            f"| Precip {day.get('precip', '?')}mm | {wind} "
            f"| Rise {day.get('sunrise', '?')} Set {day.get('sunset', '?')}{moon_str}"
        )
    return '\n'.join(lines)


def format_hourly(d):
    """逐小时预报."""
    if 'error' in d:
        return f"Error: {json.dumps(d['error'], ensure_ascii=False)}"
    if d.get('code') != '200':
        return f"API Error: code={d.get('code')}"
    lines = []
    for h in d.get('hourly', []):
        fx = h.get('fxTime', '?')
        t = fx[11:16] if 'T' in fx else fx
        lines.append(
            f"  {t} | {h.get('temp', '?')}C | {h.get('text', '?')} "
            f"| Hum {h.get('humidity', '?')}% | {h.get('windDir', '?')}{h.get('windScale', '?')} "
            f"| Pop {h.get('pop', '0')}%  Precip {h.get('precip', '0')}mm"
        )
    return '\n'.join(lines)


def format_indices(d):
    """天气生活指数."""
    if 'error' in d:
        return f"Error: {json.dumps(d['error'], ensure_ascii=False)}"
    if d.get('code') != '200':
        return f"API Error: code={d.get('code')}"
    lines = []
    seen = set()
    for item in d.get('daily', []):
        key = item.get('type')
        if key not in seen:
            seen.add(key)
            name = item.get('name', '?')
            cat = item.get('category', '?')
            txt = item.get('text', '')
            lines.append(f"  {name}: [{cat}] {txt}" if txt else f"  {name}: [{cat}]")
    return '\n'.join(lines)


def _aqi_level(score):
    """AQI 等级."""
    try:
        v = int(score)
    except (ValueError, TypeError):
        return "?"
    if v <= 50:
        return "优"
    elif v <= 100:
        return "良"
    elif v <= 150:
        return "轻度污染"
    elif v <= 200:
        return "中度污染"
    elif v <= 300:
        return "重度污染"
    else:
        return "严重污染"


def format_aqi(d):
    """空气质量 - WAQI."""
    if 'error' in d:
        return f"Error: {json.dumps(d['error'], ensure_ascii=False)}"
    if d.get('status') != 'ok':
        return f"API Error: status={d.get('status')}"
    data = d.get('data', {})
    if not data:
        return "No AQI data"
    aqi = data.get('aqi', '?')
    city = data.get('city', {}).get('name', '?')
    dom = data.get('dominentpol', '?').upper()
    t = data.get('time', {}).get('s', '?')
    ia = data.get('iaqi', {})

    def iq(k):
        """安全获取 iaqi 值."""
        return ia.get(k, {}).get('v', '?')

    lines = [
        f"\n{'='*40}",
        f"AQI: {aqi} [{_aqi_level(aqi)}]  主要污染物: {dom}",
        f"City: {city}",
        f"Time: {t}",
        f"{'-'*40}",
        f"  PM2.5: {iq('pm25')}  PM10: {iq('pm10')}",
        f"  CO: {iq('co')}  NO2: {iq('no2')}  O3: {iq('o3')}  SO2: {iq('so2')}",
        f"  Temp: {iq('t')}C  Hum: {iq('h')}%",
        f"  Pressure: {iq('p')}hPa  Wind: {iq('w')}km/h",
        f"{'='*40}",
    ]

    # Forecast
    fc = data.get('forecast', {}).get('daily', {})
    if fc.get('pm25'):
        lines.append("\nPM2.5 预报:")
        for day in fc['pm25']:
            lines.append(f"  {day.get('day', '?')}: avg={day.get('avg', '?')}  min={day.get('min', '?')}  max={day.get('max', '?')}")
    if fc.get('pm10'):
        lines.append("\nPM10 预报:")
        for day in fc['pm10']:
            lines.append(f"  {day.get('day', '?')}: avg={day.get('avg', '?')}  min={day.get('min', '?')}  max={day.get('max', '?')}")
    if fc.get('uvi') or fc.get('uv') or fc.get('uvi'):
        uv_key = 'uvi' if 'uvi' in fc else 'uv'
        if fc.get(uv_key):
            lines.append(f"\nUV 预报:")
            for day in fc[uv_key]:
                lines.append(f"  {day.get('day', '?')}: avg={day.get('avg', '?')}  max={day.get('max', '?')}")

    return '\n'.join(lines)


# ============================================================
# 主入口
# ============================================================
def main():
    parser = argparse.ArgumentParser(
        description='QWeather + WAQI 天气空气质量查询',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # 新乡实况天气
  python weather.py 101180301

  # 7天预报
  python weather.py 101180301 --endpoint 7d

  # 24小时逐小时
  python weather.py 101180301 --endpoint 24h

  # 15天预报 (经纬度)
  python weather.py 113.92,35.30 --endpoint 15d

  # 天气指数 (感冒/穿衣/紫外线)
  python weather.py 101180301 --endpoint indices

  # 空气质量
  python weather.py xinxiang --endpoint aqi

  # 空气质量 (经纬度)
  python weather.py "geo:35.30;113.92" --endpoint aqi

  # 原始 JSON 输出
  python weather.py 101180301 --json
"""
    )
    parser.add_argument('location',
                        help='位置: CityId (101180301) / 经纬度 (113.92,35.30) / 城市名 (xinxiang, aqi端点可)')
    parser.add_argument('--endpoint', default='now',
                        help='端点: now/3d/7d/10d/15d/30d/24h/72h/168h/indices/aqi (默认: now)')
    parser.add_argument('--lang', default='zh', help='语言: zh/en/ja')
    parser.add_argument('--unit', default='m', help='单位: m(公制)/i(英制)')
    parser.add_argument('--json', action='store_true', help='原始 JSON 输出')
    args = parser.parse_args()

    ep = args.endpoint

    # 检查该端点需要的配置
    _check_config(ep)

    # 智能解析 location（天气端点：英文/中文城市名 → CityId；AQI 端点：保持原样）
    resolved_location = _normalize_location(args.location, ep)

    # 调用 API
    if ep == 'aqi':
        data = fetch_aqi(resolved_location)
    elif ep == 'indices':
        url = f'https://{QW_HOST}/v7/indices/1d'
        params = {'key': QW_KEY, 'location': resolved_location,
                  'lang': args.lang, 'unit': args.unit, 'type': '0'}
        data = _json_fetch(url + '?' + urllib.parse.urlencode(params))
    else:
        data = fetch_qweather(ep, resolved_location, args.lang, args.unit)

    # 输出
    if args.json:
        print(json.dumps(data, ensure_ascii=False, indent=2))
        return

    fmt_map = {
        'now':   format_now,
        '3d':    format_daily, '7d': format_daily, '10d': format_daily,
        '15d':   format_daily, '30d': format_daily,
        '24h':   format_hourly, '72h': format_hourly, '168h': format_hourly,
        'indices': format_indices,
        'aqi':   format_aqi,
    }
    formatter = fmt_map.get(ep)
    if formatter:
        print(formatter(data))
    else:
        print(json.dumps(data, ensure_ascii=False, indent=2))


if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)