import Toybox.Lang; import Toybox.Application; import Toybox.Background; import Toybox.Communications; import Toybox.System; import Toybox.Time; import Toybox.Position; // ============================================================================ // OpenWeatherMap —— 后台拉取端 // ---------------------------------------------------------------------------- // 表盘进程绝大多数时间没在运行,onUpdate 又必须够快,不可能在绘制路径里发 // HTTP 请求。Connect IQ 的方案是**后台服务**:系统按定时器唤醒一个独立进程, // 在里面发请求,把结果通过 Background.exit() 交回前台,前台再写进 Storage。 // // 每次唤醒发两个请求: // 1. 当前天气 /data/2.5/weather // 2. 短期预报 /data/2.5/forecast?cnt=16(约 48 小时,够算明日/后日高低温) // // ⚠️ 预报接口必须带 cnt 限制条数。后台进程的内存预算非常小(约 32KB), // 完整的 5 天 / 3 小时预报解析出来能有几十 KB,会直接爆掉。 // // 响应会被**压平成一个扁平字典**再交回去,键名是短名("temp"/"hi1"…), // 与 Owm.get() 的取值一一对应 —— 这样表盘侧永远不用扛一棵 JSON 树。 // ============================================================================ (:background) class Fenix8V3Background extends System.ServiceDelegate { private var mResult as Dictionary = {}; private var mPending = 0; function initialize() { ServiceDelegate.initialize(); } // 系统按定时器唤醒后台进程时调用。 // 注意这里是**独立进程**,内存预算比前台小得多,而且不能碰 UI。 function onTemporalEvent() as Void { if (!Owm.enabled()) { Background.exit(null); return; } var lat = Application.Storage.getValue("lastLat"); var lon = Application.Storage.getValue("lastLon"); if (lat == null || lon == null) { // 表盘一拿到位置就会存进 Storage(见 Fields.Ctx.where)。 // 在那之前没有坐标可问,直接退出。 Background.exit(null); return; } mResult = {}; mPending = 2; // 一律用 units=metric 请求,单位换算统一放到 Fields 里按用户设置做, // 这样切换单位不用重新联网。 var common = {"lat" => lat, "lon" => lon, "appid" => Owm.key(), "units" => "metric"}; var opts = { :method => Communications.HTTP_REQUEST_METHOD_GET, :responseType => Communications.HTTP_RESPONSE_CONTENT_TYPE_JSON }; Communications.makeWebRequest( "https://api.openweathermap.org/data/2.5/weather", common, opts, method(:onCurrent)); var fc = {"lat" => lat, "lon" => lon, "appid" => Owm.key(), "units" => "metric", "cnt" => 16}; Communications.makeWebRequest( "https://api.openweathermap.org/data/2.5/forecast", fc, opts, method(:onForecast)); } // 当前天气的回调。OWM 的 JSON 是嵌套的,逐层取值时每层都要判空 —— // 缺字段是常态(比如没下雨就没有 rain 节点)。 function onCurrent(code as Number, body as Dictionary?) as Void { if (code == 200 && body != null) { var main = body.get("main") as Dictionary?; if (main != null) { put("temp", main.get("temp")); put("feels", main.get("feels_like")); put("tmin", main.get("temp_min")); put("tmax", main.get("temp_max")); put("pressure", main.get("pressure")); put("humidity", main.get("humidity")); } put("visibility", body.get("visibility")); var wind = body.get("wind") as Dictionary?; if (wind != null) { put("wind", wind.get("speed")); put("winddeg", wind.get("deg")); put("gust", wind.get("gust")); } var clouds = body.get("clouds") as Dictionary?; if (clouds != null) { put("clouds", clouds.get("all")); } var rain = body.get("rain") as Dictionary?; if (rain != null) { put("rain", rain.get("1h")); } var snow = body.get("snow") as Dictionary?; if (snow != null) { put("snow", snow.get("1h")); } var wx = body.get("weather") as Array?; if (wx != null && wx.size() > 0) { var w0 = wx[0] as Dictionary; put("short", w0.get("main")); put("long", w0.get("description")); } put("city", body.get("name")); put("dt", Time.now().value()); } done(); } // 预报返回的是 3 小时粒度的列表。这里按「距今天几天」把条目分到 // 明天 / 后天两个桶里,各自取最高最低温;降水概率取明天的最大值。 function onForecast(code as Number, body as Dictionary?) as Void { if (code == 200 && body != null) { var list = body.get("list") as Array?; if (list != null) { var today = dayNumber(Time.now().value()); var lo1 = null; var hi1 = null; var lo2 = null; var hi2 = null; var pop1 = null; var arr = list; for (var i = 0; i < arr.size(); i++) { var e = arr[i] as Dictionary; var dt = e.get("dt"); if (dt == null) { continue; } var day = dayNumber(dt as Number) - today; var m = e.get("main") as Dictionary?; if (m == null) { continue; } var tmin = m.get("temp_min"); var tmax = m.get("temp_max"); if (day == 1) { if (lo1 == null || tmin < lo1) { lo1 = tmin; } if (hi1 == null || tmax > hi1) { hi1 = tmax; } var pop = e.get("pop"); if (pop != null && (pop1 == null || pop > pop1)) { pop1 = pop; } } else if (day == 2) { if (lo2 == null || tmin < lo2) { lo2 = tmin; } if (hi2 == null || tmax > hi2) { hi2 = tmax; } } } put("lo1", lo1); put("hi1", hi1); put("lo2", lo2); put("hi2", hi2); put("pop", pop1); } } done(); } // 距 Unix 纪元的整天数,用来给预报条目分桶。 function dayNumber(unix as Number) as Number { return unix / 86400; } // 只写入非空值,免得用 null 覆盖字典里已有的内容。 function put(k as String, v) as Void { if (v != null) { mResult.put(k, v); } } // 两个请求都回来了才算一轮完整数据,谁最后完成谁负责退出服务。 // 一条都没拿到就传 null,避免用空字典覆盖掉上一次的好数据。 function done() as Void { mPending--; if (mPending > 0) { return; } if (mResult.size() == 0) { Background.exit(null); } else { Background.exit(mResult); } } }