Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package com.climaai.wear.data

import android.util.Log
import com.climaai.wear.data.api.NominatimApi
import com.climaai.wear.data.api.OpenMeteoApi
import com.climaai.wear.data.api.WeatherCodeMapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

data class WearWeatherData(
val temperature: Int,
val condition: String,
Expand All @@ -13,20 +20,74 @@ data class WearWeatherData(

/**
* Repository for fetching weather data on Wear OS.
* In production, syncs with phone app via Wearable Data Layer API.
* Fetches directly from Open-Meteo API (standalone mode).
*/
object WearWeatherRepository {

private const val TAG = "WearWeatherRepository"

// Default location: San Francisco
private const val DEFAULT_LAT = 37.7749
private const val DEFAULT_LON = -122.4194
private const val DEFAULT_LOCATION_NAME = "San Francisco"

private val weatherApi = OpenMeteoApi.create()
private val locationApi = NominatimApi.create()

// Cached weather data
private var cachedWeather: WearWeatherData? = null

suspend fun getWeather(): WearWeatherData {
// In production:
// 1. Try to get from phone via DataClient
// 2. Fall back to direct API call
// 3. Fall back to cached data
suspend fun getWeather(
lat: Double = DEFAULT_LAT,
lon: Double = DEFAULT_LON
): WearWeatherData = withContext(Dispatchers.IO) {
try {
// 1. Fetch Weather
val weatherResponse = weatherApi.getWeather(lat, lon)

if (weatherResponse.isSuccessful && weatherResponse.body() != null) {
val data = weatherResponse.body()!!
val current = data.current
val daily = data.daily

if (current != null) {
// 2. Fetch Location Name (optional, use default if fails)
var locationName = DEFAULT_LOCATION_NAME
// Only fetch location name if we are not using default coordinates or if we want to be accurate
try {
val locationResponse = locationApi.reverseGeocode(lat, lon)
if (locationResponse.isSuccessful && locationResponse.body() != null) {
val locationData = locationResponse.body()!!
locationData.address?.getLocationName()?.let {
locationName = it
}
}
} catch (e: Exception) {
Log.w(TAG, "Reverse geocoding failed", e)
}

// Map to WearWeatherData
val newWeather = WearWeatherData(
temperature = current.temperature.toInt(),
condition = WeatherCodeMapper.getDescription(current.weatherCode),
conditionIcon = WeatherCodeMapper.getIcon(current.weatherCode, current.isDay == 1),
high = daily?.tempMax?.firstOrNull()?.toInt() ?: current.temperature.toInt(),
low = daily?.tempMin?.firstOrNull()?.toInt() ?: current.temperature.toInt(),
humidity = current.humidity,
windSpeed = current.windSpeed.toInt(),
location = locationName
)

cachedWeather = newWeather
return@withContext newWeather
}
}
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch weather", e)
}

return cachedWeather ?: WearWeatherData(
// Return cached or default if fetch fails
return@withContext cachedWeather ?: WearWeatherData(
temperature = 72,
condition = "Partly Cloudy",
conditionIcon = "⛅",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.climaai.wear.data.api

import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Headers
import retrofit2.http.Query

/**
* Open-Meteo API - Free weather data.
*/
interface OpenMeteoApi {

@Headers("User-Agent: ClimaAI-WearOS/1.0")
@GET("v1/forecast")
suspend fun getWeather(
@Query("latitude") latitude: Double,
@Query("longitude") longitude: Double,
@Query("current") current: String = CURRENT_PARAMS,
@Query("daily") daily: String = DAILY_PARAMS,
@Query("timezone") timezone: String = "auto",
@Query("forecast_days") forecastDays: Int = 1
): Response<OpenMeteoWeatherResponse>

companion object {
const val BASE_URL = "https://api.open-meteo.com/"

const val CURRENT_PARAMS = "temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,is_day"

const val DAILY_PARAMS = "temperature_2m_max,temperature_2m_min"

fun create(): OpenMeteoApi {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(OpenMeteoApi::class.java)
}
}
}

/**
* Nominatim Geocoding API - Free location search.
*/
interface NominatimApi {

@Headers("User-Agent: ClimaAI-WearOS/1.0")
@GET("reverse")
suspend fun reverseGeocode(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double,
@Query("format") format: String = "json",
@Query("addressdetails") addressDetails: Int = 1
): Response<NominatimResult>

companion object {
const val BASE_URL = "https://nominatim.openstreetmap.org/"

fun create(): NominatimApi {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(NominatimApi::class.java)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.climaai.wear.data.api

import com.google.gson.annotations.SerializedName

// ============================================================
// Open-Meteo Weather Response Models
// ============================================================

data class OpenMeteoWeatherResponse(
val timezone: String,
val current: OpenMeteoCurrentWeather?,
val daily: OpenMeteoDaily?
)

data class OpenMeteoCurrentWeather(
@SerializedName("temperature_2m") val temperature: Double,
@SerializedName("relative_humidity_2m") val humidity: Int,
@SerializedName("weather_code") val weatherCode: Int,
@SerializedName("wind_speed_10m") val windSpeed: Double,
@SerializedName("is_day") val isDay: Int
)

data class OpenMeteoDaily(
@SerializedName("temperature_2m_max") val tempMax: List<Double>,
@SerializedName("temperature_2m_min") val tempMin: List<Double>
)

// ============================================================
// Nominatim Geocoding Models
// ============================================================

data class NominatimResult(
@SerializedName("display_name") val displayName: String,
val address: NominatimAddress?
)

data class NominatimAddress(
val city: String?,
val town: String?,
val village: String?,
val county: String?,
val state: String?
) {
fun getLocationName(): String {
return city ?: town ?: village ?: county ?: state ?: "Unknown"
}
}

// ============================================================
// Weather Code Mapping (WMO)
// ============================================================

object WeatherCodeMapper {

fun getDescription(code: Int): String = when (code) {
0 -> "Clear sky"
1 -> "Mainly clear"
2 -> "Partly cloudy"
3 -> "Overcast"
45, 48 -> "Foggy"
51, 53, 55 -> "Drizzle"
56, 57 -> "Freezing drizzle"
61, 63, 65 -> "Rain"
66, 67 -> "Freezing rain"
71, 73, 75 -> "Snow"
77 -> "Snow grains"
80, 81, 82 -> "Rain showers"
85, 86 -> "Snow showers"
95 -> "Thunderstorm"
96, 99 -> "Thunderstorm with hail"
else -> "Unknown"
}

fun getIcon(code: Int, isDay: Boolean = true): String = when (code) {
0 -> if (isDay) "☀️" else "🌙"
1, 2 -> if (isDay) "⛅" else "☁️"
3 -> "☁️"
45, 48 -> "🌫️"
51, 53, 55, 61, 63, 65 -> "🌧️"
56, 57, 66, 67 -> "🌨️"
71, 73, 75, 77, 85, 86 -> "❄️"
80, 81, 82 -> "🌦️"
95, 96, 99 -> "⛈️"
else -> "🌤️"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,8 @@ fun WeatherScreen(

// Fetch weather on launch
LaunchedEffect(Unit) {
// Demo data - in production, fetch from repository
weather = WearWeatherData(
temperature = 72,
condition = "Partly Cloudy",
conditionIcon = "⛅",
high = 78,
low = 65,
humidity = 45,
windSpeed = 8,
location = "San Francisco"
)
// Fetch real data from repository
weather = WearWeatherRepository.getWeather()
isLoading = false
}

Expand Down