Flutter接入Apple的WeatherKit

語言: CN / TW / HK

title: Flutter接入Apple的WeatherKit date: 2023-01-01 10:33:56 tags:


為什麼選擇WeatherKit

最近個人在開發一個Flutter專案,是釣魚相關的,提供水文查詢,釣點記錄,釣魚記錄等功能。其中釣魚記錄需要關聯到天氣相關的內容,主要是要用到歷史天氣資料。網上提供天氣API的服務商很多,我查了一些資料,最終發現visualcrossing這家的介面設計和資料比較符合我的要求,但是這些天氣API的免費額度都很低,如果只是個人玩玩是夠用,釋出出去商用肯定是需要充值的。後面查資料發現Apple在WWDC22新推出了WeatherKit,除了系統原生庫WeatherKit 外(僅支援Apple平臺,iOS 16.0+,iPadOS 16.0+,macOS 13.0+,Mac Catalyst 16.0+,tvOS 16.0+,watchOS 9.0+),還提供了Weather Kit REST API,而這個是全平臺支援的,同時Apple開發者會員資格提供50萬次呼叫/月的額度,並且支援歷史天氣資料的查詢,剛好滿足我Flutter專案的要求。

WeatherKit也是收費的,會員資格提供50萬次呼叫/月的額度,超過需要額外付費,詳細內容見:開始使用 WeatherKit

如何接入WeatherKit

1. 新增WeatherKit許可權

在開發者中心網站Certificates, Identifiers & Profiles頁面中選擇Identifiers欄目,然後選擇需要開啟WeatherKit許可權的專案:

進入後,選擇App Service欄目並勾選WeatherKit選項:

注意這裡AppID Prefix就是你賬戶的Team ID

2. 註冊WeatherKit金鑰

Certificates, Identifiers, and Profiles頁面選擇Keys選項,然後選擇Keys右邊的加號,進入新建頁面,勾選WeatherKit 選項,註冊一個Key。

這裡特別需要注意的是,系統會提示下載一個金鑰檔案,是簽名WeatherKit服務的私鑰,只能下載一次,一定要妥善保管到安全的位置且不能洩露。註冊後我們需要拿到Key ID,在後續簽名時需要用到:

完成以上操作後會在Services選項裡面看到WeatherKit,點選View後滑到頁面底部,可以看到WeatherKit serviceidentifier:

完成上述操作後,我們得到以下內容: 1. Team ID 2. 金鑰 3. WeatherKit Service identifier 4. WeatherKit Key ID

Weather Kit REST API簽名

Weather Kit REST API使用JWT(JSON Web Token)ES256演算法作為請求授權認證,通常這一步應該在後端伺服器上完成。具體內容可以在官方文件Request authentication for WeatherKit REST API中看到。由於現在開發的專案還沒有做後臺開發,為了方便呼叫,我先用Dart語言編寫簽名程式碼。

首先我們需要引入dart_jsonwebtoken框架,它提供了JWT的封裝。

```yaml

在 pubspec.yaml中引入dart_jsonwebtoken框架

dependencies: flutter: sdk: flutter

dart_jsonwebtoken: ^2.6.2 簽名部分的程式碼封裝如下:Dart import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

class AppleWeatherKitSign { final String secretKey; final String iss; final String sub; final String kid;

AppleWeatherKitSign( {required this.secretKey, required this.iss, required this.sub, required this.kid});

String sign(DateTime expirationDate) { final id = "$iss.$sub"; final jwt = JWT({ "iss": iss, "iat": DateTime.now().millisecondsSinceEpoch ~/ 1000, "exp": expirationDate.millisecondsSinceEpoch ~/ 1000, "sub": sub }, header: { "alg": "ES256", "kid": kid, "id": id });

final key = ECPrivateKey(secretKey);
final token = jwt.sign(key, algorithm: JWTAlgorithm.ES256);
return token;

}

factory AppleWeatherKitSign.fromDefaultConstants() => AppleWeatherKitSign( secretKey: "填入:註冊Key時下載的金鑰", iss: "填入:Team ID", sub: "填入:WeatherKit Service identifier", kid: "填入:WeatherKit Key ID"); } ```

生成簽名:

Dart final token = AppleWeatherKitSign.fromDefaultConstants() .sign(DateTime.now().add(const Duration(days: 1))); print('Signed token: $token'); // Signed token: eyJhbGciOiJFUzI1NiIsImtpZCI6IjY1UDk5NExBTlAiLCJpZCI6IlBHTFI4U1hQUVAuY29tLmRldmx4eC5maXNoaW5nV2VhdGhlclNlcnZpY2UiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJQR0xSOFNYUFFQIiwiaWF0IjoxNjcyNTgzMTY3LCJleHAiOjE2NzEyOTY5ODYsInN1YiI6ImNvbS5kZXZseHguZmlzaGluZ1dlYXRoZXJTZXJ2aWNlIn0.Ca7BY43zbtmSZTPD6zDBKIjiS8w45txCYBK3zOmgSwgUI-u8Z2-UlXH-7Xo1aQ0MUipvj8uYjekSdxB2FOI6eB

Weather Kit REST API呼叫

總共提供了兩個介面,第一個是查詢某地的天氣資料集的可用狀態,具體使用方式如下:

```Dart import 'package:tuple/tuple.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

test("request weather kit availability", () async { try { final token = AppleWeatherKitSign.fromDefaultConstants() .sign(DateTime.now().add(const Duration(days: 1))); const coordinate = Tuple2(28.212151, 112.955606); var dio = Dio(); final response1 = await dio.get( 'https://weatherkit.apple.com/api/v1/availability/${coordinate.item1}/${coordinate.item2}', options: Options(headers: {"Authorization": "Bearer $token"})); // 這裡的country引數需要符合規範 ISO Alpha-2 country code: https://www.iban.com/country-codes final response2 = await dio.get( 'https://weatherkit.apple.com/api/v1/availability/${coordinate.item1}/${coordinate.item2}?country=CN', options: Options(headers: {"Authorization": "Bearer $token"})); print(response1.data); // [currentWeather, forecastDaily, forecastHourly] print(response2.data); // [currentWeather, forecastDaily, forecastHourly] expect(response1.data, response2.data); } catch (e) { print(e); expect(true, false); } }); ```

極端天氣警報和空氣質量等資料集不是所有國家可用,因此需要額外提供國家程式碼進行查詢。目前測試中國(CN)返回currentWeather/forecastDaily/forecastHourly, 美國(US)會多返回 weatherAlerts

第二個是查詢某地的天氣資料,具體呼叫樣例如下:

```Dart import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:tuple/tuple.dart'; import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';

test("request weather data", () async { try { initializeDateFormatting(); final token = AppleWeatherKitSign.fromDefaultConstants() .sign(DateTime.now().add(const Duration(days: 1))); const language = "zh-CN"; const coordinate = Tuple2(28.212151, 112.955606);

final dateFormat = DateFormat("yyyy-MM-dd'T'HH:mm:ss", "zh-CN"); Map parameters = {}; parameters["countryCode"] = "zh-CN"; parameters["timeZone"] = "Asia/Shanghai"; const dataSets = ["forecastHourly"]; parameters["dataSets"] = dataSets.map((e) => e).join(","); final hourlyStart = DateTime.now().subtract(const Duration(hours: 1)); final hourlyEnd = DateTime.now(); parameters["hourlyStart"] = "${dateFormat.format(hourlyStart)}Z"; parameters["hourlyEnd"] = "${dateFormat.format(hourlyEnd)}Z";

var dio = Dio(); final response1 = await dio.get( 'https://weatherkit.apple.com/api/v1/weather/$language/${coordinate.item1}/${coordinate.item2}', queryParameters: parameters, options: Options(headers: {"Authorization": "Bearer $token"})); print(response1.data); expect(response1.data["forecastHourly"] != null, true); } catch (e) { print(e); expect(true, false); } }); ```

上述介面呼叫後,返回的資料樣例如下:

JSON { forecastHourly: { name: HourlyForecast, metadata: { attributionURL: https://weatherkit.apple.com/legal-attribution.html, expireTime: 2023-01-01T16:02:18Z, latitude: 28.212, longitude: 112.956, readTime: 2023-01-01T15:02:18Z, reportedTime: 2023-01-01T14:00:00Z, units: m, version: 1, }, hours: [ { forecastStart: 2023-01-01T22:00:00Z, cloudCover: 0.98, conditionCode: Cloudy, daylight: false, humidity: 0.92, precipitationAmount: 0.0, precipitationIntensity: 0.0, precipitationChance: 0.0, precipitationType: clear, pressure: 1031.96, pressureTrend: steady, snowfallIntensity: 0.0, snowfallAmount: 0.0, temperature: 4.93, temperatureApparent: 1.97, temperatureDewPoint: 3.71, uvIndex: 0, visibility: 14391.75, windDirection: 339, windGust: 25.43, windSpeed: 13.11, }, ... ], }, }

上述內容只提供了訪問歷史資料的樣例,當然WeatherKit也提供了獲取未來天氣資料的功能,具體可以檢視API文件,根據文件內容依葫蘆畫瓢填入相應引數即可,這裡不做特別說明。

歸因要求

無論是App或者Web網站,只要用到了WeatherKit,都必須清晰地展示 Apple“天氣”商標 (天氣) 以及指向其他資料來源的法律連結。具體檢視:Apple“天氣”App 和第三方歸因

問題

對於Weather Kit現在還沒有過多使用,畢竟專案還沒有正式上線,但是測試時有發現它存在資料不準確的問題。我在獲取當前定位位置的歷史天氣資料時,發現與實際有4攝氏度左右的差別,REST API現在還是Beta狀態,期望後續能修復這個問題。另外就是Weather Kit只能提供2021-08-01以後的資料,更早的歷史記錄是無法獲取的。

如果您知道更多的Weather Kit問題,歡迎在評論區告知。

參考資料:

  1. Apple WeatherKit REST API 上手
  2. 開始使用 WeatherKit
  3. Request authentication for WeatherKit REST API
  4. weatherkitrestapi
  5. Hourly history with WeatherKit REST API
  6. Apple“天氣”App 和第三方歸因

by 星的天空