├── .env.example ├── Gemfile ├── .gitignore ├── doc ├── team_id-crunch.png └── register_key-crunch.png ├── lib ├── tenkit │ ├── tenkit_error.rb │ ├── trend_comparison.rb │ ├── version.rb │ ├── response.rb │ ├── next_hour_forecast.rb │ ├── utils.rb │ ├── weather_alert.rb │ ├── weather_response.rb │ ├── weather_alert_response.rb │ ├── weather_alert_collection.rb │ ├── config.rb │ ├── weather.rb │ ├── client.rb │ └── container.rb └── tenkit.rb ├── Rakefile ├── spec └── tenkit │ ├── spec_helper.rb │ ├── tenkit │ ├── config_spec.rb │ ├── utils_spec.rb │ ├── container_spec.rb │ ├── weather_alert_spec.rb │ └── weather_spec.rb │ └── tenkit_spec.rb ├── tenkit.gemspec ├── .github └── workflows │ └── ruby.yml ├── LICENSE ├── test └── fixtures │ ├── currentWeather.json │ ├── alert.json │ ├── forecastHourly.json │ └── forecastDaily.json ├── Gemfile.lock └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | KID= 2 | TID= 3 | SID= 4 | AUTH_KEY= 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | AuthKey_* 3 | *.gem 4 | .tool-versions 5 | -------------------------------------------------------------------------------- /doc/team_id-crunch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superbasicxyz/tenkit/HEAD/doc/team_id-crunch.png -------------------------------------------------------------------------------- /lib/tenkit/tenkit_error.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class TenkitError < StandardError 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /lib/tenkit/trend_comparison.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class TrendComparison < Container; end 3 | end 4 | -------------------------------------------------------------------------------- /doc/register_key-crunch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superbasicxyz/tenkit/HEAD/doc/register_key-crunch.png -------------------------------------------------------------------------------- /lib/tenkit/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tenkit 4 | VERSION = '0.0.7' 5 | end 6 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /lib/tenkit/response.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class Response 3 | attr_reader :raw 4 | 5 | def initialize(response) 6 | @raw = response 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tenkit/next_hour_forecast.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class NextHourForecast 3 | def initialize(forecast_next_hour) 4 | return if forecast_next_hour.nil? 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/tenkit/utils.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class Utils 3 | def self.snake(str) 4 | return str.underscore if str.respond_to? :underscore 5 | 6 | str.gsub(/(.)([A-Z])/, '\1_\2').sub(/_UR_L$/, "_URL").downcase 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/tenkit/weather_alert.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class WeatherAlert 3 | attr_reader :summary 4 | 5 | def initialize(response) 6 | parsed_response = JSON.parse(response.body) 7 | @summary = WeatherAlertSummary.new(parsed_response) 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/tenkit/weather_response.rb: -------------------------------------------------------------------------------- 1 | require_relative './response' 2 | 3 | module Tenkit 4 | class WeatherResponse < Response 5 | attr_reader :weather 6 | 7 | def initialize(response) 8 | super 9 | @weather = Weather.new(response) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/tenkit/weather_alert_response.rb: -------------------------------------------------------------------------------- 1 | require_relative "response" 2 | 3 | module Tenkit 4 | class WeatherAlertResponse < Response 5 | attr_reader :weather_alert 6 | def initialize(response) 7 | super 8 | @weather_alert = Tenkit::WeatherAlert.new(response) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/tenkit/weather_alert_collection.rb: -------------------------------------------------------------------------------- 1 | module Tenkit 2 | class WeatherAlertCollection 3 | attr_reader :alerts, :details_url 4 | 5 | def initialize(weather_alerts) 6 | return if weather_alerts.nil? 7 | 8 | @alerts = weather_alerts['alerts'].map { |alert| WeatherAlertSummary.new(alert) } 9 | @details_url = weather_alerts['detailsUrl'] 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/tenkit/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../lib/tenkit' 2 | 3 | require 'webmock/rspec' 4 | require 'dotenv' 5 | Dotenv.load 6 | 7 | Tenkit.configure do |c| 8 | c.team_id = ENV.fetch('TID', 'A1A1A1A1A1') 9 | c.service_id = ENV.fetch('SID', 'com.mydomain.myapp') 10 | c.key_id = ENV.fetch('KID', 'B2B2B2B2B2') 11 | c.key = ENV.fetch('AUTH_KEY', '-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----') 12 | end 13 | -------------------------------------------------------------------------------- /lib/tenkit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'tenkit/container' 4 | require_relative 'tenkit/client' 5 | require_relative 'tenkit/config' 6 | require_relative 'tenkit/version' 7 | require_relative 'tenkit/weather' 8 | require_relative 'tenkit/weather_alert' 9 | require_relative 'tenkit/tenkit_error' 10 | 11 | module Tenkit 12 | class << self 13 | attr_accessor :config 14 | end 15 | 16 | def self.configure 17 | self.config ||= Config.new 18 | yield config 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/tenkit/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Tenkit 4 | class Config 5 | attr_accessor :team_id, :service_id, :key_id, :key 6 | 7 | REQUIRED_ATTRIBUTES = [ 8 | :@team_id, 9 | :@service_id, 10 | :@key_id, 11 | :@key 12 | ] 13 | 14 | def validate! 15 | missing_required = REQUIRED_ATTRIBUTES.filter { |required| instance_variable_get(required).nil? } 16 | if missing_required.length > 0 17 | raise TenkitError, "#{missing_required.join(', ')} cannot be blank. check that you have configured your credentials" 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit/config_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative '../../../lib/tenkit' 2 | 3 | RSpec.describe Tenkit::Config do 4 | describe '#validate!' do 5 | it 'raises a Tenkit::TenkitError exception if missing required config vals' do 6 | config = Tenkit::Config.new 7 | expect { config.validate! }.to raise_error Tenkit::TenkitError 8 | config.team_id = '123' 9 | expect { config.validate! }.to raise_error Tenkit::TenkitError 10 | config.service_id = 'abc' 11 | expect { config.validate! }.to raise_error Tenkit::TenkitError 12 | config.key_id = 'hij' 13 | expect { config.validate! }.to raise_error Tenkit::TenkitError 14 | config.key = '====PRIVATE KEY====' 15 | expect { config.validate! }.not_to raise_error 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /tenkit.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.push File.expand_path('lib', __dir__) 4 | require 'tenkit/version' 5 | 6 | Gem::Specification.new do |s| 7 | s.platform = Gem::Platform::RUBY 8 | s.name = 'tenkit' 9 | s.version = Tenkit::VERSION 10 | s.required_ruby_version = '>= 2.6.7' 11 | s.summary = 'Wrapper for WeatherKit API' 12 | s.description = 'Wrapper for Weatherkit API' 13 | s.author = 'James Pierce' 14 | s.email = 'james@superbasic.xyz' 15 | s.homepage = 'https://github.com/superbasicxyz/tenkit' 16 | s.license = 'MIT' 17 | s.files = Dir['lib/tenkit.rb', 'lib/**/*.rb'] 18 | 19 | s.add_dependency 'httparty', '~> 0.21.0' 20 | s.add_dependency 'jwt', '~> 2.7.0' 21 | 22 | s.add_development_dependency 'bundler', '~> 2.4.22' 23 | s.add_development_dependency 'dotenv', '~> 2.8' 24 | s.add_development_dependency 'rake', '~> 12.3' 25 | s.add_development_dependency 'rspec', '~> 3.0' 26 | s.add_development_dependency 'webmock', '~> 3.8' 27 | end 28 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | RSpec.describe Tenkit::Utils do 3 | subject { Tenkit::Utils } 4 | 5 | describe ".snake" do 6 | let(:simple) { "someCamelCaseString" } 7 | let(:correct) { "some_camel_case_string" } 8 | let(:complex) { "someAWSCredentials" } 9 | let(:mangled) { "some_aw_scredentials" } 10 | 11 | it "converts to snake case" do 12 | expect(subject.snake(simple)).to eq correct 13 | end 14 | 15 | context "when passed an acronym" do 16 | it "does a mediocre job converting string" do 17 | expect(subject.snake(complex)).to eq mangled 18 | end 19 | end 20 | 21 | context "when a proper underscore package is available" do 22 | let(:patched_string) { PatchedString.new complex } 23 | 24 | it "uses string underscore method" do 25 | expect(subject.snake(patched_string)).to eq :correct 26 | end 27 | end 28 | end 29 | end 30 | 31 | class PatchedString < String 32 | def underscore 33 | :correct 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: Automated Spec Runner (2.6 - 3.2) 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | ruby-version: ['2.6', '2.7', '3.0', '3.1', '3.2'] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Set up Ruby 22 | # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, 23 | # change this to (see https://github.com/ruby/setup-ruby#versioning): 24 | uses: ruby/setup-ruby@v1 25 | with: 26 | ruby-version: ${{ matrix.ruby-version }} 27 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 28 | - name: Run tests 29 | env: 30 | KID: ${{ secrets.KID }} 31 | SID: ${{ secrets.SID }} 32 | TID: ${{ secrets.TID }} 33 | AUTH_KEY: | 34 | ${{ secrets.AUTH_KEY }} 35 | run: bundle exec rake 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Super Basic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/fixtures/currentWeather.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentWeather": { 3 | "name": "CurrentWeather", 4 | "metadata": { 5 | "attributionURL": "https://developer.apple.com/weatherkit/data-source-attribution/", 6 | "expireTime": "2025-01-12T20:28:17Z", 7 | "latitude": 37.32, 8 | "longitude": 122.03, 9 | "readTime": "2025-01-12T20:23:17Z", 10 | "reportedTime": "2025-01-12T20:23:17Z", 11 | "units": "m", 12 | "version": 1, 13 | "sourceType": "modeled" 14 | }, 15 | "asOf": "2025-01-12T20:23:17Z", 16 | "cloudCover": 0.47, 17 | "cloudCoverLowAltPct": 0.02, 18 | "cloudCoverMidAltPct": 0.53, 19 | "cloudCoverHighAltPct": 0, 20 | "conditionCode": "PartlyCloudy", 21 | "daylight": false, 22 | "humidity": 0.78, 23 | "precipitationIntensity": 0, 24 | "pressure": 1024.97, 25 | "pressureTrend": "falling", 26 | "temperature": -5.68, 27 | "temperatureApparent": -6.88, 28 | "temperatureDewPoint": -8.91, 29 | "uvIndex": 0, 30 | "visibility": 23608.21, 31 | "windDirection": 183, 32 | "windGust": 13.51, 33 | "windSpeed": 6.82 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/tenkit/weather.rb: -------------------------------------------------------------------------------- 1 | require_relative './next_hour_forecast' 2 | require_relative './trend_comparison' 3 | require_relative './weather_alert_collection' 4 | 5 | module Tenkit 6 | class Weather 7 | attr_reader :current_weather, 8 | :forecast_daily, 9 | :forecast_hourly, 10 | :forecast_next_hour, 11 | :trend_comparison, 12 | :weather_alerts 13 | 14 | def initialize(response) 15 | parsed_response = JSON.parse(response.body) 16 | 17 | current_weather = parsed_response['currentWeather'] 18 | forecast_daily = parsed_response['forecastDaily'] 19 | forecast_hourly = parsed_response['forecastHourly'] 20 | forecast_next_hour = parsed_response['forecastNextHour'] 21 | trend_comparison = parsed_response['trendComparison'] 22 | weather_alerts = parsed_response['weatherAlerts'] 23 | 24 | @current_weather = CurrentWeather.new(current_weather) 25 | @forecast_daily = DailyForecast.new(forecast_daily) 26 | @forecast_hourly = HourlyForecast.new(forecast_hourly) 27 | @forecast_next_hour = NextHourForecast.new(forecast_next_hour) 28 | @trend_comparison = TrendComparison.new(trend_comparison) 29 | @weather_alerts = WeatherAlertCollection.new(weather_alerts) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit/container_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | RSpec.describe Tenkit::Container do 3 | let(:node) { {"someKey" => "someValue"} } 4 | 5 | subject { Tenkit::Container.new payload } 6 | 7 | describe ".new" do 8 | context "passed a hash" do 9 | let(:payload) { {"rootHash" => {"someArray" => [node, node], "someHash" => node}} } 10 | 11 | it "returns an object with converted attributes" do 12 | expect(subject.root_hash).to be_a Tenkit::Container 13 | expect(subject.instance_variables).to match [:@root_hash] 14 | expect(subject.root_hash.some_array.first).to be_a Tenkit::Container 15 | expect(subject.root_hash.some_array.first.some_key).to eq "someValue" 16 | expect(subject.root_hash.some_hash).to be_a Tenkit::Container 17 | expect(subject.root_hash.some_hash.some_key).to eq "someValue" 18 | end 19 | end 20 | 21 | context "passed an array" do 22 | let(:payload) { [node, node] } 23 | 24 | it "returns an empty object" do 25 | expect(subject).to be_a Tenkit::Container 26 | expect(subject.instance_variables).to match [] 27 | end 28 | end 29 | 30 | context "passed an nothing" do 31 | let(:payload) { nil } 32 | 33 | it "returns an empty object" do 34 | expect(subject).to be_a Tenkit::Container 35 | expect(subject.instance_variables).to match [] 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | tenkit (0.0.7) 5 | httparty (~> 0.21.0) 6 | jwt (~> 2.7.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | addressable (2.8.1) 12 | public_suffix (>= 2.0.2, < 6.0) 13 | crack (0.4.5) 14 | rexml 15 | diff-lcs (1.5.0) 16 | dotenv (2.8.1) 17 | hashdiff (1.0.1) 18 | httparty (0.21.0) 19 | mini_mime (>= 1.0.0) 20 | multi_xml (>= 0.5.2) 21 | jwt (2.7.0) 22 | mini_mime (1.1.2) 23 | multi_xml (0.6.0) 24 | public_suffix (5.0.1) 25 | rake (12.3.3) 26 | rexml (3.4.0) 27 | rspec (3.12.0) 28 | rspec-core (~> 3.12.0) 29 | rspec-expectations (~> 3.12.0) 30 | rspec-mocks (~> 3.12.0) 31 | rspec-core (3.12.1) 32 | rspec-support (~> 3.12.0) 33 | rspec-expectations (3.12.2) 34 | diff-lcs (>= 1.2.0, < 2.0) 35 | rspec-support (~> 3.12.0) 36 | rspec-mocks (3.12.3) 37 | diff-lcs (>= 1.2.0, < 2.0) 38 | rspec-support (~> 3.12.0) 39 | rspec-support (3.12.0) 40 | webmock (3.18.1) 41 | addressable (>= 2.8.0) 42 | crack (>= 0.3.2) 43 | hashdiff (>= 0.4.0, < 2.0.0) 44 | 45 | PLATFORMS 46 | arm64-darwin-20 47 | arm64-darwin-21 48 | arm64-darwin-22 49 | x86_64-linux 50 | 51 | DEPENDENCIES 52 | bundler (~> 2.4.22) 53 | dotenv (~> 2.8) 54 | rake (~> 12.3) 55 | rspec (~> 3.0) 56 | tenkit! 57 | webmock (~> 3.8) 58 | 59 | BUNDLED WITH 60 | 2.4.22 61 | -------------------------------------------------------------------------------- /lib/tenkit/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'jwt' 4 | require 'openssl' 5 | require 'httparty' 6 | require_relative './weather_response' 7 | require_relative './weather_alert_response' 8 | 9 | module Tenkit 10 | class Client 11 | include HTTParty 12 | base_uri 'https://weatherkit.apple.com/api/v1' 13 | 14 | DATA_SETS = { 15 | current_weather: 'currentWeather', 16 | forecast_daily: 'forecastDaily', 17 | forecast_hourly: 'forecastHourly', 18 | forecast_next_hour: 'forecastNextHour', 19 | trend_comparison: 'trendComparison', 20 | weather_alerts: 'weatherAlerts' 21 | }.freeze 22 | 23 | def initialize 24 | Tenkit.config.validate! 25 | end 26 | 27 | def availability(lat, lon, country: 'US') 28 | get("/availability/#{lat}/#{lon}?country=#{country}") 29 | end 30 | 31 | def weather(lat, lon, data_sets: [:current_weather], language: 'en') 32 | path_root = "/weather/#{language}/#{lat}/#{lon}?dataSets=" 33 | path = path_root + data_sets.map { |ds| DATA_SETS[ds] }.compact.join(',') 34 | response = get(path) 35 | WeatherResponse.new(response) 36 | end 37 | 38 | def weather_alert(id, language: 'en') 39 | path = "/weatherAlert/#{language}/#{id}" 40 | response = get(path) 41 | WeatherAlertResponse.new(response) 42 | end 43 | 44 | private 45 | 46 | def get(url) 47 | headers = { Authorization: "Bearer #{token}" } 48 | self.class.get(url, { headers: headers }) 49 | end 50 | 51 | def header 52 | { 53 | alg: 'ES256', 54 | kid: Tenkit.config.key_id, 55 | id: "#{Tenkit.config.team_id}.#{Tenkit.config.service_id}" 56 | } 57 | end 58 | 59 | def payload 60 | { 61 | iss: Tenkit.config.team_id, 62 | iat: Time.new.to_i, 63 | exp: Time.new.to_i + 600, 64 | sub: Tenkit.config.service_id 65 | } 66 | end 67 | 68 | def key 69 | OpenSSL::PKey.read Tenkit.config.key 70 | end 71 | 72 | def token 73 | JWT.encode payload, key, 'ES256', header 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /lib/tenkit/container.rb: -------------------------------------------------------------------------------- 1 | require_relative "utils" 2 | 3 | module Tenkit 4 | class Container 5 | def initialize(contents) 6 | return unless contents.is_a?(Hash) 7 | 8 | contents.each do |key, val| 9 | name = Tenkit::Utils.snake(key) 10 | singleton_class.class_eval { attr_accessor name } 11 | if val.is_a?(Array) 12 | val = if key == "days" 13 | val.map { |e| DayWeatherConditions.new(e) } 14 | elsif key == "hours" 15 | val.map { |e| HourWeatherConditions.new(e) } 16 | elsif key == "features" 17 | val.map { |e| Feature.new(e) } 18 | elsif key == "messages" 19 | val.map { |e| Message.new(e) } 20 | elsif key == "coordinates" 21 | Coordinates.new(val) 22 | else 23 | val.map { |e| Container.new(e) } 24 | end 25 | elsif val.is_a?(Hash) 26 | val = if key == "metadata" 27 | Metadata.new(val) 28 | elsif key == "daytimeForecast" 29 | DaytimeForecast.new(val) 30 | elsif key == "overnightForecast" 31 | OvernightForecast.new(val) 32 | elsif key == "restOfDayForecast" 33 | RestOfDayForecast.new(val) 34 | elsif key == "area" 35 | Area.new(val) 36 | elsif key == "geometry" 37 | Geometry.new(val) 38 | else 39 | Container.new(val) 40 | end 41 | end 42 | instance_variable_set(:"@#{name}", val) 43 | end 44 | end 45 | end 46 | 47 | class CurrentWeather < Container; end 48 | 49 | class HourlyForecast < Container; end 50 | 51 | class DailyForecast < Container; end 52 | 53 | class WeatherAlertSummary < Container; end 54 | 55 | class HourWeatherConditions < Container; end 56 | 57 | class Feature < Container; end 58 | 59 | class Message < Container; end 60 | 61 | class Coordinates < Array; end 62 | 63 | class DayWeatherConditions < Container; end 64 | 65 | class Metadata < Container; end 66 | 67 | class DaytimeForecast < Container; end 68 | 69 | class OvernightForecast < Container; end 70 | 71 | class RestOfDayForecast < Container; end 72 | 73 | class Area < Container; end 74 | 75 | class Geometry < Container; end 76 | end 77 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit/weather_alert_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | RSpec.describe Tenkit::WeatherAlert do 4 | let(:client) { Tenkit::Client.new } 5 | let(:json) { File.read("test/fixtures/alert.json") } 6 | let(:api_response) { double("WeatherAlertResponse", body: json) } 7 | 8 | before { allow(client).to receive(:get).and_return(api_response) } 9 | 10 | describe "weather_alert" do 11 | let(:alert_id) { "0828b382-f63c-4139-9f4f-91a05a4c7cdd" } 12 | 13 | subject { client.weather_alert(alert_id).weather_alert.summary } 14 | 15 | it "includes expected message" do 16 | expect(subject.messages.first).to be_a Tenkit::Message 17 | expect(subject.messages.first.language).to eq "en" 18 | expect(subject.messages.first.text).to start_with "...HEAT ADVISORY" 19 | expect(subject.messages.first.text).to end_with "outdoor activities." 20 | end 21 | 22 | it "includes expected area feature" do 23 | expect(subject.area).to be_a Tenkit::Area 24 | expect(subject.area.type).to eq "FeatureCollection" 25 | expect(subject.area.features.first).to be_a Tenkit::Feature 26 | expect(subject.area.features.first.type).to eq "Feature" 27 | expect(subject.area.features.first.geometry).to be_a Tenkit::Geometry 28 | expect(subject.area.features.first.geometry.type).to eq "Polygon" 29 | expect(subject.area.features.first.geometry.coordinates).to be_a Tenkit::Coordinates 30 | expect(subject.area.features.first.geometry.coordinates.size).to be 1 31 | expect(subject.area.features.first.geometry.coordinates.first.size).to be 177 32 | expect(subject.area.features.first.geometry.coordinates.first.first).to match [-122.4156, 38.8967] 33 | end 34 | 35 | it "includes expected summary data" do 36 | expect(subject.area_id).to eq "caz017" 37 | expect(subject.area_name).to eq "Southern Sacramento Valley" 38 | expect(subject.certainty).to eq "likely" 39 | expect(subject.country_code).to eq "US" 40 | expect(subject.description).to eq "Heat Advisory" 41 | expect(subject.details_url).to start_with "https://" 42 | expect(subject.effective_time).to eq "2022-08-20T08:54:00Z" 43 | expect(subject.event_end_time).to eq "2022-08-21T02:00:00Z" 44 | expect(subject.event_source).to eq "US" 45 | expect(subject.expire_time).to eq "2022-08-21T02:00:00Z" 46 | expect(subject.id).to eq "cbff5515-5ed0-518b-ae8b-bcfdd5844d41" 47 | expect(subject.importance).to eq "low" 48 | expect(subject.issued_time).to eq "2022-08-20T08:54:00Z" 49 | expect(subject.name).to eq "WeatherAlert" 50 | expect(subject.precedence).to be 0 51 | expect(subject.responses).to be_empty 52 | expect(subject.severity).to eq "minor" 53 | expect(subject.source).to eq "National Weather Service" 54 | expect(subject.urgency).to eq "expected" 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tenkit 天気ト 2 | 3 | ![Gem](https://img.shields.io/gem/v/tenkit) 4 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/superbasicxyz/tenkit/ruby.yml) 5 | 6 | A wrapper for Apple's WeatherKit API in Ruby 7 | 8 | A portmanteau of "tenki" the Japanese word for weather, and "kit", the end of WeatherKit. 9 | 10 | This project is in beta, some of its API may be unstable, or possibly non-functional (like WeatherKit itself). We recommend pinning the version in your Gemfile until we get to a stable release, at which point we will be using [Semantic Versioning](https://semver.org/). 11 | 12 | ## Installation 13 | 14 | Add to `Gemfile` 15 | 16 | ``` 17 | gem 'tenkit' 18 | ``` 19 | 20 | See Apple's instructions on how to [Get Started with Weatherkit](https://developer.apple.com/weatherkit/get-started/). Your WeatherKit API dashboard should be at this URL . 21 | 22 | 23 | 24 | ## Usage 25 | 26 | See [Credentials](#credentials) for information on how to obtain the configuration values below. 27 | ```ruby 28 | Tenkit.configure do |c| 29 | c.team_id = ENV["APPLE_DEVELOPER_TEAM_ID"] 30 | c.service_id = ENV["APPLE_DEVELOPER_SERVICE_ID"] 31 | c.key_id = ENV["APPLE_DEVELOPER_KEY_ID"] 32 | c.key = ENV["APPLE_DEVELOPER_PRIVATE_KEY"] 33 | end 34 | 35 | client = Tenkit::Client.new 36 | 37 | lat = '37.323' 38 | lon = '122.032' 39 | 40 | client.weather(lat, lon) 41 | ``` 42 | 43 | ## Credentials 44 | 45 | Once obtained, these values should be kept secret. 46 | 47 | For Ruby you might use ENV variables, or a .env file **not committed to git**. 48 | 49 | Rails projects will likely store these values encrypted in [custom credentials](https://guides.rubyonrails.org/security.html#custom-credentials), which can be commited to git. 50 | 51 | ### APPLE_DEVELOPER_TEAM_ID 52 | This is your Apple Team ID, taken from the top-right of your [developer account certificates page](https://developer.apple.com/account/resources/certificates/list): 53 | 54 | ![screenshot example of where to find your team ID](doc/team_id-crunch.png) 55 | 56 | ### APPLE_DEVELOPER_SERVICE_ID 57 | This is a Service ID that you’ll need to create: https://developer.apple.com/account/resources/identifiers/list/serviceId It will be in the same style as an App ID, as a reverse domain name, e.g. “com.example.fancypantsweather” 58 | 59 | ### APPLE_DEVELOPER_KEY_ID 60 | Generate a new key: https://developer.apple.com/account/resources/authkeys/add Give it a name (not important but descriptive to you) and make sure to check “WeatherKit” 61 | 62 | ![screenshot example of registering a new key](doc/register_key-crunch.png) 63 | 64 | Make sure you download the key and keep it safe for the next credential. Use the generated Key ID as the value for `APPLE DEVELOPER KEY ID`. 65 | 66 | ### APPLE_DEVELOPER_PRIVATE_KEY 67 | This is the freshly downloaded private key, a .p8 file. P8 is a format that presents the private key in plain text (.p12 is a binary format). You may have seen these before, they look something like this: 68 | 69 | ```text 70 | -----BEGIN PRIVATE KEY----- 71 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 72 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 73 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 74 | AAAAAAAA 75 | -----END PRIVATE KEY----- 76 | ``` 77 | 78 | ## About 79 | 80 | Maintained by: **[Super Basic](https://superbasic.xyz)** 81 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit/weather_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "../spec_helper" 2 | 3 | RSpec.describe Tenkit::Weather do 4 | let(:lat) { 37.32 } 5 | let(:lon) { 122.03 } 6 | let(:client) { Tenkit::Client.new } 7 | let(:api_response) { double("WeatherResponse", body: json) } 8 | let(:json) { File.read("test/fixtures/#{data_set}.json") } 9 | 10 | before { allow(client).to receive(:get).and_return(api_response) } 11 | 12 | describe "currentWeather" do 13 | let(:data_set) { "currentWeather" } 14 | 15 | subject { client.weather(lat, lon).weather.current_weather } 16 | 17 | it "includes expected metadata" do 18 | expect(subject.name).to eq "CurrentWeather" 19 | expect(subject.metadata.attribution_url).to start_with 'https://' 20 | expect(subject.metadata.latitude).to be 37.32 21 | expect(subject.metadata.longitude).to be 122.03 22 | end 23 | 24 | it "returns correct object types" do 25 | expect(subject).to be_a Tenkit::CurrentWeather 26 | expect(subject.metadata).to be_a Tenkit::Metadata 27 | end 28 | 29 | it "returns current weather data" do 30 | expect(subject.temperature).to be(-5.68) 31 | expect(subject.temperature_apparent).to be(-6.88) 32 | end 33 | end 34 | 35 | describe "forecastDaily" do 36 | let(:data_set) { "forecastDaily" } 37 | let(:first_day) { subject.days.first } 38 | 39 | subject { client.weather(lat, lon).weather.forecast_daily } 40 | 41 | it "returns 10 days of data" do 42 | expect(subject.days.size).to be 10 43 | end 44 | 45 | it "returns correct object types" do 46 | expect(subject).to be_a Tenkit::DailyForecast 47 | expect(first_day).to be_a Tenkit::DayWeatherConditions 48 | expect(first_day.daytime_forecast).to be_a Tenkit::DaytimeForecast 49 | expect(first_day.overnight_forecast).to be_a Tenkit::OvernightForecast 50 | expect(first_day.rest_of_day_forecast).to be_a Tenkit::RestOfDayForecast 51 | end 52 | 53 | it "excludes learn_more_url node" do 54 | expect(subject.respond_to? :learn_more_url).to be false 55 | end 56 | 57 | it "includes expected metadata" do 58 | expect(subject.name).to eq "DailyForecast" 59 | expect(subject.metadata.attribution_url).to start_with 'https://' 60 | expect(subject.metadata.latitude).to be 37.32 61 | expect(subject.metadata.longitude).to be 122.03 62 | end 63 | 64 | it "returns daily forecast data" do 65 | expect(first_day.condition_code).to eq "Clear" 66 | expect(first_day.max_uv_index).to be 2 67 | expect(first_day.temperature_max).to be 6.34 68 | expect(first_day.temperature_min).to be(-6.35) 69 | end 70 | 71 | it "returns daytime and overnight forecast data" do 72 | expect(first_day.daytime_forecast.condition_code).to eq "Clear" 73 | expect(first_day.daytime_forecast.temperature_max).to be 6.34 74 | expect(first_day.overnight_forecast.condition_code).to eq "Clear" 75 | expect(first_day.overnight_forecast.temperature_max).to be(-0.28) 76 | end 77 | end 78 | 79 | describe "forecastHourly" do 80 | let(:data_set) { "forecastHourly" } 81 | let(:first_hour) { subject.hours.first } 82 | 83 | subject { client.weather(lat, lon).weather.forecast_hourly } 84 | 85 | it "returns 25 hours of data" do 86 | expect(subject.hours.size).to be 25 87 | end 88 | 89 | it "includes expected metadata" do 90 | expect(subject.name).to eq "HourlyForecast" 91 | expect(subject.metadata.attribution_url).to start_with 'https://' 92 | expect(subject.metadata.latitude).to be 37.32 93 | expect(subject.metadata.longitude).to be 122.03 94 | end 95 | 96 | it "returns correct object types" do 97 | expect(subject).to be_a Tenkit::HourlyForecast 98 | expect(first_hour).to be_a Tenkit::HourWeatherConditions 99 | end 100 | 101 | it "returns hourly forecast data" do 102 | expect(first_hour.condition_code).to eq "MostlyClear" 103 | expect(first_hour.temperature).to be(-5.86) 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/tenkit/tenkit_spec.rb: -------------------------------------------------------------------------------- 1 | require_relative "spec_helper" 2 | 3 | RSpec.describe Tenkit do 4 | let(:api_url) { "https://weatherkit.apple.com/api/v1" } 5 | let(:data_sets) { Tenkit::Client::DATA_SETS } 6 | let(:tid) { ENV.fetch("TID", "9876543210") } 7 | let(:body) { "{}" } 8 | 9 | before { Tenkit.config.team_id = tid } 10 | 11 | context "when improperly configured" do 12 | let(:tid) { nil } 13 | 14 | describe "#initialize" do 15 | it "raises a TenkitError" do 16 | expect { Tenkit::Client.new }.to raise_error Tenkit::TenkitError 17 | end 18 | end 19 | end 20 | 21 | context "when properly configured" do 22 | let(:headers) { {Accept: "*/*", "Accept-Encoding": /gzip/, Authorization: /Bearer/, "User-Agent": "Ruby"} } 23 | let(:client) { Tenkit::Client.new } 24 | let(:lat) { 37.323 } 25 | let(:lon) { 122.032 } 26 | 27 | before do 28 | allow(client).to receive(:token) # Github build environment may have invalid ENV['AUTH_KEY'] 29 | stub_request(:get, url).with(headers: headers).to_return(status: 200, body: body, headers: {}) 30 | end 31 | 32 | describe "#availability" do 33 | let(:url) { "#{api_url}/availability/#{lat}/#{lon}?country=US" } 34 | let(:body) { data_sets.values.to_json } 35 | 36 | subject { client.availability(lat, lon).body } 37 | 38 | it "returns data sets available for specified location" do 39 | expect(subject).to eq(data_sets.values.to_s.delete(" ")) 40 | end 41 | end 42 | 43 | describe "#weather" do 44 | let(:url) { "#{api_url}/weather/en/#{lat}/#{lon}?dataSets=#{data_sets.values.join(",")}" } 45 | 46 | subject { client.weather(lat, lon, data_sets: data_sets.keys.map(&:to_sym)) } 47 | 48 | it "contains expected base objects" do 49 | expect(subject).to be_a(Tenkit::WeatherResponse) 50 | expect(subject.raw).to be_a(HTTParty::Response) 51 | expect(subject.weather).to be_a(Tenkit::Weather) 52 | expect(subject.weather.current_weather).to be_a(Tenkit::CurrentWeather) 53 | expect(subject.weather.forecast_daily).to be_a(Tenkit::DailyForecast) 54 | expect(subject.weather.forecast_hourly).to be_a(Tenkit::HourlyForecast) 55 | expect(subject.weather.forecast_next_hour).to be_a(Tenkit::NextHourForecast) 56 | expect(subject.weather.weather_alerts).to be_a(Tenkit::WeatherAlertCollection) 57 | end 58 | 59 | context "with CurrentWeather payload" do 60 | let(:body) { File.read("test/fixtures/currentWeather.json") } 61 | 62 | it "contains CurrentWeather payload objects" do 63 | expect(subject.weather.current_weather.name).to eq "CurrentWeather" 64 | expect(subject.weather.current_weather.metadata).to be_a(Tenkit::Metadata) 65 | end 66 | end 67 | 68 | context "with DailyForecast payload" do 69 | let(:body) { File.read("test/fixtures/forecastDaily.json") } 70 | 71 | it "contains DailyForecast payload objects" do 72 | expect(subject.weather.forecast_daily.name).to eq "DailyForecast" 73 | expect(subject.weather.forecast_daily.metadata).to be_a(Tenkit::Metadata) 74 | expect(subject.weather.forecast_daily.days.first).to be_a(Tenkit::DayWeatherConditions) 75 | end 76 | end 77 | 78 | context "with HourlyForecast payload" do 79 | let(:body) { File.read("test/fixtures/forecastHourly.json") } 80 | 81 | it "contains HourlyForecast payload objects" do 82 | expect(subject.weather.forecast_hourly.name).to eq "HourlyForecast" 83 | expect(subject.weather.forecast_hourly.metadata).to be_a(Tenkit::Metadata) 84 | expect(subject.weather.forecast_hourly.hours.first).to be_a(Tenkit::HourWeatherConditions) 85 | end 86 | end 87 | end 88 | 89 | describe "#weather_alert" do 90 | let(:alert_id) { "0828b382-f63c-4139-9f4f-91a05a4c7cdd" } 91 | let(:url) { "#{api_url}/weatherAlert/en/#{alert_id}" } 92 | 93 | subject { client.weather_alert(alert_id) } 94 | 95 | it "contains expected base objects" do 96 | expect(subject).to be_a(Tenkit::WeatherAlertResponse) 97 | expect(subject.raw).to be_a(HTTParty::Response) 98 | expect(subject.weather_alert).to be_a(Tenkit::WeatherAlert) 99 | expect(subject.weather_alert.summary).to be_a(Tenkit::WeatherAlertSummary) 100 | end 101 | 102 | context "with WeatherAlert payload" do 103 | let(:body) { File.read("test/fixtures/alert.json") } 104 | 105 | it "contains expected payload objects" do 106 | expect(subject.weather_alert.summary.name).to eq "WeatherAlert" 107 | expect(subject.weather_alert.summary.messages.first).to be_a Tenkit::Message 108 | expect(subject.weather_alert.summary.area).to be_a Tenkit::Area 109 | expect(subject.weather_alert.summary.area.features.first).to be_a Tenkit::Feature 110 | expect(subject.weather_alert.summary.area.features.first.geometry).to be_a Tenkit::Geometry 111 | end 112 | end 113 | end 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /test/fixtures/alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WeatherAlert", 3 | "id": "cbff5515-5ed0-518b-ae8b-bcfdd5844d41", 4 | "areaId": "caz017", 5 | "areaName": "Southern Sacramento Valley", 6 | "countryCode": "US", 7 | "description": "Heat Advisory", 8 | "effectiveTime": "2022-08-20T08:54:00Z", 9 | "expireTime": "2022-08-21T02:00:00Z", 10 | "issuedTime": "2022-08-20T08:54:00Z", 11 | "eventEndTime": "2022-08-21T02:00:00Z", 12 | "detailsUrl": "https://weatherkit.apple.com/alertDetails/index.html?ids=cbff5515-5ed0-518b-ae8b-bcfdd5844d41&lang=en-US&timezone=Europe/Copenhagen", 13 | "precedence": 0, 14 | "severity": "minor", 15 | "source": "National Weather Service", 16 | "eventSource": "US", 17 | "urgency": "expected", 18 | "certainty": "likely", 19 | "importance": "low", 20 | "responses": [], 21 | "area": { 22 | "features": [ 23 | { 24 | "geometry": { 25 | "type": "Polygon", 26 | "coordinates": [ 27 | [ 28 | [-122.4156, 38.8967], 29 | [-122.4042, 38.8783], 30 | [-122.3918, 38.8678], 31 | [-122.3956, 38.8621], 32 | [-122.3816, 38.8494], 33 | [-122.3761, 38.8488], 34 | [-122.3716, 38.8446], 35 | [-122.3501, 38.835], 36 | [-122.3438, 38.84], 37 | [-122.3348, 38.8435], 38 | [-122.3305, 38.8425], 39 | [-122.3236, 38.846], 40 | [-122.3162, 38.8387], 41 | [-122.3052, 38.8418], 42 | [-122.2979, 38.8385], 43 | [-122.2885, 38.8394], 44 | [-122.2817, 38.8246], 45 | [-122.2789, 38.822], 46 | [-122.2728, 38.8063], 47 | [-122.2627, 38.7933], 48 | [-122.253, 38.7722], 49 | [-122.25, 38.7534], 50 | [-122.231, 38.7327], 51 | [-122.2262, 38.7222], 52 | [-122.2284, 38.7206], 53 | [-122.2233, 38.7034], 54 | [-122.2242, 38.7], 55 | [-122.219, 38.6929], 56 | [-122.2083, 38.6948], 57 | [-122.2028, 38.6892], 58 | [-122.2005, 38.6821], 59 | [-122.2032, 38.6776], 60 | [-122.1983, 38.6694], 61 | [-122.1849, 38.657], 62 | [-122.1768, 38.6583], 63 | [-122.167, 38.6534], 64 | [-122.163, 38.6441], 65 | [-122.1657, 38.6414], 66 | [-122.1615, 38.6348], 67 | [-122.1703, 38.6302], 68 | [-122.168, 38.6157], 69 | [-122.1528, 38.6239], 70 | [-122.1372, 38.6044], 71 | [-122.1329, 38.5926], 72 | [-122.1293, 38.5873], 73 | [-122.1283, 38.5795], 74 | [-122.1214, 38.5609], 75 | [-122.1148, 38.5386], 76 | [-122.1146, 38.5277], 77 | [-122.1033, 38.5133], 78 | [-122.1063, 38.5084], 79 | [-122.1075, 38.4912], 80 | [-122.1206, 38.4539], 81 | [-122.1236, 38.4477], 82 | [-122.1282, 38.4288], 83 | [-122.1173, 38.4134], 84 | [-122.0875, 38.3813], 85 | [-122.0726, 38.357], 86 | [-122.0698, 38.3411 ], 87 | [-122.0615, 38.3279 ], 88 | [-121.694, 38.3275], 89 | [-121.6936, 38.3139], 90 | [-121.5907, 38.3124], 91 | [-121.5897, 38.322], 92 | [-121.5856, 38.3241], 93 | [-121.5823, 38.3313], 94 | [-121.576, 38.3274], 95 | [-121.5728, 38.33], 96 | [-121.5011, 38.286], 97 | [-121.4727, 38.2592], 98 | [-121.4625, 38.259], 99 | [-121.4508, 38.2541], 100 | [-121.4452, 38.2564], 101 | [-121.4293, 38.2531], 102 | [-121.4254, 38.2499], 103 | [-121.4256, 38.2436], 104 | [-121.4201, 38.237], 105 | [-121.3992, 38.2268], 106 | [-121.3905, 38.2317], 107 | [-121.3639, 38.2292], 108 | [-121.3573, 38.2308], 109 | [-121.3445, 38.2279], 110 | [-121.3114, 38.2351], 111 | [-121.305, 38.2348], 112 | [-121.292, 38.2389], 113 | [-121.2877, 38.2459], 114 | [-121.2682, 38.2523], 115 | [-121.2592, 38.2502], 116 | [-121.2493, 38.2438], 117 | [-121.2369, 38.2469], 118 | [-121.2213, 38.2439], 119 | [-121.2152, 38.2474], 120 | [-121.2025, 38.2479], 121 | [-121.1915, 38.2531], 122 | [-121.1809, 38.2526], 123 | [-121.1728, 38.2489], 124 | [-121.1645, 38.2514], 125 | [-121.1537, 38.2623], 126 | [-121.14, 38.2645], 127 | [-121.1295, 38.2739], 128 | [-121.1146, 38.2783], 129 | [-121.1013, 38.2849], 130 | [-121.0935, 38.2842], 131 | [-121.0792, 38.2874], 132 | [-121.0616, 38.2974], 133 | [-121.0532, 38.2942], 134 | [-121.05, 38.2872], 135 | [-121.0406, 38.2853], 136 | [-121.036, 38.2947], 137 | [-121.0267, 38.2977], 138 | [-121.0154, 38.319], 139 | [-120.9686, 38.3198], 140 | [-120.921, 38.3404], 141 | [-120.8569, 38.396], 142 | [-120.868, 38.4639], 143 | [-120.8752, 38.5205], 144 | [-120.8792, 38.5296], 145 | [-120.9046, 38.6018], 146 | [-120.9692, 38.5902], 147 | [-121.0423, 38.6903], 148 | [-121.0158, 38.7909], 149 | [-121.0431, 38.8099], 150 | [-121.0751, 38.8247], 151 | [-121.1765, 38.9609], 152 | [-121.2035, 39.0159], 153 | [-121.2057, 39.0124], 154 | [-121.2215, 39.0127], 155 | [-121.2303, 39.0247], 156 | [-121.244, 39.0232], 157 | [-121.2495, 39.0281], 158 | [-121.2648, 39.0301], 159 | [-121.2751, 39.0344], 160 | [-121.291, 39.0378], 161 | [-121.2932, 39.0448], 162 | [-121.3061, 39.053], 163 | [-121.3164, 39.0519], 164 | [-121.3236, 39.043], 165 | [-121.3291, 39.0445], 166 | [-121.337, 39.0386], 167 | [-121.3593, 39.0309], 168 | [-121.3618, 39.0339], 169 | [-121.3743, 39.0268], 170 | [-121.3759, 39.0237], 171 | [-121.3996, 39.0079], 172 | [-121.414, 39.0036], 173 | [-121.4154, 38.9978], 174 | [-121.4332, 38.9942], 175 | [-121.4428, 38.9942], 176 | [-121.4555, 39.0001], 177 | [-121.4739, 38.9925], 178 | [-121.4829, 38.9938], 179 | [-121.4904, 38.9914], 180 | [-121.4935, 38.9855], 181 | [-121.5079, 38.983], 182 | [-121.5246, 38.9716], 183 | [-121.5435, 38.9725], 184 | [-121.5535, 38.9538], 185 | [-121.5615, 38.9535], 186 | [-121.5718, 38.9382], 187 | [-121.5768, 38.9199], 188 | [-121.581, 38.9298], 189 | [-121.8361, 38.9301], 190 | [-121.8385, 38.925], 191 | [-121.8915, 38.925], 192 | [-122.0109, 38.9258], 193 | [-122.0173, 38.9261], 194 | [-122.1201, 38.9256], 195 | [-122.1555, 38.9258], 196 | [-122.2139, 38.9255], 197 | [-122.2467, 38.924], 198 | [-122.282, 38.9245], 199 | [-122.3377, 38.924], 200 | [-122.3409, 38.9255], 201 | [-122.4069, 38.9254], 202 | [-122.4116, 38.9141], 203 | [-122.4244, 38.9008], 204 | [-122.4156, 38.8967] 205 | ] 206 | ] 207 | }, 208 | "type": "Feature" 209 | } 210 | ], 211 | "type": "FeatureCollection" 212 | }, 213 | "messages": [ 214 | { 215 | "language": "en", 216 | "text": "...HEAT ADVISORY REMAINS IN EFFECT UNTIL 7 PM PDT THIS EVENING...\n* WHAT...Highs 100 to 110.\n* WHERE...Sacramento Valley, northern San Joaquin Valley, the\nadjacent foothills, and the eastern Delta. Includes the cities\nof Redding, Chico, Sacramento, Grass Valley, Stockton and\nModesto.\n* WHEN...Until 7 PM PDT this evening.\n* IMPACTS...Widespread moderate to high heat risk expected. Hot\ntemperatures will significantly increase the potential for\nheat- related illnesses, particularly for those working or\nparticipating in outdoor activities." 217 | } 218 | ] 219 | } 220 | -------------------------------------------------------------------------------- /test/fixtures/forecastHourly.json: -------------------------------------------------------------------------------- 1 | { 2 | "forecastHourly": { 3 | "name": "HourlyForecast", 4 | "metadata": { 5 | "attributionURL": "https://developer.apple.com/weatherkit/data-source-attribution/", 6 | "expireTime": "2025-01-12T21:25:58Z", 7 | "latitude": 37.32, 8 | "longitude": 122.03, 9 | "readTime": "2025-01-12T20:25:58Z", 10 | "reportedTime": "2025-01-12T19:00:36Z", 11 | "units": "m", 12 | "version": 1, 13 | "sourceType": "modeled" 14 | }, 15 | "hours": [ 16 | { 17 | "forecastStart": "2025-01-12T20:00:00Z", 18 | "cloudCover": 0.28, 19 | "conditionCode": "MostlyClear", 20 | "daylight": false, 21 | "humidity": 0.78, 22 | "precipitationAmount": 0, 23 | "precipitationIntensity": 0, 24 | "precipitationChance": 0, 25 | "precipitationType": "clear", 26 | "pressure": 1025.15, 27 | "pressureTrend": "falling", 28 | "snowfallIntensity": 0, 29 | "snowfallAmount": 0, 30 | "temperature": -5.86, 31 | "temperatureApparent": -7.23, 32 | "temperatureDewPoint": -9.08, 33 | "uvIndex": 0, 34 | "visibility": 23701, 35 | "windDirection": 183, 36 | "windGust": 13.72, 37 | "windSpeed": 6.72 38 | }, 39 | { 40 | "forecastStart": "2025-01-12T21:00:00Z", 41 | "cloudCover": 0.14, 42 | "conditionCode": "MostlyClear", 43 | "daylight": false, 44 | "humidity": 0.78, 45 | "precipitationAmount": 0, 46 | "precipitationIntensity": 0, 47 | "precipitationChance": 0, 48 | "precipitationType": "clear", 49 | "pressure": 1024.76, 50 | "pressureTrend": "falling", 51 | "snowfallIntensity": 0, 52 | "snowfallAmount": 0, 53 | "temperature": -5.46, 54 | "temperatureApparent": -7.09, 55 | "temperatureDewPoint": -8.69, 56 | "uvIndex": 0, 57 | "visibility": 23393, 58 | "windDirection": 186, 59 | "windGust": 12.86, 60 | "windSpeed": 6.89 61 | }, 62 | { 63 | "forecastStart": "2025-01-12T22:00:00Z", 64 | "cloudCover": 0, 65 | "conditionCode": "Clear", 66 | "daylight": false, 67 | "humidity": 0.81, 68 | "precipitationAmount": 0, 69 | "precipitationIntensity": 0, 70 | "precipitationChance": 0, 71 | "precipitationType": "clear", 72 | "pressure": 1024.72, 73 | "pressureTrend": "falling", 74 | "snowfallIntensity": 0, 75 | "snowfallAmount": 0, 76 | "temperature": -5.39, 77 | "temperatureApparent": -7.01, 78 | "temperatureDewPoint": -8.14, 79 | "uvIndex": 0, 80 | "visibility": 23306, 81 | "windDirection": 194, 82 | "windGust": 12.53, 83 | "windSpeed": 6.56 84 | }, 85 | { 86 | "forecastStart": "2025-01-12T23:00:00Z", 87 | "cloudCover": 0.01, 88 | "conditionCode": "Clear", 89 | "daylight": false, 90 | "humidity": 0.81, 91 | "precipitationAmount": 0, 92 | "precipitationIntensity": 0, 93 | "precipitationChance": 0, 94 | "precipitationType": "clear", 95 | "pressure": 1024.78, 96 | "pressureTrend": "steady", 97 | "snowfallIntensity": 0, 98 | "snowfallAmount": 0, 99 | "temperature": -4.76, 100 | "temperatureApparent": -6.47, 101 | "temperatureDewPoint": -7.52, 102 | "uvIndex": 0, 103 | "visibility": 22933, 104 | "windDirection": 207, 105 | "windGust": 14.29, 106 | "windSpeed": 6.76 107 | }, 108 | { 109 | "forecastStart": "2025-01-13T00:00:00Z", 110 | "cloudCover": 0.1, 111 | "conditionCode": "Clear", 112 | "daylight": true, 113 | "humidity": 0.82, 114 | "precipitationAmount": 0, 115 | "precipitationIntensity": 0, 116 | "precipitationChance": 0, 117 | "precipitationType": "clear", 118 | "pressure": 1024.95, 119 | "pressureTrend": "steady", 120 | "snowfallIntensity": 0, 121 | "snowfallAmount": 0, 122 | "temperature": -4.25, 123 | "temperatureApparent": -3.43, 124 | "temperatureDewPoint": -6.86, 125 | "uvIndex": 0, 126 | "visibility": 23288, 127 | "windDirection": 210, 128 | "windGust": 16.41, 129 | "windSpeed": 7.69 130 | }, 131 | { 132 | "forecastStart": "2025-01-13T01:00:00Z", 133 | "cloudCover": 0.02, 134 | "conditionCode": "Clear", 135 | "daylight": true, 136 | "humidity": 0.71, 137 | "precipitationAmount": 0, 138 | "precipitationIntensity": 0, 139 | "precipitationChance": 0, 140 | "precipitationType": "clear", 141 | "pressure": 1025.14, 142 | "pressureTrend": "steady", 143 | "snowfallIntensity": 0, 144 | "snowfallAmount": 0, 145 | "temperature": -1.33, 146 | "temperatureApparent": 0.16, 147 | "temperatureDewPoint": -5.91, 148 | "uvIndex": 1, 149 | "visibility": 23846, 150 | "windDirection": 216, 151 | "windGust": 24.44, 152 | "windSpeed": 9.26 153 | }, 154 | { 155 | "forecastStart": "2025-01-13T02:00:00Z", 156 | "cloudCover": 0.01, 157 | "conditionCode": "Clear", 158 | "daylight": true, 159 | "humidity": 0.63, 160 | "precipitationAmount": 0, 161 | "precipitationIntensity": 0, 162 | "precipitationChance": 0, 163 | "precipitationType": "clear", 164 | "pressure": 1025.06, 165 | "pressureTrend": "steady", 166 | "snowfallIntensity": 0, 167 | "snowfallAmount": 0, 168 | "temperature": 0.92, 169 | "temperatureApparent": 1.09, 170 | "temperatureDewPoint": -5.33, 171 | "uvIndex": 1, 172 | "visibility": 24045, 173 | "windDirection": 222, 174 | "windGust": 28.99, 175 | "windSpeed": 12.43 176 | }, 177 | { 178 | "forecastStart": "2025-01-13T03:00:00Z", 179 | "cloudCover": 0.04, 180 | "conditionCode": "Clear", 181 | "daylight": true, 182 | "humidity": 0.53, 183 | "precipitationAmount": 0, 184 | "precipitationIntensity": 0, 185 | "precipitationChance": 0, 186 | "precipitationType": "clear", 187 | "pressure": 1024.27, 188 | "pressureTrend": "falling", 189 | "snowfallIntensity": 0, 190 | "snowfallAmount": 0, 191 | "temperature": 3.63, 192 | "temperatureApparent": 2.08, 193 | "temperatureDewPoint": -5.07, 194 | "uvIndex": 2, 195 | "visibility": 24951, 196 | "windDirection": 225, 197 | "windGust": 32.98, 198 | "windSpeed": 16.04 199 | }, 200 | { 201 | "forecastStart": "2025-01-13T04:00:00Z", 202 | "cloudCover": 0.03, 203 | "conditionCode": "Clear", 204 | "daylight": true, 205 | "humidity": 0.5, 206 | "precipitationAmount": 0, 207 | "precipitationIntensity": 0, 208 | "precipitationChance": 0, 209 | "precipitationType": "clear", 210 | "pressure": 1023.15, 211 | "pressureTrend": "falling", 212 | "snowfallIntensity": 0, 213 | "snowfallAmount": 0, 214 | "temperature": 4.66, 215 | "temperatureApparent": 2.46, 216 | "temperatureDewPoint": -4.89, 217 | "uvIndex": 2, 218 | "visibility": 24818, 219 | "windDirection": 228, 220 | "windGust": 33.39, 221 | "windSpeed": 17.76 222 | }, 223 | { 224 | "forecastStart": "2025-01-13T05:00:00Z", 225 | "cloudCover": 0.01, 226 | "conditionCode": "Clear", 227 | "daylight": true, 228 | "humidity": 0.48, 229 | "precipitationAmount": 0, 230 | "precipitationIntensity": 0, 231 | "precipitationChance": 0, 232 | "precipitationType": "clear", 233 | "pressure": 1022.07, 234 | "pressureTrend": "falling", 235 | "snowfallIntensity": 0, 236 | "snowfallAmount": 0, 237 | "temperature": 5.45, 238 | "temperatureApparent": 3.56, 239 | "temperatureDewPoint": -4.7, 240 | "uvIndex": 2, 241 | "visibility": 24820, 242 | "windDirection": 227, 243 | "windGust": 32.02, 244 | "windSpeed": 17.25 245 | }, 246 | { 247 | "forecastStart": "2025-01-13T06:00:00Z", 248 | "cloudCover": 0, 249 | "conditionCode": "Clear", 250 | "daylight": true, 251 | "humidity": 0.47, 252 | "precipitationAmount": 0, 253 | "precipitationIntensity": 0, 254 | "precipitationChance": 0, 255 | "precipitationType": "clear", 256 | "pressure": 1021.39, 257 | "pressureTrend": "falling", 258 | "snowfallIntensity": 0, 259 | "snowfallAmount": 0, 260 | "temperature": 6.34, 261 | "temperatureApparent": 4.72, 262 | "temperatureDewPoint": -4.16, 263 | "uvIndex": 1, 264 | "visibility": 24818, 265 | "windDirection": 222, 266 | "windGust": 30.84, 267 | "windSpeed": 16.54 268 | }, 269 | { 270 | "forecastStart": "2025-01-13T07:00:00Z", 271 | "cloudCover": 0, 272 | "conditionCode": "Clear", 273 | "daylight": true, 274 | "humidity": 0.55, 275 | "precipitationAmount": 0, 276 | "precipitationIntensity": 0, 277 | "precipitationChance": 0, 278 | "precipitationType": "clear", 279 | "pressure": 1021.23, 280 | "pressureTrend": "falling", 281 | "snowfallIntensity": 0, 282 | "snowfallAmount": 0, 283 | "temperature": 5.23, 284 | "temperatureApparent": 3.75, 285 | "temperatureDewPoint": -3.08, 286 | "uvIndex": 1, 287 | "visibility": 23295, 288 | "windDirection": 218, 289 | "windGust": 28.14, 290 | "windSpeed": 15.04 291 | }, 292 | { 293 | "forecastStart": "2025-01-13T08:00:00Z", 294 | "cloudCover": 0, 295 | "conditionCode": "Clear", 296 | "daylight": true, 297 | "humidity": 0.64, 298 | "precipitationAmount": 0, 299 | "precipitationIntensity": 0, 300 | "precipitationChance": 0, 301 | "precipitationType": "clear", 302 | "pressure": 1021.39, 303 | "pressureTrend": "falling", 304 | "snowfallIntensity": 0, 305 | "snowfallAmount": 0, 306 | "temperature": 3.96, 307 | "temperatureApparent": 2.76, 308 | "temperatureDewPoint": -2.24, 309 | "uvIndex": 0, 310 | "visibility": 22390, 311 | "windDirection": 215, 312 | "windGust": 23.64, 313 | "windSpeed": 11.77 314 | }, 315 | { 316 | "forecastStart": "2025-01-13T09:00:00Z", 317 | "cloudCover": 0, 318 | "conditionCode": "Clear", 319 | "daylight": false, 320 | "humidity": 0.73, 321 | "precipitationAmount": 0, 322 | "precipitationIntensity": 0, 323 | "precipitationChance": 0, 324 | "precipitationType": "clear", 325 | "pressure": 1021.82, 326 | "pressureTrend": "steady", 327 | "snowfallIntensity": 0, 328 | "snowfallAmount": 0, 329 | "temperature": 2.08, 330 | "temperatureApparent": -0.89, 331 | "temperatureDewPoint": -2.26, 332 | "uvIndex": 0, 333 | "visibility": 21475, 334 | "windDirection": 215, 335 | "windGust": 19.95, 336 | "windSpeed": 9.19 337 | }, 338 | { 339 | "forecastStart": "2025-01-13T10:00:00Z", 340 | "cloudCover": 0, 341 | "conditionCode": "Clear", 342 | "daylight": false, 343 | "humidity": 0.83, 344 | "precipitationAmount": 0, 345 | "precipitationIntensity": 0, 346 | "precipitationChance": 0, 347 | "precipitationType": "clear", 348 | "pressure": 1022.27, 349 | "pressureTrend": "rising", 350 | "snowfallIntensity": 0, 351 | "snowfallAmount": 0, 352 | "temperature": 0.22, 353 | "temperatureApparent": -2.15, 354 | "temperatureDewPoint": -2.33, 355 | "uvIndex": 0, 356 | "visibility": 20817, 357 | "windDirection": 224, 358 | "windGust": 16.46, 359 | "windSpeed": 8.02 360 | }, 361 | { 362 | "forecastStart": "2025-01-13T11:00:00Z", 363 | "cloudCover": 0, 364 | "conditionCode": "Clear", 365 | "daylight": false, 366 | "humidity": 0.85, 367 | "precipitationAmount": 0, 368 | "precipitationIntensity": 0, 369 | "precipitationChance": 0, 370 | "precipitationType": "clear", 371 | "pressure": 1022.79, 372 | "pressureTrend": "rising", 373 | "snowfallIntensity": 0, 374 | "snowfallAmount": 0, 375 | "temperature": -0.59, 376 | "temperatureApparent": -2.7, 377 | "temperatureDewPoint": -2.8, 378 | "uvIndex": 0, 379 | "visibility": 20652, 380 | "windDirection": 263, 381 | "windGust": 13.21, 382 | "windSpeed": 7.49 383 | }, 384 | { 385 | "forecastStart": "2025-01-13T12:00:00Z", 386 | "cloudCover": 0, 387 | "conditionCode": "Clear", 388 | "daylight": false, 389 | "humidity": 0.86, 390 | "precipitationAmount": 0, 391 | "precipitationIntensity": 0, 392 | "precipitationChance": 0, 393 | "precipitationType": "clear", 394 | "pressure": 1023.26, 395 | "pressureTrend": "rising", 396 | "snowfallIntensity": 0, 397 | "snowfallAmount": 0, 398 | "temperature": -1.47, 399 | "temperatureApparent": -3.66, 400 | "temperatureDewPoint": -3.51, 401 | "uvIndex": 0, 402 | "visibility": 19594, 403 | "windDirection": 297, 404 | "windGust": 12.46, 405 | "windSpeed": 7.65 406 | }, 407 | { 408 | "forecastStart": "2025-01-13T13:00:00Z", 409 | "cloudCover": 0, 410 | "conditionCode": "Clear", 411 | "daylight": false, 412 | "humidity": 0.82, 413 | "precipitationAmount": 0, 414 | "precipitationIntensity": 0, 415 | "precipitationChance": 0, 416 | "precipitationType": "clear", 417 | "pressure": 1023.73, 418 | "pressureTrend": "rising", 419 | "snowfallIntensity": 0, 420 | "snowfallAmount": 0, 421 | "temperature": -1.64, 422 | "temperatureApparent": -4.37, 423 | "temperatureDewPoint": -4.31, 424 | "uvIndex": 0, 425 | "visibility": 20808, 426 | "windDirection": 302, 427 | "windGust": 16.16, 428 | "windSpeed": 8.76 429 | }, 430 | { 431 | "forecastStart": "2025-01-13T14:00:00Z", 432 | "cloudCover": 0, 433 | "conditionCode": "Clear", 434 | "daylight": false, 435 | "humidity": 0.78, 436 | "precipitationAmount": 0, 437 | "precipitationIntensity": 0, 438 | "precipitationChance": 0, 439 | "precipitationType": "clear", 440 | "pressure": 1024.28, 441 | "pressureTrend": "rising", 442 | "snowfallIntensity": 0, 443 | "snowfallAmount": 0, 444 | "temperature": -1.85, 445 | "temperatureApparent": -4.91, 446 | "temperatureDewPoint": -5.18, 447 | "uvIndex": 0, 448 | "visibility": 21887, 449 | "windDirection": 300, 450 | "windGust": 19.71, 451 | "windSpeed": 9.32 452 | }, 453 | { 454 | "forecastStart": "2025-01-13T15:00:00Z", 455 | "cloudCover": 0, 456 | "conditionCode": "Clear", 457 | "daylight": false, 458 | "humidity": 0.76, 459 | "precipitationAmount": 0, 460 | "precipitationIntensity": 0, 461 | "precipitationChance": 0, 462 | "precipitationType": "clear", 463 | "pressure": 1024.69, 464 | "pressureTrend": "rising", 465 | "snowfallIntensity": 0, 466 | "snowfallAmount": 0, 467 | "temperature": -1.75, 468 | "temperatureApparent": -5.29, 469 | "temperatureDewPoint": -5.42, 470 | "uvIndex": 0, 471 | "visibility": 22024, 472 | "windDirection": 300, 473 | "windGust": 21.47, 474 | "windSpeed": 10.26 475 | }, 476 | { 477 | "forecastStart": "2025-01-13T16:00:00Z", 478 | "cloudCover": 0.01, 479 | "conditionCode": "Clear", 480 | "daylight": false, 481 | "humidity": 0.78, 482 | "precipitationAmount": 0, 483 | "precipitationIntensity": 0, 484 | "precipitationChance": 0, 485 | "precipitationType": "clear", 486 | "pressure": 1024.93, 487 | "pressureTrend": "rising", 488 | "snowfallIntensity": 0, 489 | "snowfallAmount": 0, 490 | "temperature": -1.62, 491 | "temperatureApparent": -5.38, 492 | "temperatureDewPoint": -4.95, 493 | "uvIndex": 0, 494 | "visibility": 21027, 495 | "windDirection": 299, 496 | "windGust": 20.85, 497 | "windSpeed": 10.77 498 | }, 499 | { 500 | "forecastStart": "2025-01-13T17:00:00Z", 501 | "cloudCover": 0.05, 502 | "conditionCode": "Clear", 503 | "daylight": false, 504 | "humidity": 0.82, 505 | "precipitationAmount": 0, 506 | "precipitationIntensity": 0, 507 | "precipitationChance": 0, 508 | "precipitationType": "clear", 509 | "pressure": 1025.3, 510 | "pressureTrend": "rising", 511 | "snowfallIntensity": 0, 512 | "snowfallAmount": 0, 513 | "temperature": -1.37, 514 | "temperatureApparent": -4.89, 515 | "temperatureDewPoint": -4.05, 516 | "uvIndex": 0, 517 | "visibility": 19742, 518 | "windDirection": 299, 519 | "windGust": 20.19, 520 | "windSpeed": 10.47 521 | }, 522 | { 523 | "forecastStart": "2025-01-13T18:00:00Z", 524 | "cloudCover": 0.04, 525 | "conditionCode": "Clear", 526 | "daylight": false, 527 | "humidity": 0.79, 528 | "precipitationAmount": 0, 529 | "precipitationIntensity": 0, 530 | "precipitationChance": 0, 531 | "precipitationType": "clear", 532 | "pressure": 1025.5, 533 | "pressureTrend": "rising", 534 | "snowfallIntensity": 0, 535 | "snowfallAmount": 0, 536 | "temperature": -0.57, 537 | "temperatureApparent": -3.59, 538 | "temperatureDewPoint": -3.76, 539 | "uvIndex": 0, 540 | "visibility": 19041, 541 | "windDirection": 297, 542 | "windGust": 19.35, 543 | "windSpeed": 9.48 544 | }, 545 | { 546 | "forecastStart": "2025-01-13T19:00:00Z", 547 | "cloudCover": 0.08, 548 | "conditionCode": "Clear", 549 | "daylight": false, 550 | "humidity": 0.78, 551 | "precipitationAmount": 0, 552 | "precipitationIntensity": 0, 553 | "precipitationChance": 0, 554 | "precipitationType": "clear", 555 | "pressure": 1025.55, 556 | "pressureTrend": "steady", 557 | "snowfallIntensity": 0, 558 | "snowfallAmount": 0, 559 | "temperature": -0.32, 560 | "temperatureApparent": -2.91, 561 | "temperatureDewPoint": -3.69, 562 | "uvIndex": 0, 563 | "visibility": 19269, 564 | "windDirection": 294, 565 | "windGust": 18.63, 566 | "windSpeed": 8.71 567 | }, 568 | { 569 | "forecastStart": "2025-01-13T20:00:00Z", 570 | "cloudCover": 0.08, 571 | "conditionCode": "Clear", 572 | "daylight": false, 573 | "humidity": 0.77, 574 | "precipitationAmount": 0, 575 | "precipitationIntensity": 0, 576 | "precipitationChance": 0, 577 | "precipitationType": "clear", 578 | "pressure": 1025.62, 579 | "pressureTrend": "steady", 580 | "snowfallIntensity": 0, 581 | "snowfallAmount": 0, 582 | "temperature": -0.29, 583 | "temperatureApparent": -2.48, 584 | "temperatureDewPoint": -3.83, 585 | "uvIndex": 0, 586 | "visibility": 20053, 587 | "windDirection": 294, 588 | "windGust": 15.96, 589 | "windSpeed": 7.88 590 | } 591 | ] 592 | } 593 | } 594 | -------------------------------------------------------------------------------- /test/fixtures/forecastDaily.json: -------------------------------------------------------------------------------- 1 | { 2 | "forecastDaily": { 3 | "name": "DailyForecast", 4 | "metadata": { 5 | "attributionURL": "https://developer.apple.com/weatherkit/data-source-attribution/", 6 | "expireTime": "2025-01-12T21:25:22Z", 7 | "latitude": 37.32, 8 | "longitude": 122.03, 9 | "readTime": "2025-01-12T20:25:22Z", 10 | "reportedTime": "2025-01-12T19:00:36Z", 11 | "units": "m", 12 | "version": 1, 13 | "sourceType": "modeled" 14 | }, 15 | "days": [ 16 | { 17 | "forecastStart": "2025-01-12T16:00:00Z", 18 | "forecastEnd": "2025-01-13T16:00:00Z", 19 | "conditionCode": "Clear", 20 | "maxUvIndex": 2, 21 | "moonPhase": "waxingGibbous", 22 | "moonrise": "2025-01-13T07:58:53Z", 23 | "moonset": "2025-01-12T22:43:32Z", 24 | "precipitationAmount": 0, 25 | "precipitationChance": 0, 26 | "precipitationType": "clear", 27 | "snowfallAmount": 0, 28 | "solarMidnight": "2025-01-12T16:00:20Z", 29 | "solarNoon": "2025-01-13T04:00:41Z", 30 | "sunrise": "2025-01-12T23:05:34Z", 31 | "sunriseCivil": "2025-01-12T22:37:06Z", 32 | "sunriseNautical": "2025-01-12T22:04:51Z", 33 | "sunriseAstronomical": "2025-01-12T21:33:33Z", 34 | "sunset": "2025-01-13T08:55:54Z", 35 | "sunsetCivil": "2025-01-13T09:24:29Z", 36 | "sunsetNautical": "2025-01-13T09:56:35Z", 37 | "sunsetAstronomical": "2025-01-13T10:28:01Z", 38 | "temperatureMax": 6.34, 39 | "temperatureMin": -6.35, 40 | "windGustSpeedMax": 33.53, 41 | "windSpeedAvg": 9.95, 42 | "windSpeedMax": 17.81, 43 | "daytimeForecast": { 44 | "forecastStart": "2025-01-12T23:00:00Z", 45 | "forecastEnd": "2025-01-13T11:00:00Z", 46 | "cloudCover": 0.02, 47 | "conditionCode": "Clear", 48 | "humidity": 0.64, 49 | "precipitationAmount": 0, 50 | "precipitationChance": 0, 51 | "precipitationType": "clear", 52 | "snowfallAmount": 0, 53 | "temperatureMax": 6.34, 54 | "temperatureMin": -4.76, 55 | "windDirection": 222, 56 | "windGustSpeedMax": 33.53, 57 | "windSpeed": 12.35, 58 | "windSpeedMax": 17.81 59 | }, 60 | "overnightForecast": { 61 | "forecastStart": "2025-01-13T11:00:00Z", 62 | "forecastEnd": "2025-01-13T23:00:00Z", 63 | "cloudCover": 0.06, 64 | "conditionCode": "Clear", 65 | "humidity": 0.79, 66 | "precipitationAmount": 0, 67 | "precipitationChance": 0, 68 | "precipitationType": "clear", 69 | "snowfallAmount": 0, 70 | "temperatureMax": -0.28, 71 | "temperatureMin": -1.85, 72 | "windDirection": 297, 73 | "windGustSpeedMax": 21.51, 74 | "windSpeed": 8.82, 75 | "windSpeedMax": 10.78 76 | }, 77 | "restOfDayForecast": { 78 | "forecastStart": "2025-01-12T20:25:22Z", 79 | "forecastEnd": "2025-01-13T16:00:00Z", 80 | "cloudCover": 0.02, 81 | "conditionCode": "Clear", 82 | "humidity": 0.71, 83 | "precipitationAmount": 0, 84 | "precipitationChance": 0, 85 | "precipitationType": "clear", 86 | "snowfallAmount": 0, 87 | "temperatureMax": 6.34, 88 | "temperatureMin": -5.67, 89 | "windDirection": 233, 90 | "windGustSpeedMax": 33.53, 91 | "windSpeed": 10.76, 92 | "windSpeedMax": 17.81 93 | } 94 | }, 95 | { 96 | "forecastStart": "2025-01-13T16:00:00Z", 97 | "forecastEnd": "2025-01-14T16:00:00Z", 98 | "conditionCode": "PartlyCloudy", 99 | "maxUvIndex": 2, 100 | "moonPhase": "full", 101 | "moonrise": "2025-01-14T09:07:53Z", 102 | "moonset": "2025-01-13T23:32:17Z", 103 | "precipitationAmount": 0, 104 | "precipitationChance": 0, 105 | "precipitationType": "clear", 106 | "snowfallAmount": 0, 107 | "solarMidnight": "2025-01-13T16:00:43Z", 108 | "solarNoon": "2025-01-14T04:01:04Z", 109 | "sunrise": "2025-01-13T23:05:18Z", 110 | "sunriseCivil": "2025-01-13T22:36:53Z", 111 | "sunriseNautical": "2025-01-13T22:04:42Z", 112 | "sunriseAstronomical": "2025-01-13T21:33:25Z", 113 | "sunset": "2025-01-14T08:56:54Z", 114 | "sunsetCivil": "2025-01-14T09:25:27Z", 115 | "sunsetNautical": "2025-01-14T09:57:29Z", 116 | "sunsetAstronomical": "2025-01-14T10:28:53Z", 117 | "temperatureMax": 5.36, 118 | "temperatureMin": -1.62, 119 | "windGustSpeedMax": 41.62, 120 | "windSpeedAvg": 14.63, 121 | "windSpeedMax": 23.64, 122 | "daytimeForecast": { 123 | "forecastStart": "2025-01-13T23:00:00Z", 124 | "forecastEnd": "2025-01-14T11:00:00Z", 125 | "cloudCover": 0.61, 126 | "conditionCode": "PartlyCloudy", 127 | "humidity": 0.63, 128 | "precipitationAmount": 0, 129 | "precipitationChance": 0, 130 | "precipitationType": "clear", 131 | "snowfallAmount": 0, 132 | "temperatureMax": 5.36, 133 | "temperatureMin": -1.17, 134 | "windDirection": 317, 135 | "windGustSpeedMax": 39.46, 136 | "windSpeed": 16.29, 137 | "windSpeedMax": 22.03 138 | }, 139 | "overnightForecast": { 140 | "forecastStart": "2025-01-14T11:00:00Z", 141 | "forecastEnd": "2025-01-14T23:00:00Z", 142 | "cloudCover": 0.75, 143 | "conditionCode": "MostlyCloudy", 144 | "humidity": 0.61, 145 | "precipitationAmount": 0, 146 | "precipitationChance": 0, 147 | "precipitationType": "clear", 148 | "snowfallAmount": 0, 149 | "temperatureMax": 0, 150 | "temperatureMin": -3.57, 151 | "windDirection": 334, 152 | "windGustSpeedMax": 41.62, 153 | "windSpeed": 17.54, 154 | "windSpeedMax": 23.64 155 | } 156 | }, 157 | { 158 | "forecastStart": "2025-01-14T16:00:00Z", 159 | "forecastEnd": "2025-01-15T16:00:00Z", 160 | "conditionCode": "PartlyCloudy", 161 | "maxUvIndex": 2, 162 | "moonPhase": "waningGibbous", 163 | "moonrise": "2025-01-15T10:16:25Z", 164 | "moonset": "2025-01-15T00:11:23Z", 165 | "precipitationAmount": 0, 166 | "precipitationChance": 0, 167 | "precipitationType": "clear", 168 | "snowfallAmount": 0, 169 | "solarMidnight": "2025-01-14T16:01:04Z", 170 | "solarNoon": "2025-01-15T04:01:26Z", 171 | "sunrise": "2025-01-14T23:05:00Z", 172 | "sunriseCivil": "2025-01-14T22:36:39Z", 173 | "sunriseNautical": "2025-01-14T22:04:30Z", 174 | "sunriseAstronomical": "2025-01-14T21:33:15Z", 175 | "sunset": "2025-01-15T08:57:54Z", 176 | "sunsetCivil": "2025-01-15T09:26:25Z", 177 | "sunsetNautical": "2025-01-15T09:58:24Z", 178 | "sunsetAstronomical": "2025-01-15T10:29:46Z", 179 | "temperatureMax": 1.29, 180 | "temperatureMin": -4.44, 181 | "windGustSpeedMax": 43.01, 182 | "windSpeedAvg": 16.39, 183 | "windSpeedMax": 24.08, 184 | "daytimeForecast": { 185 | "forecastStart": "2025-01-14T23:00:00Z", 186 | "forecastEnd": "2025-01-15T11:00:00Z", 187 | "cloudCover": 0.39, 188 | "conditionCode": "PartlyCloudy", 189 | "humidity": 0.54, 190 | "precipitationAmount": 0, 191 | "precipitationChance": 0, 192 | "precipitationType": "clear", 193 | "snowfallAmount": 0, 194 | "temperatureMax": 1.29, 195 | "temperatureMin": -3.39, 196 | "windDirection": 305, 197 | "windGustSpeedMax": 43.01, 198 | "windSpeed": 19.18, 199 | "windSpeedMax": 24.08 200 | }, 201 | "overnightForecast": { 202 | "forecastStart": "2025-01-15T11:00:00Z", 203 | "forecastEnd": "2025-01-15T23:00:00Z", 204 | "cloudCover": 0, 205 | "conditionCode": "Clear", 206 | "humidity": 0.55, 207 | "precipitationAmount": 0, 208 | "precipitationChance": 0, 209 | "precipitationType": "clear", 210 | "snowfallAmount": 0, 211 | "temperatureMax": -2.97, 212 | "temperatureMin": -6.77, 213 | "windDirection": 236, 214 | "windGustSpeedMax": 22.62, 215 | "windSpeed": 8.75, 216 | "windSpeedMax": 13.36 217 | } 218 | }, 219 | { 220 | "forecastStart": "2025-01-15T16:00:00Z", 221 | "forecastEnd": "2025-01-16T16:00:00Z", 222 | "conditionCode": "Clear", 223 | "maxUvIndex": 2, 224 | "moonPhase": "waningGibbous", 225 | "moonrise": "2025-01-16T11:22:08Z", 226 | "moonset": "2025-01-16T00:43:11Z", 227 | "precipitationAmount": 0, 228 | "precipitationChance": 0, 229 | "precipitationType": "clear", 230 | "snowfallAmount": 0, 231 | "solarMidnight": "2025-01-15T16:01:25Z", 232 | "solarNoon": "2025-01-16T04:01:46Z", 233 | "sunrise": "2025-01-15T23:04:40Z", 234 | "sunriseCivil": "2025-01-15T22:36:23Z", 235 | "sunriseNautical": "2025-01-15T22:04:16Z", 236 | "sunriseAstronomical": "2025-01-15T21:33:04Z", 237 | "sunset": "2025-01-16T08:58:55Z", 238 | "sunsetCivil": "2025-01-16T09:27:23Z", 239 | "sunsetNautical": "2025-01-16T09:59:19Z", 240 | "sunsetAstronomical": "2025-01-16T10:30:40Z", 241 | "temperatureMax": 4.97, 242 | "temperatureMin": -6.77, 243 | "windGustSpeedMax": 37.5, 244 | "windSpeedAvg": 11.06, 245 | "windSpeedMax": 19.95, 246 | "daytimeForecast": { 247 | "forecastStart": "2025-01-15T23:00:00Z", 248 | "forecastEnd": "2025-01-16T11:00:00Z", 249 | "cloudCover": 0, 250 | "conditionCode": "Clear", 251 | "humidity": 0.45, 252 | "precipitationAmount": 0, 253 | "precipitationChance": 0, 254 | "precipitationType": "clear", 255 | "snowfallAmount": 0, 256 | "temperatureMax": 4.97, 257 | "temperatureMin": -6.63, 258 | "windDirection": 286, 259 | "windGustSpeedMax": 37.5, 260 | "windSpeed": 13.95, 261 | "windSpeedMax": 19.95 262 | }, 263 | "overnightForecast": { 264 | "forecastStart": "2025-01-16T11:00:00Z", 265 | "forecastEnd": "2025-01-16T23:00:00Z", 266 | "cloudCover": 0, 267 | "conditionCode": "Clear", 268 | "humidity": 0.83, 269 | "precipitationAmount": 0, 270 | "precipitationChance": 0, 271 | "precipitationType": "clear", 272 | "snowfallAmount": 0, 273 | "temperatureMax": 0.87, 274 | "temperatureMin": -3.78, 275 | "windDirection": 328, 276 | "windGustSpeedMax": 19.89, 277 | "windSpeed": 6.22, 278 | "windSpeedMax": 9.45 279 | } 280 | }, 281 | { 282 | "forecastStart": "2025-01-16T16:00:00Z", 283 | "forecastEnd": "2025-01-17T16:00:00Z", 284 | "conditionCode": "Clear", 285 | "maxUvIndex": 2, 286 | "moonPhase": "waningGibbous", 287 | "moonrise": "2025-01-17T12:24:33Z", 288 | "moonset": "2025-01-17T01:09:43Z", 289 | "precipitationAmount": 0, 290 | "precipitationChance": 0, 291 | "precipitationType": "clear", 292 | "snowfallAmount": 0, 293 | "solarMidnight": "2025-01-16T16:01:45Z", 294 | "solarNoon": "2025-01-17T04:02:07Z", 295 | "sunrise": "2025-01-16T23:04:19Z", 296 | "sunriseCivil": "2025-01-16T22:36:04Z", 297 | "sunriseNautical": "2025-01-16T22:04:01Z", 298 | "sunriseAstronomical": "2025-01-16T21:32:51Z", 299 | "sunset": "2025-01-17T08:59:57Z", 300 | "sunsetCivil": "2025-01-17T09:28:22Z", 301 | "sunsetNautical": "2025-01-17T10:00:15Z", 302 | "sunsetAstronomical": "2025-01-17T10:31:34Z", 303 | "temperatureMax": 5.72, 304 | "temperatureMin": -3.78, 305 | "windGustSpeedMax": 18.58, 306 | "windSpeedAvg": 5.69, 307 | "windSpeedMax": 9.3, 308 | "daytimeForecast": { 309 | "forecastStart": "2025-01-16T23:00:00Z", 310 | "forecastEnd": "2025-01-17T11:00:00Z", 311 | "cloudCover": 0, 312 | "conditionCode": "Clear", 313 | "humidity": 0.61, 314 | "precipitationAmount": 0, 315 | "precipitationChance": 0, 316 | "precipitationType": "clear", 317 | "snowfallAmount": 0, 318 | "temperatureMax": 5.72, 319 | "temperatureMin": -3.77, 320 | "windDirection": 208, 321 | "windGustSpeedMax": 14.72, 322 | "windSpeed": 4.91, 323 | "windSpeedMax": 7.96 324 | }, 325 | "overnightForecast": { 326 | "forecastStart": "2025-01-17T11:00:00Z", 327 | "forecastEnd": "2025-01-17T23:00:00Z", 328 | "cloudCover": 0.18, 329 | "conditionCode": "MostlyClear", 330 | "humidity": 0.8, 331 | "precipitationAmount": 0, 332 | "precipitationChance": 0, 333 | "precipitationType": "clear", 334 | "snowfallAmount": 0, 335 | "temperatureMax": 0.99, 336 | "temperatureMin": -2.63, 337 | "windDirection": 186, 338 | "windGustSpeedMax": 18.58, 339 | "windSpeed": 8.52, 340 | "windSpeedMax": 9.3 341 | } 342 | }, 343 | { 344 | "forecastStart": "2025-01-17T16:00:00Z", 345 | "forecastEnd": "2025-01-18T16:00:00Z", 346 | "conditionCode": "MostlyCloudy", 347 | "maxUvIndex": 2, 348 | "moonPhase": "waningGibbous", 349 | "moonrise": "2025-01-18T13:24:16Z", 350 | "moonset": "2025-01-18T01:33:04Z", 351 | "precipitationAmount": 0, 352 | "precipitationChance": 0, 353 | "precipitationType": "clear", 354 | "snowfallAmount": 0, 355 | "solarMidnight": "2025-01-17T16:02:05Z", 356 | "solarNoon": "2025-01-18T04:02:26Z", 357 | "sunrise": "2025-01-17T23:03:55Z", 358 | "sunriseCivil": "2025-01-17T22:35:44Z", 359 | "sunriseNautical": "2025-01-17T22:03:43Z", 360 | "sunriseAstronomical": "2025-01-17T21:32:36Z", 361 | "sunset": "2025-01-18T09:01:00Z", 362 | "sunsetCivil": "2025-01-18T09:29:22Z", 363 | "sunsetNautical": "2025-01-18T10:01:11Z", 364 | "sunsetAstronomical": "2025-01-18T10:32:28Z", 365 | "temperatureMax": 5.7, 366 | "temperatureMin": -2.69, 367 | "windGustSpeedMax": 31.06, 368 | "windSpeedAvg": 11.68, 369 | "windSpeedMax": 17.81, 370 | "daytimeForecast": { 371 | "forecastStart": "2025-01-17T23:00:00Z", 372 | "forecastEnd": "2025-01-18T11:00:00Z", 373 | "cloudCover": 0.75, 374 | "conditionCode": "MostlyCloudy", 375 | "humidity": 0.76, 376 | "precipitationAmount": 0, 377 | "precipitationChance": 0, 378 | "precipitationType": "clear", 379 | "snowfallAmount": 0, 380 | "temperatureMax": 5.7, 381 | "temperatureMin": -2.69, 382 | "windDirection": 197, 383 | "windGustSpeedMax": 31.06, 384 | "windSpeed": 13.99, 385 | "windSpeedMax": 17.81 386 | }, 387 | "overnightForecast": { 388 | "forecastStart": "2025-01-18T11:00:00Z", 389 | "forecastEnd": "2025-01-18T23:00:00Z", 390 | "cloudCover": 0.58, 391 | "conditionCode": "PartlyCloudy", 392 | "humidity": 0.9, 393 | "precipitationAmount": 0, 394 | "precipitationChance": 0, 395 | "precipitationType": "clear", 396 | "snowfallAmount": 0, 397 | "temperatureMax": 2.61, 398 | "temperatureMin": -1.41, 399 | "windDirection": 205, 400 | "windGustSpeedMax": 22.92, 401 | "windSpeed": 8.93, 402 | "windSpeedMax": 12.04 403 | } 404 | }, 405 | { 406 | "forecastStart": "2025-01-18T16:00:00Z", 407 | "forecastEnd": "2025-01-19T16:00:00Z", 408 | "conditionCode": "MostlyClear", 409 | "maxUvIndex": 2, 410 | "moonPhase": "waningGibbous", 411 | "moonrise": "2025-01-19T14:22:21Z", 412 | "moonset": "2025-01-19T01:54:34Z", 413 | "precipitationAmount": 0, 414 | "precipitationChance": 0, 415 | "precipitationType": "clear", 416 | "snowfallAmount": 0, 417 | "solarMidnight": "2025-01-18T16:02:24Z", 418 | "solarNoon": "2025-01-19T04:02:45Z", 419 | "sunrise": "2025-01-18T23:03:29Z", 420 | "sunriseCivil": "2025-01-18T22:35:22Z", 421 | "sunriseNautical": "2025-01-18T22:03:24Z", 422 | "sunriseAstronomical": "2025-01-18T21:32:19Z", 423 | "sunset": "2025-01-19T09:02:03Z", 424 | "sunsetCivil": "2025-01-19T09:30:22Z", 425 | "sunsetNautical": "2025-01-19T10:02:08Z", 426 | "sunsetAstronomical": "2025-01-19T10:33:23Z", 427 | "temperatureMax": 6.18, 428 | "temperatureMin": -1.42, 429 | "windGustSpeedMax": 28.28, 430 | "windSpeedAvg": 8.19, 431 | "windSpeedMax": 11.97, 432 | "daytimeForecast": { 433 | "forecastStart": "2025-01-18T23:00:00Z", 434 | "forecastEnd": "2025-01-19T11:00:00Z", 435 | "cloudCover": 0.23, 436 | "conditionCode": "MostlyClear", 437 | "humidity": 0.62, 438 | "precipitationAmount": 0, 439 | "precipitationChance": 0, 440 | "precipitationType": "clear", 441 | "snowfallAmount": 0, 442 | "temperatureMax": 6.18, 443 | "temperatureMin": -1.42, 444 | "windDirection": 321, 445 | "windGustSpeedMax": 28.28, 446 | "windSpeed": 8.78, 447 | "windSpeedMax": 11.97 448 | }, 449 | "overnightForecast": { 450 | "forecastStart": "2025-01-19T11:00:00Z", 451 | "forecastEnd": "2025-01-19T23:00:00Z", 452 | "cloudCover": 0.17, 453 | "conditionCode": "MostlyClear", 454 | "humidity": 0.89, 455 | "precipitationAmount": 0, 456 | "precipitationChance": 0, 457 | "precipitationType": "clear", 458 | "snowfallAmount": 0, 459 | "temperatureMax": 2.52, 460 | "temperatureMin": -2.04, 461 | "windDirection": 314, 462 | "windGustSpeedMax": 20.98, 463 | "windSpeed": 8.63, 464 | "windSpeedMax": 9.73 465 | } 466 | }, 467 | { 468 | "forecastStart": "2025-01-19T16:00:00Z", 469 | "forecastEnd": "2025-01-20T16:00:00Z", 470 | "conditionCode": "Clear", 471 | "maxUvIndex": 2, 472 | "moonPhase": "waningGibbous", 473 | "moonrise": "2025-01-20T15:20:05Z", 474 | "moonset": "2025-01-20T02:15:40Z", 475 | "precipitationAmount": 0, 476 | "precipitationChance": 0, 477 | "precipitationType": "clear", 478 | "snowfallAmount": 0, 479 | "solarMidnight": "2025-01-19T16:02:42Z", 480 | "solarNoon": "2025-01-20T04:03:03Z", 481 | "sunrise": "2025-01-19T23:03:02Z", 482 | "sunriseCivil": "2025-01-19T22:34:58Z", 483 | "sunriseNautical": "2025-01-19T22:03:03Z", 484 | "sunriseAstronomical": "2025-01-19T21:32:00Z", 485 | "sunset": "2025-01-20T09:03:08Z", 486 | "sunsetCivil": "2025-01-20T09:31:22Z", 487 | "sunsetNautical": "2025-01-20T10:03:06Z", 488 | "sunsetAstronomical": "2025-01-20T10:34:18Z", 489 | "temperatureMax": 5.18, 490 | "temperatureMin": -2.04, 491 | "windGustSpeedMax": 33.59, 492 | "windSpeedAvg": 13.31, 493 | "windSpeedMax": 17.4, 494 | "daytimeForecast": { 495 | "forecastStart": "2025-01-19T23:00:00Z", 496 | "forecastEnd": "2025-01-20T11:00:00Z", 497 | "cloudCover": 0.04, 498 | "conditionCode": "Clear", 499 | "humidity": 0.66, 500 | "precipitationAmount": 0, 501 | "precipitationChance": 0, 502 | "precipitationType": "clear", 503 | "snowfallAmount": 0, 504 | "temperatureMax": 5.18, 505 | "temperatureMin": -1.36, 506 | "windDirection": 332, 507 | "windGustSpeedMax": 33.59, 508 | "windSpeed": 14.91, 509 | "windSpeedMax": 17.4 510 | }, 511 | "overnightForecast": { 512 | "forecastStart": "2025-01-20T11:00:00Z", 513 | "forecastEnd": "2025-01-20T23:00:00Z", 514 | "cloudCover": 0.01, 515 | "conditionCode": "Clear", 516 | "humidity": 0.76, 517 | "precipitationAmount": 0, 518 | "precipitationChance": 0, 519 | "precipitationType": "clear", 520 | "snowfallAmount": 0, 521 | "temperatureMax": 0.43, 522 | "temperatureMin": -1.39, 523 | "windDirection": 331, 524 | "windGustSpeedMax": 31.17, 525 | "windSpeed": 14.89, 526 | "windSpeedMax": 15.64 527 | } 528 | }, 529 | { 530 | "forecastStart": "2025-01-20T16:00:00Z", 531 | "forecastEnd": "2025-01-21T16:00:00Z", 532 | "conditionCode": "MostlyClear", 533 | "maxUvIndex": 2, 534 | "moonPhase": "waningGibbous", 535 | "moonset": "2025-01-21T02:37:32Z", 536 | "precipitationAmount": 0, 537 | "precipitationChance": 0, 538 | "precipitationType": "clear", 539 | "snowfallAmount": 0, 540 | "solarMidnight": "2025-01-20T16:02:59Z", 541 | "solarNoon": "2025-01-21T04:03:21Z", 542 | "sunrise": "2025-01-20T23:02:32Z", 543 | "sunriseCivil": "2025-01-20T22:34:32Z", 544 | "sunriseNautical": "2025-01-20T22:02:40Z", 545 | "sunriseAstronomical": "2025-01-20T21:31:40Z", 546 | "sunset": "2025-01-21T09:04:12Z", 547 | "sunsetCivil": "2025-01-21T09:32:23Z", 548 | "sunsetNautical": "2025-01-21T10:04:04Z", 549 | "sunsetAstronomical": "2025-01-21T10:35:14Z", 550 | "temperatureMax": 1.14, 551 | "temperatureMin": -1.39, 552 | "windGustSpeedMax": 32.84, 553 | "windSpeedAvg": 16.39, 554 | "windSpeedMax": 21.92, 555 | "daytimeForecast": { 556 | "forecastStart": "2025-01-20T23:00:00Z", 557 | "forecastEnd": "2025-01-21T11:00:00Z", 558 | "cloudCover": 0.15, 559 | "conditionCode": "MostlyClear", 560 | "humidity": 0.63, 561 | "precipitationAmount": 0, 562 | "precipitationChance": 0, 563 | "precipitationType": "clear", 564 | "snowfallAmount": 0, 565 | "temperatureMax": 1.14, 566 | "temperatureMin": -0.58, 567 | "windDirection": 322, 568 | "windGustSpeedMax": 32.84, 569 | "windSpeed": 18.79, 570 | "windSpeedMax": 21.92 571 | }, 572 | "overnightForecast": { 573 | "forecastStart": "2025-01-21T11:00:00Z", 574 | "forecastEnd": "2025-01-21T23:00:00Z", 575 | "cloudCover": 0.11, 576 | "conditionCode": "Clear", 577 | "humidity": 0.72, 578 | "precipitationAmount": 0, 579 | "precipitationChance": 0, 580 | "precipitationType": "clear", 581 | "snowfallAmount": 0, 582 | "temperatureMax": 1.13, 583 | "temperatureMin": -1.13, 584 | "windDirection": 312, 585 | "windGustSpeedMax": 18.4, 586 | "windSpeed": 10.46, 587 | "windSpeedMax": 15.46 588 | } 589 | }, 590 | { 591 | "forecastStart": "2025-01-21T16:00:00Z", 592 | "forecastEnd": "2025-01-22T16:00:00Z", 593 | "conditionCode": "Clear", 594 | "maxUvIndex": 2, 595 | "moonPhase": "thirdQuarter", 596 | "moonrise": "2025-01-21T16:18:33Z", 597 | "moonset": "2025-01-22T03:01:18Z", 598 | "precipitationAmount": 0, 599 | "precipitationChance": 0, 600 | "precipitationType": "clear", 601 | "snowfallAmount": 0, 602 | "solarMidnight": "2025-01-21T16:03:16Z", 603 | "solarNoon": "2025-01-22T04:03:37Z", 604 | "sunrise": "2025-01-21T23:02:01Z", 605 | "sunriseCivil": "2025-01-21T22:34:04Z", 606 | "sunriseNautical": "2025-01-21T22:02:16Z", 607 | "sunriseAstronomical": "2025-01-21T21:31:17Z", 608 | "sunset": "2025-01-22T09:05:17Z", 609 | "sunsetCivil": "2025-01-22T09:33:24Z", 610 | "sunsetNautical": "2025-01-22T10:05:02Z", 611 | "sunsetAstronomical": "2025-01-22T10:36:09Z", 612 | "temperatureMax": 3.58, 613 | "temperatureMin": -2.72, 614 | "windGustSpeedMax": 25.44, 615 | "windSpeedAvg": 11.37, 616 | "windSpeedMax": 18.23, 617 | "daytimeForecast": { 618 | "forecastStart": "2025-01-21T23:00:00Z", 619 | "forecastEnd": "2025-01-22T11:00:00Z", 620 | "cloudCover": 0, 621 | "conditionCode": "Clear", 622 | "humidity": 0.69, 623 | "precipitationAmount": 0, 624 | "precipitationChance": 0, 625 | "precipitationType": "clear", 626 | "snowfallAmount": 0, 627 | "temperatureMax": 3.58, 628 | "temperatureMin": -1.13, 629 | "windDirection": 314, 630 | "windGustSpeedMax": 25.44, 631 | "windSpeed": 13.15, 632 | "windSpeedMax": 18.23 633 | }, 634 | "overnightForecast": { 635 | "forecastStart": "2025-01-22T11:00:00Z", 636 | "forecastEnd": "2025-01-22T23:00:00Z", 637 | "cloudCover": 0, 638 | "conditionCode": "Clear", 639 | "humidity": 0.68, 640 | "precipitationAmount": 0, 641 | "precipitationChance": 0, 642 | "precipitationType": "clear", 643 | "snowfallAmount": 0, 644 | "temperatureMax": 3.13, 645 | "temperatureMin": -3.31, 646 | "windDirection": 342, 647 | "windGustSpeedMax": 24.58, 648 | "windSpeed": 10.76, 649 | "windSpeedMax": 11.7 650 | } 651 | } 652 | ] 653 | } 654 | } 655 | --------------------------------------------------------------------------------