Skip to content

weather-bridge-90005 平台能力对接文档

能力简介

Source file does not provide it

项目
项目名称weather-bridge-90005
厂商Source file does not provide it
型号weather-bridge-90005
业务代码Not applicable
对接模式Source file does not provide it
协议Source file does not provide it

模型清单

可使用下列名称或 ID 在 ThinkLink 中搜索对应模型。

模型类型名称id_name平台模型 ID
RPC[WEATHER FETCH] 90005weather_fetch_90005119139033283457025
RPC[WEATHER CONFIG] 90005weather_set_config_90005119139033283457024
TemplateWEATHER-VIRTUAL120034851645452288
Thing Model[WEATHER] Current & Forecast 90005weather_current_90005119139033283457026

运维 Skill

部署或调试天气配置、取数、customMqtt→AS 转发器、遥测和预报读回时,使用 create-weather。该 Skill 会保护 API key,并按链路定位第一处失败边界。

产品特点与适用范围

本项目面向“bridge”场景。能力边界、字段和可部署模型均以当前 descriptor、requirements 与模型源文件为准。

部署与采集信息

| DTU | Not applicable | | 供电 | Source file does not provide it | | LoRaWAN Class | Source file does not provide it | | 接口 | Source file does not provide it | | 波特率 | Not applicable |

遥测与寄存器定义

源文件未提供独立遥测字段表;请以模型清单中的当前物模型为准。

参数定义

无项目专用参数定义。

Requirements 证据

以下关键事实由 requirements 源文件提取;原始行号用于复核。

类别来源事实来源
部署> 类型:platform 能力项目 | 业务码:90005 | 设计:docs/superpowers/specs/2026-06-15-weather-virtual-device-design.mdrequirement/requirements.md:3
部署> 状态:已生成产物,待部署。requirement/requirements.md:4
协议/数据| base_url | string | dftValue https://api.openweathermap.org | 取数地址(自有源可覆盖)|requirement/requirements.md:66
协议/数据| 2026-06-15 | 数据源接口 = OWM 免费档(current + 5day/3h forecast) | 用户确认;无需绑卡,预报 3h 粒度满足槽位 |requirement/requirements.md:101
部署| 2026-06-16 | 遥测只放实时值;预报移出遥测、改入 shared_attrs.forecast 带时间戳序列(未来3天 3h 粒度 ~24 点),每周期整体刷新 | 用户修订:只有遥测会存历史;预报每周期会变,留历史无意义且污染趋势。查看曲线时历史遥测 + 最新预报序列按 ts 拼成连续曲线。结构选「带时间戳序列」(用户确认)而非旧扁平槽位 f3/d1,便于前端按 ts 直接拼接。物模型 telemetry 字段 35→11;预报 24 字段删除。已部署 PUBLIC 模型需经 SYSADMIN 重新推送(带 _commitMessage |requirement/requirements.md:105

物模型与 RPC 代码

RPC: [WEATHER FETCH] 90005

id_name: weather_fetch_90005 · ID: 119139033283457025

javascript
// weather_fetch_90005 — 虚拟气象设备「取数」RPC
// 读 device.server_attrs 的气象配置 → axios.get(OpenWeatherMap current + 5day/3h forecast)
// → 归一成 {t: 实时遥测, s: 文本 + forecast 预报序列} → customMqtt 动作发布到固定 topic
//   /v32/{tenant}/tkl/up/customMqtt/{eui}
// 关键约定:遥测 t 只放实时获取的实时值(历史化成趋势);预报放 s.forecast(带时间戳序列,
//   每周期整体刷新、不留历史),供前端把历史遥测曲线 + 未来 3 天预报序列按 ts 拼成连续曲线。
// 由「天气→AS」转发器消费后转成 AS 上行 → 设备 payload_parser 落成遥测(设备化)。
//
// 可被排程(KB-27):无交互表单参数,运行时只读 device.server_attrs + axios。
// 平台把本函数体放进 `async function rpcRunner(){ <body> }` 执行,故 body 内可直接 top-level await;
// 平台提供 axios/device/params/logger/org_params/RPCHelper/Utils/BufferTKL 等脚本全局变量。
//   注:本地 `node` 跑会因 axios 未定义而抛错(axios 由平台注入),仅供 node --check 结构校验。
import { RPCHelper, Utils, BufferTKL } from '#tklHelper';

async function rpc_script({ device, params, alarms, logger, org_params }) {
    const num = (v) => (v == null || isNaN(Number(v)) ? null : Number(v));
    const pct = (v) => (v == null ? null : Math.round(Number(v) * 100)); // OWM pop 0-1 → 0-100%

    const cfg = device?.server_attrs || {};
    const source = cfg.source || 'openweathermap';

    // 缺配置不取数(KB-27/31:绝不空请求)
    if (!cfg.apikey) { logger && logger.systemWarn && logger.systemWarn('weather_fetch: missing apikey'); return null; }
    const hasLatLon = cfg.lat != null && cfg.lon != null;
    if (!hasLatLon && !cfg.city) { logger && logger.systemWarn && logger.systemWarn('weather_fetch: missing lat/lon and city'); return null; }

    // 首期只实现 openweathermap;预留 source 分支(qweather/cma/custom 后续增量)
    if (source !== 'openweathermap') { logger && logger.systemWarn && logger.systemWarn(`weather_fetch: source ${source} not implemented`); return null; }

    const base = (cfg.base_url || 'https://api.openweathermap.org').replace(/\/+$/, '');
    const common = { appid: cfg.apikey, units: cfg.units || 'metric', lang: cfg.lang || 'zh_cn' };
    const loc = hasLatLon ? { lat: cfg.lat, lon: cfg.lon } : { q: cfg.city };

    let curResp, fcResp;
    try {
        curResp = await axios.get(`${base}/data/2.5/weather`, { params: { ...common, ...loc } });
        fcResp = await axios.get(`${base}/data/2.5/forecast`, { params: { ...common, ...loc } });
    } catch (e) {
        logger && logger.systemError && logger.systemError(`weather_fetch http error: ${e && e.message}`, { status: e && e.response && e.response.status });
        return null; // 取数失败/超时/非 200 → 不写脏数据,下个周期重试
    }
    const c = curResp && curResp.data;
    const fd = fcResp && fcResp.data;
    if (!c || !fd) return null;

    const fetchedAt = new Date().toISOString();

    // ── 当前天气(遥测,全部是实时获取的实时值,历史化成趋势)──
    // 遥测只放实时数据:预报不进遥测(预报每周期会变,不应留历史点)。
    const t = {
        temp: num(c.main && c.main.temp),
        feels_like: num(c.main && c.main.feels_like),
        humidity: num(c.main && c.main.humidity),
        pressure: num(c.main && c.main.pressure),
        wind_speed: num(c.wind && c.wind.speed),
        wind_deg: num(c.wind && c.wind.deg),
        clouds: num(c.clouds && c.clouds.all),
        visibility: num(c.visibility),
        rain_1h: num(c.rain && c.rain['1h']) != null ? num(c.rain && c.rain['1h']) : 0,
        weather_code: num(c.weather && c.weather[0] && c.weather[0].id),
    };

    // ── 预报序列(放 shared_attrs,随遥测每周期实时刷新;整体覆盖、不留历史)──
    // 取未来 3 天(72h)内的 OWM 3h 预报点 → 带时间戳的序列,供前端把
    //   「历史遥测曲线(过去真实值)」与「未来 3 天预报序列」按 ts 直接拼成一条连续曲线。
    const list = Array.isArray(fd.list) ? fd.list : [];
    t.pop = list[0] ? pct(list[0].pop) : null; // 临近(当前时刻)降水概率取 forecast[0],属实时遥测

    const nowSec = Math.floor(new Date(fetchedAt).getTime() / 1000);
    const horizonSec = nowSec + 3 * 24 * 3600; // 未来 3 天
    const points = list
        .filter((e) => e && e.dt != null && Number(e.dt) <= horizonSec)
        .map((e) => ({
            ts: new Date(Number(e.dt) * 1000).toISOString(),
            temp: num(e.main && e.main.temp),
            pop: pct(e.pop),
            code: num(e.weather && e.weather[0] && e.weather[0].id),
        }));

    // ── 文本 + 预报序列属性(shared_attrs,不入遥测趋势)──
    const w0 = (c.weather && c.weather[0]) || {};
    const s = {
        weather_main: w0.main != null ? w0.main : null,
        weather_desc: w0.description != null ? w0.description : null,
        city: c.name != null ? c.name : (cfg.city || null),
        source: source,
        fetched_at: fetchedAt,
        // 顶层 key(mergeDeviceAttrs 顶层浅合并 → 每周期整体覆盖该 forecast 对象,永远是最新预报)
        forecast: { fetched_at: fetchedAt, points: points },
    };

    // ── 发布到固定 customMqtt topic,交「天气→AS」转发器转 AS 上行 ──
    return [{ sleepTimeMs: 0, type: 'customMqtt', dnMsg: { eui: device.eui, t: t, s: s } }];
}

// ── 本地结构占位测试(axios 由平台注入,本地不可跑通;仅供 node --check 结构校验)──
rpc_script({
    device: { eui: '0123456789abcdef', tenant_code: 'PUBLIC', server_attrs: { apikey: 'TEST', lat: 36.07, lon: 120.38, source: 'openweathermap', units: 'metric', lang: 'zh_cn' } },
    params: {},
    alarms: [],
    logger: console,
    org_params: {},
}).then((r) => console.log(JSON.stringify(r, null, 2))).catch((e) => console.error(e));

RPC: [WEATHER CONFIG] 90005

id_name: weather_set_config_90005 · ID: 119139033283457024

javascript
// weather_set_config_90005 — 虚拟气象设备「配置」RPC(交互表单,写 server_attrs)
// 把数据源 / apikey / 取数地址 / 定位(经纬度或城市) / 单位 / 语言 / 预报地平线 写进 device.server_attrs。
// weather_fetch 运行时读这些配置取数。表单各项 default_from:"server_attrs" 回填当前值(KB-31 闭环)。
//
// server_attrs 为扁平顶层键(平台 mergeDeviceAttrs 顶层浅合并 → 只写本次提交的键,其余保留,KB-36 安全)。
// 配置 RPC 必须有非空脚本(KB-34):attr_field_name 只供表单回填,写回靠 rpc_script 的 modifyAttrs。
import { RPCHelper, Utils, BufferTKL } from '#tklHelper';

function rpc_script({ device, params, alarms, logger, org_params }) {
    // 读-改-写:从设备现存 server_attrs 播种,再叠加本次提交字段(缺省 null 跳过,KB-31/36)
    const sa = {};
    const set = (k) => { if (params[k] !== undefined && params[k] !== null && params[k] !== '') sa[k] = params[k]; };

    set('source');
    set('apikey');
    set('base_url');
    set('units');
    set('lang');
    set('forecast_window');
    set('city');
    // lat/lon 为数值:0 是合法经度,故只跳过 null/undefined(不按空串判定)
    if (params.lat !== undefined && params.lat !== null) sa.lat = Number(params.lat);
    if (params.lon !== undefined && params.lon !== null) sa.lon = Number(params.lon);

    if (Object.keys(sa).length === 0) return null;
    return [{ sleepTimeMs: 0, type: 'modifyAttrs', dnMsg: { server_attrs: sa } }];
}

// ── 本地结构占位测试(仅供 node --check 结构校验)──
console.log(rpc_script({
    device: { eui: '0123456789abcdef', tenant_code: 'PUBLIC', server_attrs: {} },
    params: { source: 'openweathermap', apikey: 'KEY', base_url: 'https://api.openweathermap.org', units: 'metric', lang: 'zh_cn', forecast_window: '24h', lat: 36.07, lon: 120.38, city: 'Qingdao' },
    alarms: [],
    logger: console,
    org_params: {},
}));

Thing Model: [WEATHER] Current & Forecast 90005

id_name: weather_current_90005 · ID: 119139033283457026

javascript
// weather_current_90005 — 虚拟气象设备遥测物模型(payload_parser_type = CUSTOMER)
// 数据不来自 LoRaWAN 上行,而来自「天气→AS」转发器注入的 AS 上行:
//   weather_fetch RPC 归一出 {t: 实时遥测, s: 文本 + forecast 预报序列} → 转发器置入 userdata.payload = base64(JSON) → AS 上行。
// 本 parser 解 base64 JSON,把 t 落成 telemetry_data(实时值,历史化成趋势)、
//   s 落成 shared_attrs(文本属性 + forecast 未来3天预报序列,每周期刷新、不入遥测趋势)。
// 查看曲线时:历史遥测(过去真实值)+ shared_attrs.forecast.points(未来3天最新预报)按时间戳拼成连续曲线。
//
// CUSTOMER 类型:平台提供 msg/Buffer/device,并调用解析函数;
//   body 必须 return {telemetry_data, shared_attrs}。无需 LoRaWAN bytes 解码(非二进制报文)。
import {BufferTKL, PayloadParser, RPCHelper, Utils} from '#tklHelper';

function payload_parser({ device, msg, thingModelId, noticeAttrs }) {
    let o;
    try {
        const raw = msg && msg.userdata && msg.userdata.payload;
        if (raw == null) return null;
        // 转发器以 base64(JSON.stringify({t,s})) 注入;兼容直接传对象的情形
        o = typeof raw === 'string' ? JSON.parse(Buffer.from(raw, 'base64').toString('utf8')) : raw;
    } catch (e) {
        return null; // 解析失败不写脏数据
    }
    if (!o || typeof o !== 'object') return null;
    let result = {
        telemetry_data: o.t || null,
        shared_attrs: o.s || null,
    };
    // ── rpcHook (设备对接通用规则) ──
    if (typeof RPCHelper.buildRpcHookAction === "function") {
      const __a = RPCHelper.buildRpcHookAction({ device, msg, thingModelId, idName: "weather_current_90005", name: "[WEATHER] Current & Forecast 90005", parsed: { telemetry_data: result.telemetry_data, server_attrs: result.server_attrs, shared_attrs: result.shared_attrs } });
      if (__a) { result.actions = result.actions || []; result.actions.push(__a); }
    }
    return result;
}

// ── 本地测试(仅供 node --check + 本地跑通验证)──
const _demo = { t: { temp: 21.3, humidity: 58, pressure: 1012, wind_speed: 3.1, weather_code: 800, pop: 10 }, s: { weather_main: 'Clear', weather_desc: '晴', city: 'Qingdao', source: 'openweathermap', fetched_at: '2026-06-15T00:00:00.000Z', forecast: { fetched_at: '2026-06-15T00:00:00.000Z', points: [{ ts: '2026-06-15T03:00:00.000Z', temp: 20.8, pop: 10, code: 800 }, { ts: '2026-06-15T06:00:00.000Z', temp: 24.0, pop: 0, code: 801 }] } } };
console.log(payload_parser({
    device: { eui: '0123456789abcdef', tenant_code: 'PUBLIC' },
    msg: { userdata: { port: 1, payload: Buffer.from(JSON.stringify(_demo)).toString('base64') } },
    thingModelId: '119139033283457026',
    noticeAttrs: {},
}));

物模型字段定义

物模型字段类型单位名称
weather_current_90005tempnumber°CTemperature
weather_current_90005feels_likenumber°CFeels Like
weather_current_90005humiditynumber%Humidity
weather_current_90005pressurenumberhPaPressure
weather_current_90005wind_speednumberm/sWind Speed
weather_current_90005wind_degnumber°Wind Direction
weather_current_90005cloudsnumber%Cloudiness
weather_current_90005visibilitynumbermVisibility
weather_current_90005rain_1hnumbermmRain (1h)
weather_current_90005popnumber%Precipitation Probability
weather_current_90005weather_codenumberWeather Code

RPC 参数定义

RPC参数类型说明默认值单位
weather_set_config_90005sourcestringData Sourceopenweathermap
weather_set_config_90005apikeystringAPI Key
weather_set_config_90005base_urlstringBase URLhttps://api.openweathermap.org
weather_set_config_90005latnumberLatitude°
weather_set_config_90005lonnumberLongitude°
weather_set_config_90005citystringCity
weather_set_config_90005unitsstringUnitsmetric
weather_set_config_90005langstringLanguagezh_cn
weather_set_config_90005forecast_windowstringForecast Window24h

触发器定义

无项目专用触发器。

第三方平台数据订阅

MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}

json
{
  "eui": "6353012af10a9331",
  "active_time": "2026-07-13T08:35:48.000Z",
  "thingModelId": "Use the platform model ID from the model inventory",
  "thingModelIdName": "Use the current telemetry model id_name",
  "telemetry_data": {
    "snr": 13.5,
    "rssi": -51,
    "battery": 3.37
  }
}

模板选择与验证

在 ThinkLink 平台按项目名称、业务代码或模型清单中的 id_name、ID 搜索并绑定模型。部署后使用真实上行帧核对字段、单位、RPC 和触发器行为。

补充说明

需求与决策依据:requirement/requirements.md