星的天空的博客

种一颗树,最好的时间是十年前,其次是现在。

0%

Flutter接入Apple的WeatherKit

为什么选择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的封装。

1
2
3
4
5
6
# 在 pubspec.yaml中引入dart_jsonwebtoken框架
dependencies:
flutter:
sdk: flutter

dart_jsonwebtoken: ^2.6.2

签名部分的代码封装如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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");
}

生成签名:

1
2
3
4
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调用

总共提供了两个接口,第一个是查询某地的天气数据集的可用状态,具体使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

第二个是查询某地的天气数据,具体调用样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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<String, dynamic> 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);
}
});

上述接口调用后,返回的数据样例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{
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 和第三方归因