1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-06-18 11:00:09 +02:00

Nothing CMF Watch Pro: Add weather support

This patch adds support for current weather, and next 6 days' weather. Condition mapping added to align with the available icons on the watch.
It also transmits the hourly condition and temperature for the coming 24 hours as part of the update.

Tested on CMF Nothing Watch Pro firmware 11.0.0.50 with weather data cooming from Breezy Weather (using Accuweather)

For current day:
- Weather symbol shows
- Name of current location shows (long names scroll)
- Current temperature shows
- Written condition shows (e.g. "Cloudy")
- Min/max temperatures show
- Air quality indicator shows

For upcoming days:
- Weather symbol shows
- Min/max temperatures show
- Name of day shows (patch doesn't touch this)

Nothing CMF Watch Pro: Use putShort() for air quality indicator; fix max location length

- Using putShort() as suggested from code review - tested to give same result
- Reduced max location length to 16 bytes, as 32 was not working

Nothing CMF Watch Pro: Better handle limited data from weather providers

- Check max length of daily and hourly datasets
- Populate with dummy data if insufficient data available
- Use null as the weather condition in any situation where no data available

Nothing CMF Watch Pro: If hourly weather data is missing, use current data

This should create a better fallback behaviour if a weather source is lacking hour-by-hour data.
Assuming the current data will apply in the next hour is less messy than showing placeholder (inaccurate) figures.

Nothing CMF Watch Pro: Allow location names of up to 30 characters, improve string processing
This commit is contained in:
g_p 2024-03-24 20:41:57 +00:00
parent 1e2a561dfd
commit 7cb7c0ea8a
3 changed files with 209 additions and 2 deletions

View File

@ -297,7 +297,7 @@ public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsWeather() {
return false; // TODO weather is not implemented
return true;
}
@Override

View File

@ -889,6 +889,151 @@ public class Weather {
return 5;
}
}
public static byte mapToCmfCondition(int openWeatherMapCondition) {
/* deducted values:
1 = sunny
2 = cloudy
3 = overcast
4 = showers
5 = snow showers
6 = fog
9 = thunder showers
14 = sleet
19 = hot (extreme)
20 = cold (extreme)
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)
*/
switch (openWeatherMapCondition) {
//Group 2xx: Thunderstorm
case 210: //light thunderstorm:: //11d
case 200: //thunderstorm with light rain: //11d
case 201: //thunderstorm with rain: //11d
case 202: //thunderstorm with heavy rain: //11d
case 230: //thunderstorm with light drizzle: //11d
case 231: //thunderstorm with drizzle: //11d
case 232: //thunderstorm with heavy drizzle: //11d
case 211: //thunderstorm: //11d
case 212: //heavy thunderstorm: //11d
case 221: //ragged thunderstorm: //11d
return 9;
//Group 90x: Extreme
case 901: //tropical storm
//Group 7xx: Atmosphere
case 781: //tornado: //[[file:50d.png]]
//Group 90x: Extreme
case 900: //tornado
// Group 7xx: Atmosphere
case 771: //squalls: //[[file:50d.png]]
//Group 9xx: Additional
case 960: //storm
case 961: //violent storm
case 902: //hurricane
case 962: //hurricane
return 21;
//Group 3xx: Drizzle
case 300: //light intensity drizzle: //09d
case 301: //drizzle: //09d
case 302: //heavy intensity drizzle: //09d
case 310: //light intensity drizzle rain: //09d
case 311: //drizzle rain: //09d
case 312: //heavy intensity drizzle rain: //09d
case 313: //shower rain and drizzle: //09d
case 314: //heavy shower rain and drizzle: //09d
case 321: //shower drizzle: //09d
//Group 5xx: Rain
case 500: //light rain: //10d
case 501: //moderate rain: //10d
case 502: //heavy intensity rain: //10d
case 503: //very heavy rain: //10d
case 504: //extreme rain: //10d
case 520: //light intensity shower rain: //09d
case 521: //shower rain: //09d
case 522: //heavy intensity shower rain: //09d
case 531: //ragged shower rain: //09d
return 4;
//Group 90x: Extreme
case 906: //hail
case 615: //light rain and snow: //[[file:13d.png]]
case 616: //rain and snow: //[[file:13d.png]]
case 511: //freezing rain: //13d
return 14;
//Group 6xx: Snow
case 611: //sleet: //[[file:13d.png]]
case 612: //shower sleet: //[[file:13d.png]]
//Group 6xx: Snow
case 600: //light snow: //[[file:13d.png]]
case 601: //snow: //[[file:13d.png]]
//Group 6xx: Snow
case 602: //heavy snow: //[[file:13d.png]]
//Group 6xx: Snow
case 620: //light shower snow: //[[file:13d.png]]
case 621: //shower snow: //[[file:13d.png]]
case 622: //heavy shower snow: //[[file:13d.png]]
return 5;
//Group 7xx: Atmosphere
case 701: //mist: //[[file:50d.png]]
case 711: //smoke: //[[file:50d.png]]
case 721: //haze: //[[file:50d.png]]
case 731: //sandcase dust whirls: //[[file:50d.png]]
case 741: //fog: //[[file:50d.png]]
case 751: //sand: //[[file:50d.png]]
case 761: //dust: //[[file:50d.png]]
case 762: //volcanic ash: //[[file:50d.png]]
return 6;
//Group 800: Clear
case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]]
return 1;
//Group 90x: Extreme
case 904: //hot
return 19;
//Group 80x: Clouds
case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]]
case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]]
return 26;
case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]]
return 2;
//Group 80x: Clouds
case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]]
return 3;
//Group 9xx: Additional
case 905: //windy
case 951: //calm
case 952: //light breeze
case 953: //gentle breeze
case 954: //moderate breeze
case 955: //fresh breeze
case 956: //strong breeze
case 957: //high windcase near gale
case 958: //gale
case 959: //severe gale
return 21;
default:
//Group 90x: Extreme
case 903: //cold
return 20;
}
}
public static byte mapToFitProCondition(int openWeatherMapCondition) {
switch (openWeatherMapCondition) {

View File

@ -56,6 +56,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.Contact;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
@ -584,7 +585,68 @@ public class CmfWatchProSupport extends AbstractBTLEDeviceSupport implements Cmf
@Override
public void onSendWeather(final WeatherSpec weatherSpec) {
// TODO onSendWeather
// TODO consider adjusting the condition code for clear/sunny so "clear" at night doesn't show a sunny icon (perhaps 23 decimal)?
// Each weather entry takes up 9 bytes
// There are 7 of those weather entries - 7*9 bytes
// Then there are 24-hour entries of temp and weather condition (2 bytes each)
// Then finally the location name as bytes - allow for 30 bytes, watch auto-scrolls
final ByteBuffer buf = ByteBuffer.allocate((7*9) + (24*2) + 30).order(ByteOrder.BIG_ENDIAN);
// start with the current day's weather
buf.put(Weather.mapToCmfCondition(weatherSpec.currentConditionCode));
buf.put((byte) (weatherSpec.currentTemp - 273 + 100)); // convert Kelvin to C, add 100
buf.put((byte) (weatherSpec.todayMaxTemp - 273 + 100)); // convert Kelvin to C, add 100
buf.put((byte) (weatherSpec.todayMinTemp - 273 + 100)); // convert Kelvin to C, add 100
buf.put((byte) weatherSpec.currentHumidity);
buf.putShort((short) weatherSpec.airQuality.aqi);
buf.put((byte) weatherSpec.uvIndex); // UV index isn't shown. uvi decimal/100, so 0x07 = 700 UVI.
buf.put((byte) weatherSpec.windSpeed); // isn't shown by watch, unsure of correct units
// find out how many future days' forecasts are available
int maxForecastsAvailable = weatherSpec.forecasts.size();
// For each day of the forecast
for (int i=0; i < 6; i++) {
if (i < maxForecastsAvailable) {
WeatherSpec.Daily forecastDay = weatherSpec.forecasts.get(i);
buf.put((byte) (Weather.mapToCmfCondition(forecastDay.conditionCode))); // weather condition flag
buf.put((byte) (forecastDay.maxTemp - 273 + 100)); // temp in C (not shown in future days' forecasts)
buf.put((byte) (forecastDay.maxTemp - 273 + 100)); // max temp in C, + 100
buf.put((byte) (forecastDay.minTemp - 273 + 100)); // min temp in C, + 100
buf.put((byte) forecastDay.humidity); // humidity as a %
try { // AQI data might not be available for the full 7 day forecast.
buf.putShort((short) weatherSpec.airQuality.aqi);
} catch (java.lang.NullPointerException ex) {
buf.putShort((short) 0);
}
buf.put((byte) forecastDay.uvIndex); // UV index isn't shown. uvi decimal/100, so 0x07 = 700 UVI.
buf.put((byte) forecastDay.windSpeed); // isn't shown by watch, unsure of correct units
} else {
// we need to provide a dummy forecast as there's no data available
buf.put((byte) 0x00); // NULL weather condition
buf.put((byte) 0x01); // -99 C temp temp
buf.put((byte) 0x01); // -99 C max temp
buf.put((byte) 0x01); // -99 C min temp
buf.put((byte) 0x00); // 0 humidity
buf.putShort((short) 0); // aqi
buf.put((byte) 0x00); // 0 UV index
buf.put((byte) 0x00); // 0 wind speed
}
}
// now add the hourly data for today - just condition and temperature
int maxHourlyForecastsAvailable = weatherSpec.hourly.size();
for (int i=0; i < 24; i++) {
if (i < maxHourlyForecastsAvailable) {
WeatherSpec.Hourly forecastHr = weatherSpec.hourly.get(i);
buf.put((byte) (forecastHr.temp - 273 + 100)); // temperature
buf.put((byte) forecastHr.conditionCode); // condition
} else {
buf.put((byte) (weatherSpec.currentTemp - 273 + 100)); // assume current temp
buf.put((byte) (Weather.mapToCmfCondition(weatherSpec.currentConditionCode))); // current condition
}
}
// place name - watch scrolls after ~10 chars
buf.put(StringUtils.truncate(weatherSpec.location, 30).getBytes(StandardCharsets.UTF_8));
sendCommand("send weather", CmfCommand.WEATHER_SET_1, buf.array());
}
@Override