WeatherSpec: determine whether it's night

This also resolves several TODO comments in code that that previously
hard-coded it to never be night, primarily in Pebble code because
that's what I have for testing.

In WeatherMapper, map the day and night icon variants for all condition codes,
even if the reference doesn't show a difference between the two icons.
Map condition 511 "freezing rain" to use icon 13 "snow" instead of icon 09
"shower rain" to match the API reference documentation.
This commit is contained in:
Avery Sterk
2026-02-05 04:08:24 +01:00
committed by Avery Sterk
parent aefa384bbe
commit c13d5fcadb
9 changed files with 67 additions and 30 deletions
@@ -1,6 +1,6 @@
/* Copyright (C) 2016-2024 Andreas Shimokawa, Arjan Schrijver, beardhatcode,
/* Copyright (C) 2016-2026 Andreas Shimokawa, Arjan Schrijver, beardhatcode,
Carsten Pfeiffer, Daniele Gobbetti, Enrico Brambilla, José Rebelo, Taavi
Eomäe
Eomäe, Avery Sterk
This file is part of Gadgetbridge.
@@ -130,6 +130,49 @@ class WeatherSpec() : Parcelable {
isCurrentLocation = currLoc
}
/**
* Determines whether the weather condition was retrieved when the sun was down
* @return True if the weather timestamp was outside the sunrise-sunset interval
*/
fun isNight(): Boolean {
return isTimeNight( this.timestamp * 1000L )
}
/**
* Abstraction for whether the sunrise/set information indicates "polar night" (no sunrise)
* @return True if polar night, false otherwise
*/
fun isPolarNight(): Boolean {
return (this.sunSet == 0) // unix time instant of 0
}
/**
* Abstraction for whether the sunrise/set information indicates "polar day" (sun never sets)
* @return True if polar day, false otherwise
*/
fun isPolarDay(): Boolean {
return ((this.sunSet - this.sunRise) >= 86399) // sun is up every second of the day
}
/**
* Determines whether the current time falls during a night period based on sunrise and sunset
* @return True if the current time of day is outside the sunset-sunrise interval
*/
fun isCurrentTimeNight(): Boolean {
return isTimeNight( System.currentTimeMillis() )
}
/**
* Checks whether a given time falls outside the sunrise-sunset interval
* @param unixTimeMilliSeconds Unix timestamp, in UTC, in milliseconds
* @return True if outside sunrise interval, false if during sunrise
*/
fun isTimeNight(unixTimeMilliSeconds: Long): Boolean {
if (isPolarNight()) return true
if (isPolarDay()) return false
val millisPastMidnight = unixTimeMilliSeconds % 86400000
if ( millisPastMidnight < (this.sunRise % 86400 * 1000L ) ) return true
return ( millisPastMidnight > (this.sunSet % 86400 * 1000L ) )
}
fun getLocationObject(): Location? {
return if (latitude == 0f && longitude == 0f) null
else Location("weatherSpec").apply {
@@ -751,4 +794,4 @@ class WeatherSpec() : Parcelable {
return weather
}
}
}
}
@@ -6,18 +6,19 @@ import nodomain.freeyourgadget.gadgetbridge.R
object WeatherMapper {
@JvmStatic
fun mapToOpenWeatherMapIcon(code: Int): String = when {
fun mapToOpenWeatherMapIcon(code: Int, isNight: Boolean = false): String = when (code) {
//see https://openweathermap.org/weather-conditions
code in 200..299 -> "11d"
code in 300..499 -> "09d"
code in 500..509 -> "10d"
code in 511..599 -> "09d"
code in 600..699 -> "13d"
code in 700..799 -> "50d"
code == 800 -> "01d" //TODO: night?
code == 801 -> "02d" //TODO: night?
code == 802 -> "03d" //TODO: night?
code == 803 || code == 804 -> "04d" //TODO: night?
in 200..299 -> if (isNight) { "11n" } else { "11d" }
in 300..499 -> if (isNight) { "09n" } else { "09d" }
in 500..509 -> if (isNight) { "10n" } else { "10d" }
511 -> if (isNight) { "13n" } else { "13d" }
in 512..599 -> if (isNight) { "09n" } else { "09d" }
in 600..699 -> if (isNight) { "13n" } else { "13d" }
in 700..799 -> if (isNight) { "50n" } else { "50d" }
800 -> if (isNight) { "01n" } else { "01d" }
801 -> if (isNight) { "02n" } else { "02d" }
802 -> if (isNight) { "03n" } else { "03d" }
803, 804 -> if (isNight) { "04n" } else { "04d" }
else -> "02d" // fallback
}
@@ -149,10 +149,10 @@ class AppMessageHandlerObsidian extends AppMessageHandler {
}
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
boolean isNight = false; //TODO: use the night icons when night
boolean isNight = weatherSpec.isNight();
pairs.add(new Pair<>(messageKeys.get("CONFIG_WEATHER_REFRESH"), (Object) 60));
pairs.add(new Pair<>(messageKeys.get("CONFIG_WEATHER_UNIT_LOCAL"), (Object) 1)); //celsius
pairs.add(new Pair<>(messageKeys.get("MSG_KEY_WEATHER_ICON"), (Object) getIconForConditionCode(weatherSpec.getCurrentConditionCode(), isNight))); //celsius
pairs.add(new Pair<>(messageKeys.get("MSG_KEY_WEATHER_ICON"), (Object) getIconForConditionCode(weatherSpec.getCurrentConditionCode(), isNight)));
pairs.add(new Pair<>(messageKeys.get("MSG_KEY_WEATHER_TEMP"), (Object) (weatherSpec.getCurrentTemp() - 273)));
return mPebbleProtocol.encodeApplicationMessagePush(PebbleProtocol.ENDPOINT_APPLICATIONMESSAGE, mUUID, pairs, null);
@@ -173,4 +173,4 @@ class AppMessageHandlerObsidian extends AppMessageHandler {
public byte[] encodeUpdateWeather(WeatherSpec weatherSpec) {
return encodeObsidianWeather(weatherSpec);
}
}
}
@@ -115,7 +115,7 @@ class AppMessageHandlerRealWeather extends AppMessageHandler {
if (weatherSpec == null) {
return null;
}
boolean isNight = false; // TODO
boolean isNight = weatherSpec.isNight();
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_WEATHER_TEMP, (Object) (String.format(Locale.ENGLISH, "%.0f°", weatherSpec.getCurrentTemp() - 273.15))));
pairs.add(new Pair<>(KEY_WEATHER_ICON, (Object) (getIconForConditionCode(weatherSpec.getCurrentConditionCode(), isNight))));
@@ -79,12 +79,8 @@ class AppMessageHandlerTearsOfTheKingdom extends AppMessageHandler {
return null;
}
int weather_time = weatherSpec.getTimestamp();
// approximate night as the time as any time before sunrise or after sunset
boolean isNight = ( weather_time < weatherSpec.getSunRise() ) || ( weather_time > weatherSpec.getSunSet()) ;
int temperature_f = (weatherSpec.getCurrentTemp() - 255) * 9 / 5; // 255K is 0 degF
int condition = getIconFromConditionCode(weatherSpec.getCurrentConditionCode(), isNight);
int condition = getIconFromConditionCode(weatherSpec.getCurrentConditionCode(), weatherSpec.isNight());
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(TEMPERATURE_KEY, (Object) temperature_f));
@@ -137,10 +137,7 @@ class AppMessageHandlerTimeStylePebble extends AppMessageHandler {
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>();
boolean isNight = false;
if (weatherSpec.getSunRise() != 0 && weatherSpec.getSunSet() != 0) {
isNight = weatherSpec.getSunRise() * 1000L > System.currentTimeMillis() || weatherSpec.getSunSet() * 1000L < System.currentTimeMillis();
}
boolean isNight = weatherSpec.isCurrentTimeNight();
pairs.add(new Pair<>(messageKeys.get("SettingUseMetric"), 1)); //celsius
pairs.add(new Pair<>(messageKeys.get("WeatherUseNightIcon"), isNight ? 1 : 0));
@@ -190,7 +190,7 @@ class AppMessageHandlerYWeather extends AppMessageHandler {
if (weatherSpec == null) {
return null;
}
boolean isNight = false; // TODO
boolean isNight = weatherSpec.isNight();
ArrayList<Pair<Integer, Object>> pairs = new ArrayList<>(2);
pairs.add(new Pair<>(KEY_LOCATION_NAME, (Object) (weatherSpec.getLocation())));
pairs.add(new Pair<>(KEY_WEATHER_TEMP, (Object) (String.format(Locale.ENGLISH, "%.0f°", weatherSpec.getCurrentTemp() - 273.15))));
@@ -367,7 +367,7 @@ public class GBWebClient extends WebViewClient {
condition.put("id", weatherSpec.getCurrentConditionCode());
condition.put("main", weatherSpec.getCurrentCondition());
condition.put("description", weatherSpec.getCurrentCondition());
condition.put("icon", WeatherMapper.mapToOpenWeatherMapIcon(weatherSpec.getCurrentConditionCode()));
condition.put("icon", WeatherMapper.mapToOpenWeatherMapIcon(weatherSpec.getCurrentConditionCode(),weatherSpec.isNight()));
weather.put(condition);
@@ -50,7 +50,7 @@ public class WeatherMapperTest extends TestBase {
for (int i = 0; i <= 3200; i++) {
Assert.assertEquals(Weather.mapToPebbleCondition(i), WeatherMapper.mapToPebbleCondition(i));
Assert.assertEquals(Weather.mapToYahooCondition(i), WeatherMapper.mapToYahooCondition(i));
Assert.assertEquals(Weather.mapToOpenWeatherMapIcon(i), WeatherMapper.mapToOpenWeatherMapIcon(i));
Assert.assertEquals(Weather.mapToOpenWeatherMapIcon(i), WeatherMapper.mapToOpenWeatherMapIcon(i,false));
Assert.assertEquals(Weather.mapToOpenWeatherMapCondition(i), WeatherMapper.mapToOpenWeatherMapCondition(i));
Assert.assertEquals(Weather.getConditionString(getContext(), i), WeatherMapper.getConditionString(getContext(), i));
Assert.assertEquals(Weather.getAqiLevelString(getContext(), i), WeatherMapper.getAqiLevelString(getContext(), i));