diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/weather/WeatherMapper.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/weather/WeatherMapper.kt index a22f44decb..a5919e6c79 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/weather/WeatherMapper.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/weather/WeatherMapper.kt @@ -335,57 +335,71 @@ object WeatherMapper { @JvmStatic fun mapToCmfCondition(openWeatherMapCondition: Int): Byte { /* deducted values: - 1 = sunny - 2 = cloudy - 3 = overcast - 4 = showers - 5 = snow showers - 6 = fog + 1 = sunny - sun with rays + 2 = cloudy - cloud + 3 = overcast - two clouds + 4 = showers - cloud with rain + 5 = snow showers - showflake + 6 = fog - multiple lines showing "fog" - 9 = thunder showers + 9 = thunder showers - lightning - 14 = sleet + 14 = sleet - cloud with dots under - 19 = hot (extreme) - 20 = cold (extreme) + 19 = hot (extreme) - flame + 20 = cold (extreme) - snowman - 21 = strong wind - 22 = (night) sunny - with moon - 23 = (night) sunny with stars - 24 = (night) cloudy - with moon - 25 = sun with haze - 26 = cloudy (sun with cloud) - */ + 21 = strong wind - wind moving icon + 22 = night haze - with moon haze + 23 = clear night - moon with star + 24 = cloudy night - moon with cloud + 25 = sun with haze + 26 = cloudy - sun with cloud + */ return when (openWeatherMapCondition) { - 210, 200, 201, 202, 230, 231, 232, 211, 212, 221 -> 9 + // Group 2xx: Thunderstorm + 200, 201, 202, 210, 211, 212, 221, 230, 231, 232 -> 9 // Thunderstorm - only one thunderstorm icon on watch + // Group 3xx: Drizzle + 300, 301, 302, 310, 311, 312, 313, 314, 321 -> 4 // showers + // Group 5xx: Rain + 500, 501, 502, 503, 504, 520, 521, 522, 531 -> 4 // showers + 511 -> 11 // freezing rain, icon used by openweathermap + // Group 6xx: Snow + 600, 601, 602, 620, 621, 622 -> 11 // snow showers + 611, 612, 613, 615, 616 -> 14 // Sleet + // Group 7xx: Atmosphere + 701, 711, 731, 741, 751, 761, 762 -> 6 // fog + 721 -> 25 // haze (day & night) + 781, 771 -> 21 // strong wind (tornado, squalls) + // Group 800: Clear + 800 -> 1 // clear (day & night) + // Group 80x: Clouds + 801, 802 -> 26 // sun/moon with cloud (night & day) + 803 -> 2 // cloudy + 804 -> 3 // overcast - 901, 781, 900, 771, 960, 961, 902, 962 -> 21 + // Other codes + 900 -> 9 // thunderstorm + 901, 902, 962, 905 -> 21 // strong wind (tropical storm, hurricane, windy) + 903 -> 20 // cold + 904 -> 19 // hot + 906 -> 11 // hail, snow showers (or should it be sleet?) - 300, 301, 302, 310, 311, 312, 313, 314, 321, 500, 501, 502, 503, 504, 520, 521, 522, 531 -> 4 - - 906, 615, 616, 511 -> 14 - - 611, 612, 600, 601, 602, 620, 621, 622 -> 5 - - - 701, 711, 721, 731, 741, 751, 761, 762 -> 6 - - 800 -> 1 - - 904 -> 19 - - 801, 802 -> 26 - 803 -> 2 - - 804 -> 3 - - 905, 951, 952, 953, 954, 955, 956, 957, 958, 959 -> 21 - - 903 -> 20 - else -> 20 + // strong wind (calm, light,gentle,moderate,fresh,strong-breeze, high,severe,windcase-gale, (violent) storm) + 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961 -> 21 + else -> 0 // no data icon } } + @JvmStatic + fun cmfConditionToNight(cmfCondition: Byte): Byte { + if (cmfCondition.toInt() == 1) return 23 // clear --> clear night + if (cmfCondition.toInt() == 26) return 24 // sun with cloud --> moon with cloud + if (cmfCondition.toInt() == 25) return 22 // sun with haze --> moon with haze + return cmfCondition + } + + @JvmStatic fun mapToFitProCondition(openWeatherMapCondition: Int): Byte { return when (openWeatherMapCondition) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfWatchProSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfWatchProSupport.java index 5725f94143..962b87d7af 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfWatchProSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/cmfwatchpro/CmfWatchProSupport.java @@ -44,6 +44,9 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -829,8 +832,19 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen final boolean supportsSunriseSunset = getCoordinator().supportsSunriseSunset(); final int payloadLength = (7 * 9) + (24 * 2) + (supportsSunriseSunset ? 32 : 30) + (supportsSunriseSunset ? 7 * 8 : 0); final ByteBuffer buf = ByteBuffer.allocate(payloadLength).order(ByteOrder.BIG_ENDIAN); + + long currentTime = System.currentTimeMillis() / 1000; + long sunrise = weatherSpec.getSunRise(); // epoch seconds + long sunset = weatherSpec.getSunSet(); // epoch seconds + boolean isDay = currentTime >= sunrise && currentTime < sunset; + // start with the current day's weather - buf.put(WeatherMapper.mapToCmfCondition(weatherSpec.getCurrentConditionCode())); + // Condition + byte cmfCondition = WeatherMapper.mapToCmfCondition(weatherSpec.getCurrentConditionCode()); + // If the sun has set, use moon icons where possible + if (!isDay) cmfCondition = WeatherMapper.cmfConditionToNight(cmfCondition); + buf.put(cmfCondition); + // Temperatures, humidity, aqi, uv and wind speed buf.put((byte) (weatherSpec.getCurrentTemp() - 273 + 100)); // convert Kelvin to C, add 100 buf.put((byte) (weatherSpec.getTodayMaxTemp() - 273 + 100)); // convert Kelvin to C, add 100 buf.put((byte) (weatherSpec.getTodayMinTemp() - 273 + 100)); // convert Kelvin to C, add 100 @@ -845,11 +859,12 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen for (int i = 0; i < 6; i++) { if (i < maxForecastsAvailable) { WeatherSpec.Daily forecastDay = weatherSpec.getForecasts().get(i); + // The watch can only show one icon for future days and this is the "day" icon: buf.put(WeatherMapper.mapToCmfCondition(forecastDay.getConditionCode())); // weather condition flag buf.put((byte) (forecastDay.getMaxTemp() - 273 + 100)); // temp in C (not shown in future days' forecasts) buf.put((byte) (forecastDay.getMaxTemp() - 273 + 100)); // max temp in C, + 100 buf.put((byte) (forecastDay.getMinTemp() - 273 + 100)); // min temp in C, + 100 - buf.put((byte) forecastDay.getHumidity()); // humidity as a % + buf.put((byte) forecastDay.getHumidity()); // humidity as a %, not shown by watch? buf.putShort((short) (forecastDay.getAirQuality() != null ? forecastDay.getAirQuality().getAqi() : 0)); buf.put((byte) forecastDay.getUvIndex()); // UV index isn't shown. uvi decimal/100, so 0x07 = 700 UVI. buf.put((byte) forecastDay.getWindSpeed()); // isn't shown by watch, unsure of correct units @@ -866,18 +881,52 @@ public class CmfWatchProSupport extends AbstractBTLESingleDeviceSupport implemen } } - // now add the hourly data for today - just condition and temperature + + // hourly data for next 24 hours, the current hour (or hours before that) should not be included! only condition and temperature int maxHourlyForecastsAvailable = weatherSpec.getHourly().size(); - for (int i = 0; i < 24; i++) { - if (i < maxHourlyForecastsAvailable) { - WeatherSpec.Hourly forecastHr = weatherSpec.getHourly().get(i); - buf.put((byte) (forecastHr.getTemp() - 273 + 100)); // temperature - buf.put((byte) forecastHr.getConditionCode()); // condition - } else { - buf.put((byte) (weatherSpec.getCurrentTemp() - 273 + 100)); // assume current temp - buf.put(WeatherMapper.mapToCmfCondition(weatherSpec.getCurrentConditionCode())); // current condition + int writtenHourlyForecasts = 0; + long nextHour = ((currentTime + 3600 - 1) / 3600) * 3600; + + // sunset/sunrise stuff, since we only show 24h, only today and tomorrow is important + WeatherSpec.Daily tomorrow = weatherSpec.getForecasts().get(0); // Weatherspec is today, forecasts is tomorrow and onward + LocalDate tomorrowDate = Instant.ofEpochSecond(tomorrow.getSunRise()).atZone(ZoneOffset.UTC).toLocalDate(); + + for (int i = 0; i < maxHourlyForecastsAvailable && writtenHourlyForecasts < 24; i++) { + WeatherSpec.Hourly forecastHr = weatherSpec.getHourly().get(i); + // Skip current / past hours + if (forecastHr.getTimestamp() < nextHour) { + continue; } + + // Temperature + buf.put((byte) (forecastHr.getTemp() - 273 + 100)); + + // If the checked forecast-hour is more the current day sunset, check if the day has ended, if so update the sunset/sunrise to be for tomorrow + if (forecastHr.getTimestamp() > sunset) { + LocalDate forecastHrDate = Instant.ofEpochSecond(forecastHr.getTimestamp()).atZone(ZoneOffset.UTC).toLocalDate(); + if (forecastHrDate.equals(tomorrowDate)) { + sunrise = tomorrow.getSunRise(); + sunset = tomorrow.getSunSet(); + } + } + isDay = forecastHr.getTimestamp() >= sunrise && forecastHr.getTimestamp() < sunset; + + // Get condition + cmfCondition = WeatherMapper.mapToCmfCondition(forecastHr.getConditionCode()); + // If the sun has set, use moon icons where possible + if (!isDay) cmfCondition = WeatherMapper.cmfConditionToNight(cmfCondition); + buf.put(cmfCondition); // condition + + writtenHourlyForecasts++; } + + // Pad if fewer than 24 entries were written + while (writtenHourlyForecasts < 24) { + buf.put((byte) 0x01); + buf.put((byte) 0x00); + writtenHourlyForecasts++; + } + // place name - watch scrolls after ~10 chars. Pad up to 32 bytes. final byte[] locationNameBytes = nodomain.freeyourgadget.gadgetbridge.util.StringUtils.truncateToBytes(weatherSpec.getLocation(), 30); buf.put(locationNameBytes);