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] 90005 | weather_fetch_90005 | 119139033283457025 |
| RPC | [WEATHER CONFIG] 90005 | weather_set_config_90005 | 119139033283457024 |
| Template | WEATHER-VIRTUAL | 120034851645452288 | |
| Thing Model | [WEATHER] Current & Forecast 90005 | weather_current_90005 | 119139033283457026 |
运维 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.md | requirement/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
// 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
// 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
// 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_90005 | temp | number | °C | Temperature |
| weather_current_90005 | feels_like | number | °C | Feels Like |
| weather_current_90005 | humidity | number | % | Humidity |
| weather_current_90005 | pressure | number | hPa | Pressure |
| weather_current_90005 | wind_speed | number | m/s | Wind Speed |
| weather_current_90005 | wind_deg | number | ° | Wind Direction |
| weather_current_90005 | clouds | number | % | Cloudiness |
| weather_current_90005 | visibility | number | m | Visibility |
| weather_current_90005 | rain_1h | number | mm | Rain (1h) |
| weather_current_90005 | pop | number | % | Precipitation Probability |
| weather_current_90005 | weather_code | number | Weather Code |
RPC 参数定义
| RPC | 参数 | 类型 | 说明 | 默认值 | 单位 |
|---|---|---|---|---|---|
| weather_set_config_90005 | source | string | Data Source | openweathermap | |
| weather_set_config_90005 | apikey | string | API Key | — | |
| weather_set_config_90005 | base_url | string | Base URL | https://api.openweathermap.org | |
| weather_set_config_90005 | lat | number | Latitude | — | ° |
| weather_set_config_90005 | lon | number | Longitude | — | ° |
| weather_set_config_90005 | city | string | City | — | |
| weather_set_config_90005 | units | string | Units | metric | |
| weather_set_config_90005 | lang | string | Language | zh_cn | |
| weather_set_config_90005 | forecast_window | string | Forecast Window | 24h |
触发器定义
无项目专用触发器。
第三方平台数据订阅
MQTT Topic: /v32/{Organization Account}/tkl/up/telemetry/{eui}
{
"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。