mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-10 12:09:27 +01: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:
parent
1e2a561dfd
commit
7cb7c0ea8a
@ -297,7 +297,7 @@ public class CmfWatchProCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false; // TODO weather is not implemented
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user