Files
jarvis/Jervis/Notifications/WeatherService.swift
Kutesir bd632a1592
Some checks are pending
CI / Build · Debug (push) Waiting to run
CI / Build · Release (push) Waiting to run
rename: Jarvis -> Jervis target, scheme, and folder throughout
The bundle-ID and display-name renames weren't enough — the actual
Xcode target/product name was still "Jarvis", which drives CFBundleName,
Xcode's Organizer archive list, the .xcodeproj filename, and the scheme
name. Renamed all the way through:

- project.yml: top-level name, target key, PRODUCT_NAME, source/info paths
- Jarvis/ -> Jervis/ (source folder, git-tracked as renames, no content
  diffs on the moved files)
- .gitignore, CI workflows, README, CONTRIBUTING: Jarvis.xcodeproj /
  scheme Jarvis -> Jervis.xcodeproj / scheme Jervis

Verified: xcodebuild -scheme Jervis succeeds, produces Jervis.app,
CFBundleName/CFBundleExecutable/CFBundleDisplayName all read "Jervis".
CFBundleIdentifier intentionally stays com.kisani.jarvis (per prior
decision — bundle ID isn't user-facing anywhere including Organizer).

Swift type names (JarvisApp, JarvisWordmark, etc.) and the Gitea repo
name/URL are unchanged — pure internal source identifiers, not user or
developer-facing product identity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 03:49:31 +03:00

94 lines
4.0 KiB
Swift
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WeatherService.swift
// Jarvis weather via Open-Meteo (free, no API key, no entitlement). Used to put
// a "will it rain today" line in the morning/evening briefing.
import Foundation
struct DailyWeather {
let place: String
let tempMax: Double
let tempMin: Double
let precipProbability: Int // %
let code: Int // WMO weather code
var willRain: Bool { precipProbability >= 40 }
/// e.g. "Kampala 26°/18°, light rain · 70% take an umbrella "
var summary: String {
let temps = "\(Int(tempMax.rounded()))°/\(Int(tempMin.rounded()))°"
let cond = WeatherService.codeText(code)
let rain = "\(precipProbability)% rain"
let tail = willRain ? " — take an umbrella ☔️" : ""
return "\(place) \(temps), \(cond) · \(rain)\(tail)"
}
}
enum WeatherService {
struct GeoResult: Decodable { let latitude: Double; let longitude: Double; let name: String; let country: String? }
private struct GeoResponse: Decodable { let results: [GeoResult]? }
private struct ForecastResponse: Decodable {
struct Daily: Decodable {
let temperature_2m_max: [Double]
let temperature_2m_min: [Double]
let precipitation_probability_max: [Int?]
let weathercode: [Int]
}
let daily: Daily
}
private static let session = URLSession(configuration: .default)
/// Geocode a free-text place to coordinates.
static func geocode(_ query: String) async -> GeoResult? {
let q = query.trimmingCharacters(in: .whitespaces)
guard !q.isEmpty,
var c = URLComponents(string: "https://geocoding-api.open-meteo.com/v1/search")
else { return nil }
c.queryItems = [.init(name: "name", value: q), .init(name: "count", value: "1"),
.init(name: "language", value: "en"), .init(name: "format", value: "json")]
guard let url = c.url else { return nil }
do {
let (data, _) = try await session.data(from: url)
return try JSONDecoder().decode(GeoResponse.self, from: data).results?.first
} catch { return nil }
}
/// Today's forecast for coordinates.
static func forecast(latitude: Double, longitude: Double, place: String) async -> DailyWeather? {
guard var c = URLComponents(string: "https://api.open-meteo.com/v1/forecast") else { return nil }
c.queryItems = [
.init(name: "latitude", value: "\(latitude)"),
.init(name: "longitude", value: "\(longitude)"),
.init(name: "daily", value: "temperature_2m_max,temperature_2m_min,precipitation_probability_max,weathercode"),
.init(name: "timezone", value: "auto"),
.init(name: "forecast_days", value: "1"),
]
guard let url = c.url else { return nil }
do {
let (data, _) = try await session.data(from: url)
let r = try JSONDecoder().decode(ForecastResponse.self, from: data)
guard let tmax = r.daily.temperature_2m_max.first,
let tmin = r.daily.temperature_2m_min.first,
let code = r.daily.weathercode.first else { return nil }
let prob = r.daily.precipitation_probability_max.first.flatMap { $0 } ?? 0
return DailyWeather(place: place, tempMax: tmax, tempMin: tmin, precipProbability: prob, code: code)
} catch { return nil }
}
/// Coarse WMO weather-code words.
static func codeText(_ code: Int) -> String {
switch code {
case 0: return "clear"
case 1, 2: return "partly cloudy"
case 3: return "overcast"
case 45, 48: return "fog"
case 51, 53, 55, 56, 57: return "drizzle"
case 61, 63, 65, 66, 67: return "rain"
case 71, 73, 75, 77: return "snow"
case 80, 81, 82: return "rain showers"
case 95, 96, 99: return "thunderstorms"
default: return "mixed"
}
}
}