Skip to content
Open
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,13 @@
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
import kotlin.math.roundToInt

data class WearWeatherData(
val temperature: Int,
val condition: String,
Expand All @@ -17,18 +25,71 @@ data class WearWeatherData(
*/
object WearWeatherRepository {

private const val TAG = "WearWeatherRepository"

// API instances
private val weatherApi by lazy { OpenMeteoApi.create() }
private val nominatimApi by lazy { NominatimApi.create() }

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

suspend fun getWeather(): WearWeatherData {
suspend fun getWeather(lat: Double = 37.7749, lon: Double = -122.4194): WearWeatherData = withContext(Dispatchers.IO) {
// In production:
// 1. Try to get from phone via DataClient
// 1. Try to get from phone via DataClient (skipped for this task)
// 2. Fall back to direct API call
// 3. Fall back to cached data

return cachedWeather ?: WearWeatherData(
try {
// Fetch weather
val weatherResponse = weatherApi.getWeather(lat, lon)

if (weatherResponse.isSuccessful && weatherResponse.body() != null) {
val data = weatherResponse.body()!!
val current = data.current ?: throw Exception("No current weather data")

// Fetch location name (optional, best effort)
val locationName = try {
val locationResponse = nominatimApi.reverseGeocode(lat, lon)
if (locationResponse.isSuccessful && locationResponse.body() != null) {
locationResponse.body()!!.address?.getLocationName() ?: "Unknown"
} else {
"Unknown Location"
}
} catch (e: Exception) {
Log.w(TAG, "Failed to fetch location name", e)
"Unknown Location"
}

// Extract daily high/low (assuming first element is today)
val daily = data.daily
val high = daily?.tempMax?.firstOrNull() ?: current.temperature
val low = daily?.tempMin?.firstOrNull() ?: current.temperature

val newData = WearWeatherData(
temperature = current.temperature.roundToInt(),
condition = WeatherCodeMapper.getDescription(current.weatherCode),
conditionIcon = WeatherCodeMapper.getIcon(current.weatherCode, current.isDay == 1),
high = high.roundToInt(),
low = low.roundToInt(),
humidity = current.humidity,
windSpeed = current.windSpeed.roundToInt(),
location = if (locationName != "Unknown Location" && locationName != "Unknown") locationName else "Lat: %.2f, Lon: %.2f".format(lat, lon)
)

cachedWeather = newData
return@withContext newData
} else {
Log.e(TAG, "Weather API error: ${weatherResponse.code()}")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch weather", e)
}

// Return cached or default on error
return@withContext cachedWeather ?: WearWeatherData(
temperature = 72,
condition = "Partly Cloudy",
condition = "Partly Cloudy (Demo)",
conditionIcon = "⛅",
high = 78,
low = 65,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.climaai.wear.data.api

import okhttp3.OkHttpClient
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Query

/**
* Open-Meteo API - Free weather data, no API key required.
* https://open-meteo.com/
*/
interface OpenMeteoApi {

@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"
): Response<OpenMeteoWeatherResponse>

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

const val CURRENT_PARAMS = "temperature_2m,relative_humidity_2m,apparent_temperature," +
"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, no API key.
* https://nominatim.openstreetmap.org/
*/
interface NominatimApi {

@GET("reverse")
suspend fun reverseGeocode(
@Query("lat") latitude: Double,
@Query("lon") longitude: Double,
@Query("format") format: String = "json",
@Query("addressdetails") addressDetails: Int = 1,
@Query("zoom") zoom: Int = 10
): Response<NominatimResult>

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

fun create(): NominatimApi {
val client = OkHttpClient.Builder()
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "ClimaAI-WearOS/1.0")
.build()
chain.proceed(request)
}
.build()

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

import com.google.gson.annotations.SerializedName

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

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

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

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

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

data class NominatimResult(
@SerializedName("place_id") val placeId: Long,
val lat: String,
val lon: String,
@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?,
val country: String?,
@SerializedName("country_code") val countryCode: String?
) {
fun getLocationName(): String {
return city ?: town ?: village ?: county ?: state ?: "Unknown"
}
}

// ============================================================
// Weather Code Mapping
// ============================================================

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 from repository
weather = WearWeatherRepository.getWeather()
isLoading = false
}

Expand Down