1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-28 21:06:50 +01:00

PineTime Weather support

This commit is contained in:
TaaviE 2022-09-07 16:38:39 +03:00 committed by Gitea
parent 2928a0e13b
commit 230cbe964b
4 changed files with 1198 additions and 6 deletions

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2020-2021 Taavi Eomäe
/* Copyright (C) 2020-2022 Taavi Eomäe, Stephan Lachnit, ITCactus
This file is part of Gadgetbridge.
@ -34,8 +34,14 @@ public class PineTimeJFConstants {
public static final UUID UUID_CHARACTERISTICS_MUSIC_REPEAT = UUID.fromString("0000000b-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_CHARACTERISTICS_MUSIC_SHUFFLE = UUID.fromString("0000000c-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_SERVICE_NAVIGATION = UUID.fromString("00010000-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT = UUID.fromString("00020001-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_SERVICE_WEATHER = UUID.fromString("00040000-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_CHARACTERISTIC_WEATHER_DATA = UUID.fromString("00040001-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_CHARACTERISTIC_WEATHER_CONTROL = UUID.fromString("00040002-78fc-48fe-8e23-433b3a1942d0");
// since 1.7. https://github.com/InfiniTimeOrg/InfiniTime/blob/develop/doc/MotionService.md
public static final UUID UUID_SERVICE_MOTION = UUID.fromString("00030000-78fc-48fe-8e23-433b3a1942d0");
public static final UUID UUID_CHARACTERISTIC_MOTION_STEP_COUNT = UUID.fromString("00030001-78fc-48fe-8e23-433b3a1942d0");

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2021 Andreas Shimokawa, Carsten Pfeiffer, Daniele
/* Copyright (C) 2016-2022 Andreas Shimokawa, Carsten Pfeiffer, Daniele
Gobbetti, José Rebelo, Taavi Eomäe
This file is part of Gadgetbridge.
@ -126,7 +126,7 @@ public class PineTimeJFCoordinator extends AbstractBLEDeviceCoordinator {
@Override
public boolean supportsWeather() {
return false;
return true;
}
@Override

View File

@ -0,0 +1,927 @@
/* Copyright (C) 2021-2022 TaaviE
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather;
/**
* Implemented based on and other material:
* https://en.wikipedia.org/wiki/METAR
* https://www.weather.gov/jetstream/obscurationtypes
* http://www.faraim.org/aim/aim-4-03-14-493.html
*/
public class WeatherData {
/**
* OpenWeather's condition codes are a bit annoying,
* they've mixed precipitation with other weather events.
* <p>
* Even if they're absolutely not mutually exclusive.
* <p>
* So, this function only returns PrecipitationType
*/
public static final PrecipitationType mapOpenWeatherConditionToPineTimePrecipitation(int openWeatherMapCondition) {
switch (openWeatherMapCondition) {
// Group 2xx: Thunderstorm
case 200: // Thunderstorm with light rain
case 201: // Thunderstorm with rain
case 202: // Thunderstorm with heavy rain
case 210: // Light thunderstorm
case 211: // Thunderstorm
case 230: // Thunderstorm with light drizzle
case 231: // Thunderstorm with drizzle
case 232: // Thunderstorm with heavy drizzle
case 212: // Heavy thunderstorm
case 221: // Ragged thunderstorm
return PrecipitationType.Rain;
// Group 3xx: Drizzle
case 300: // Light intensity drizzle
case 301: // Drizzle
case 302: // Heavy intensity drizzle
case 310: // Light intensity drizzle rain
case 311: // Drizzle rain
case 312: // Heavy intensity drizzle rain
case 313: // Shower rain and drizzle
case 314: // Heavy shower rain and drizzle
case 321: // Shower drizzle
return PrecipitationType.Drizzle;
// Group 5xx: Rain
case 500: // Light rain
case 501: // Moderate rain
case 502: // Heavy intensity rain
case 503: // Very heavy rain
case 504: // Extreme rain
return PrecipitationType.Rain;
case 511: // Freezing rain
return PrecipitationType.FreezingRain;
case 520: // Light intensity shower rain
case 521: // Shower rain
case 522: // Heavy intensity shower rain
case 531: // Ragged shower rain
return PrecipitationType.Rain;
// Group 6xx: Snow
case 600: // Light snow
case 601: // Snow
case 620: // Light shower snow
case 602: // Heavy snow
return PrecipitationType.Snow;
case 611: // Sleet
case 612: // Shower sleet
return PrecipitationType.Sleet;
case 621: // Shower snow
case 622: // Heavy shower snow
return PrecipitationType.Snow;
case 615: // Light rain and snow
case 616: // Rain and snow
return PrecipitationType.Sleet;
// Group 7xx: Atmosphere
case 701: // Mist
case 711: // Smoke
case 721: // Haze
case 731: // Sandcase/dust whirls
case 741: // Fog
case 751: // Sand
case 761: // Dust
case 762: // Volcanic ash
return PrecipitationType.Ash;
case 771: // Squalls
case 781: // Tornado
case 900: // Tornado
// Group 800: Clear
case 800: // Clear sky
return PrecipitationType.None;
// Group 80x: Clouds
case 801: // Few clouds
case 802: // Scattered clouds
case 803: // Broken clouds
case 804: // Overcast clouds
// Group 90x: Extreme
case 901: // Tropical storm
case 903: // Cold
case 904: // Hot
case 905: // Windy
case 906: // Hail
return PrecipitationType.Hail;
// Group 9xx: Additional
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
case 960: // Storm
case 961: // Violent storm
case 902: // Hurricane
case 962: // Hurricane
default:
return PrecipitationType.Length;
}
}
;
/**
* OpenWeather's condition codes are a bit annoying,
* they've mixed obscuration with other weather events.
*
* <p>
* Even if they're absolutely not mutually exclusive.
* <p>
* So, this function only returns ObscurationType
*/
public static final ObscurationType mapOpenWeatherConditionToPineTimeObscuration(int openWeatherMapCondition) {
switch (openWeatherMapCondition) {
// Group 2xx: Thunderstorm
case 200: // Thunderstorm with light rain
case 201: // Thunderstorm with rain
case 202: // Thunderstorm with heavy rain
case 210: // Light thunderstorm
case 211: // Thunderstorm
case 230: // Thunderstorm with light drizzle
case 231: // Thunderstorm with drizzle
case 232: // Thunderstorm with heavy drizzle
case 212: // Heavy thunderstorm
case 221: // Ragged thunderstorm
return ObscurationType.Precipitation;
// Group 3xx: Drizzle
case 300: // Light intensity drizzle
case 301: // Drizzle
case 302: // Heavy intensity drizzle
case 310: // Light intensity drizzle rain
case 311: // Drizzle rain
case 312: // Heavy intensity drizzle rain
case 313: // Shower rain and drizzle
case 314: // Heavy shower rain and drizzle
case 321: // Shower drizzle
return ObscurationType.Precipitation;
// Group 5xx: Rain
case 500: // Light rain
case 501: // Moderate rain
case 502: // Heavy intensity rain
case 503: // Very heavy rain
case 504: // Extreme rain
case 511: // Freezing rain
case 520: // Light intensity shower rain
case 521: // Shower rain
case 522: // Heavy intensity shower rain
case 531: // Ragged shower rain
return ObscurationType.Precipitation;
// Group 6xx: Snow
case 600: // Light snow
case 601: // Snow
case 620: // Light shower snow
case 602: // Heavy snow
case 611: // Sleet
case 612: // Shower sleet
case 621: // Shower snow
case 622: // Heavy shower snow
case 615: // Light rain and snow
case 616: // Rain and snow
return ObscurationType.Precipitation;
// Group 7xx: Atmosphere
case 701: // Mist
return ObscurationType.Mist;
case 711: // Smoke
return ObscurationType.Smoke;
case 721: // Haze
return ObscurationType.Haze;
case 731: // Sandcase/dust whirls
return ObscurationType.Sand;
case 741: // Fog
return ObscurationType.Fog;
case 751: // Sand
return ObscurationType.Sand;
case 761: // Dust
return ObscurationType.Dust;
case 762: // Volcanic ash
return ObscurationType.Ash;
case 771: // Squalls
return ObscurationType.Length;
case 781: // Tornado
case 900: // Tornado
return ObscurationType.Precipitation;
// Group 800: Clear
case 800: // Clear sky
return ObscurationType.Length;
// Group 80x: Clouds
case 801: // Few clouds
case 802: // Scattered clouds
case 803: // Broken clouds
case 804: // Overcast clouds
return ObscurationType.Length;
// Group 90x: Extreme
case 901: // Tropical storm
return ObscurationType.Precipitation;
case 903: // Cold
case 904: // Hot
return ObscurationType.Length;
case 905: // Windy
case 906: // Hail
return ObscurationType.Precipitation;
// Group 9xx: Additional
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 ObscurationType.Length;
case 960: // Storm
case 961: // Violent storm
return ObscurationType.Precipitation;
case 902: // Hurricane
return ObscurationType.Precipitation;
case 962: // Hurricane
return ObscurationType.Precipitation;
default:
return ObscurationType.Length;
}
}
;
/**
* OpenWeather's condition codes are a bit annoying,
* they've mixed precipitation with other weather events.
* <p>
* Even if they're absolutely not mutually exclusive.
*
* <p>
* So, this function only returns SpecialType
*/
public static final SpecialType mapOpenWeatherConditionToPineTimeSpecial(int openWeatherMapCondition) {
switch (openWeatherMapCondition) {
// Group 2xx: Thunderstorm
case 200: // Thunderstorm with light rain
case 201: // Thunderstorm with rain
case 202: // Thunderstorm with heavy rain
case 210: // Light thunderstorm
case 211: // Thunderstorm
case 230: // Thunderstorm with light drizzle
case 231: // Thunderstorm with drizzle
case 232: // Thunderstorm with heavy drizzle
case 212: // Heavy thunderstorm
case 221: // Ragged thunderstorm
// Group 3xx: Drizzle
case 300: // Light intensity drizzle
case 301: // Drizzle
case 302: // Heavy intensity drizzle
case 310: // Light intensity drizzle rain
case 311: // Drizzle rain
case 312: // Heavy intensity drizzle rain
case 313: // Shower rain and drizzle
case 314: // Heavy shower rain and drizzle
case 321: // Shower drizzle
// Group 5xx: Rain
case 500: // Light rain
case 501: // Moderate rain
case 502: // Heavy intensity rain
case 503: // Very heavy rain
case 504: // Extreme rain
case 511: // Freezing rain
case 520: // Light intensity shower rain
case 521: // Shower rain
case 522: // Heavy intensity shower rain
case 531: // Ragged shower rain
// Group 6xx: Snow
case 600: // Light snow
case 601: // Snow
case 620: // Light shower snow
case 602: // Heavy snow
case 611: // Sleet
case 612: // Shower sleet
case 621: // Shower snow
case 622: // Heavy shower snow
case 615: // Light rain and snow
case 616: // Rain and snow
// Group 7xx: Atmosphere
case 701: // Mist
case 711: // Smoke
case 721: // Haze
case 731: // Sandcase/dust whirls
case 741: // Fog
case 751: // Sand
case 761: // Dust
case 762: // Volcanic ash
return SpecialType.Length;
case 771: // Squalls
return SpecialType.Squall;
case 781: // Tornado
case 900: // Tornado
// Group 800: Clear
case 800: // Clear sky
// Group 80x: Clouds
case 801: // Few clouds
case 802: // Scattered clouds
case 803: // Broken clouds
case 804: // Overcast clouds
// Group 90x: Extreme
case 901: // Tropical storm
case 903: // Cold
return SpecialType.Length;
case 904: // Hot
return SpecialType.Fire;
case 905: // Windy
case 906: // Hail
// Group 9xx: Additional
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
case 960: // Storm
case 961: // Violent storm
case 902: // Hurricane
case 962: // Hurricane
default:
return SpecialType.Length;
}
}
;
/**
* OpenWeather's condition codes are a bit annoying,
* they've mixed precipitation with other weather events.
* <p>
* Even if they're absolutely not mutually exclusive.
*
* <p>
* So, this function only returns cloudyness 0-100%
*/
public static final int mapOpenWeatherConditionToCloudCover(int openWeatherMapCondition) {
switch (openWeatherMapCondition) {
// Group 2xx: Thunderstorm
case 200: // Thunderstorm with light rain
case 201: // Thunderstorm with rain
case 202: // Thunderstorm with heavy rain
case 210: // Light thunderstorm
case 211: // Thunderstorm
case 230: // Thunderstorm with light drizzle
case 231: // Thunderstorm with drizzle
case 232: // Thunderstorm with heavy drizzle
case 212: // Heavy thunderstorm
case 221: // Ragged thunderstorm
return 100;
// Group 3xx: Drizzle
case 300: // Light intensity drizzle
case 301: // Drizzle
case 302: // Heavy intensity drizzle
case 310: // Light intensity drizzle rain
case 311: // Drizzle rain
case 312: // Heavy intensity drizzle rain
case 313: // Shower rain and drizzle
case 314: // Heavy shower rain and drizzle
case 321: // Shower drizzle
return 75;
// Group 5xx: Rain
case 500: // Light rain
case 501: // Moderate rain
case 502: // Heavy intensity rain
case 503: // Very heavy rain
case 504: // Extreme rain
case 511: // Freezing rain
case 520: // Light intensity shower rain
case 521: // Shower rain
case 522: // Heavy intensity shower rain
case 531: // Ragged shower rain
return 75;
// Group 6xx: Snow
case 600: // Light snow
case 601: // Snow
case 620: // Light shower snow
case 602: // Heavy snow
case 611: // Sleet
case 612: // Shower sleet
case 621: // Shower snow
case 622: // Heavy shower snow
case 615: // Light rain and snow
case 616: // Rain and snow
return 75;
// Group 7xx: Atmosphere
case 701: // Mist
case 711: // Smoke
case 721: // Haze
case 731: // Sandcase/dust whirls
case 741: // Fog
case 751: // Sand
case 761: // Dust
return -1;
case 762: // Volcanic ash
return 100;
case 771: // Squalls
case 781: // Tornado
case 900: // Tornado
// Group 800: Clouds & Clear
case 800: // Clear sky
return 0;
case 801: // Few clouds
return 25;
case 802: // Scattered clouds
return 50;
case 803: // Broken clouds
return 75;
case 804: // Overcast clouds
return 100;
// Group 90x: Extreme
case 901: // Tropical storm
case 903: // Cold
case 904: // Hot
case 905: // Windy
case 906: // Hail
return 75;
// Group 9xx: Additional
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
case 960: // Storm
case 961: // Violent storm
case 902: // Hurricane
case 962: // Hurricane
default:
return -1;
}
}
;
/**
* Visibility obscuration types
*/
public enum ObscurationType {
/**
* No obscuration
*/
None(0),
/**
* Water particles suspended in the air; low visibility; does not fall
*/
Fog(1),
/**
* Tiny, dry particles in the air; invisible to the eye; opalescent
*/
Haze(2),
/**
* Small fire-created particles suspended in the air
*/
Smoke(3),
/**
* Fine rock powder, from for example volcanoes
*/
Ash(4),
/**
* Fine particles of earth suspended in the air by the wind
*/
Dust(5),
/**
* Fine particles of sand suspended in the air by the wind
*/
Sand(6),
/**
* Water particles suspended in the air; low-ish visibility; temperature is near dewpoint
*/
Mist(7),
/**
* This is special in the sense that the thing falling down is doing the obscuration
*/
Precipitation(8),
Length(9);
public final int value;
ObscurationType(int value) {
this.value = value;
}
}
/**
* Types of precipitation
*/
public enum PrecipitationType {
/**
* No precipitation
* <p>
* Theoretically we could just _not_ send the event, but then
* how do we differentiate between no precipitation and
* no information about precipitation
*/
None(0),
/**
* Drops larger than a drizzle; also widely separated drizzle
*/
Rain(1),
/**
* Fairly uniform rain consisting of fine drops
*/
Drizzle(2),
/**
* Rain that freezes upon contact with objects and ground
*/
FreezingRain(3),
/**
* Rain + hail; ice pellets; small translucent frozen raindrops
*/
Sleet(4),
/**
* Larger ice pellets; falling separately or in irregular clumps
*/
Hail(5),
/**
* Hail with smaller grains of ice; mini-snowballs
*/
SmallHail(6),
/**
* Snow...
*/
Snow(7),
/**
* Frozen drizzle; very small snow crystals
*/
SnowGrains(8),
/**
* Needles; columns or plates of ice. Sometimes described as "diamond dust". In very cold regions
*/
IceCrystals(9),
/**
* It's raining down ash, e.g. from a volcano
*/
Ash(10),
Length(11);
public final int value;
PrecipitationType(int value) {
this.value = value;
}
}
/**
* These are special events that can "enhance" the "experience" of existing weather events
*/
public enum SpecialType {
/**
* Strong wind with a sudden onset that lasts at least a minute
*/
Squall(0),
/**
* Series of waves in a water body caused by the displacement of a large volume of water
*/
Tsunami(1),
/**
* Violent; rotating column of air
*/
Tornado(2),
/**
* Unplanned; unwanted; uncontrolled fire in an area
*/
Fire(3),
/**
* Thunder and/or lightning
*/
Thunder(4),
Length(5);
public final int value;
SpecialType(int value) {
this.value = value;
}
}
/**
* These are used for weather timeline manipulation
* that isn't just adding to the stack of weather events
*/
public enum ControlCodes {
/**
* How much is stored already
*/
GetLength(0),
/**
* This wipes the entire timeline
*/
DelTimeline(1),
/**
* There's a currently valid timeline event with the given type
*/
HasValidEvent(3),
Length(4);
public final int value;
ControlCodes(int value) {
this.value = value;
}
}
/**
* Events have types
* then they're easier to parse after sending them over the air
*/
public enum EventType {
/**
* @see WeatherData.Obscuration
*/
Obscuration(0),
/**
* @see WeatherData.Precipitation
*/
Precipitation(1),
/**
* @see WeatherData.Wind
*/
Wind(2),
/**
* @see WeatherData.Temperature
*/
Temperature(3),
/**
* @see WeatherData.AirQuality
*/
AirQuality(4),
/**
* @see WeatherData.Special
*/
Special(5),
/**
* @see WeatherData.Pressure
*/
Pressure(6),
/**
* @see WeatherData.Location
*/
Location(7),
/**
* @see WeatherData.Clouds
*/
Clouds(8),
/**
* @see WeatherData.Humidity
*/
Humidity(9),
Length(10);
public final int value;
EventType(int value) {
this.value = value;
}
}
;
/**
* Valid event query
*/
static public class ValidEventQuery {
ControlCodes code = ControlCodes.HasValidEvent;
EventType eventType;
}
;
/**
* The header used for further parsing
*/
static public class TimelineHeader {
/**
* UNIX timestamp
*/
long timestamp;
/**
* Time in seconds until the event expires
* <p>
* 32 bits ought to be enough for everyone
* <p>
* If there's a newer event of the same type then it overrides this one, even if it hasn't expired
*/
int expires;
/**
* What type of weather-related event
*/
EventType eventType;
}
/**
* Specifies how cloudiness is stored
*/
static public class Clouds extends TimelineHeader {
/**
* Cloud coverage in percentage, 0-100%
*/
byte amount;
}
/**
* Specifies how obscuration is stored
*/
static public class Obscuration extends TimelineHeader {
/**
* Type
*/
ObscurationType type;
/**
* Visibility distance in meters (0-65535)
*/
int amount;
}
/**
* Specifies how precipitation is stored
*/
static public class Precipitation extends TimelineHeader {
/**
* Type
*/
PrecipitationType type;
/**
* How much is it going to rain? In millimeters (0-255)
*/
int amount;
}
/**
* How wind speed is stored
* <p>
* In order to represent bursts of wind instead of constant wind,
* you have minimum and maximum speeds.
* <p>
* As direction can fluctuate wildly and some watchfaces might wish to display it nicely,
* we're following the aerospace industry weather report option of specifying a range.
*/
static public class Wind extends TimelineHeader {
/**
* Meters per second (0-255)
*/
byte speedMin;
/**
* Meters per second (0-255)
*/
byte speedMax;
/**
* Unitless direction between 0-255; approximately 1 unit per 0.71 degrees
*/
byte directionMin;
/**
* Unitless direction between 0-255; approximately 1 unit per 0.71 degrees
*/
byte directionMax;
}
/**
* How temperature is stored
* <p>
* As it's annoying to figure out the dewpoint on the watch,
* please send it from the companion
* <p>
* We don't do floats, microdegrees are not useful. Make sure to multiply.
*/
static public class Temperature extends TimelineHeader {
/**
* Temperature °C but multiplied by 100 (e.g. -12.50°C becomes -1250, 0-65535)
*/
short temperature;
/**
* Dewpoint °C but multiplied by 100 (e.g. -12.50°C becomes -1250, 0-65535)
*/
short dewPoint;
}
/**
* How location info is stored
* <p>
* This can be mostly static with long expiration,
* as it usually is, but it could change during a trip for ex.
* so we allow changing it dynamically.
* <p>
* Location info can be for some kind of map watchface
* or daylight calculations, should those be required.
*/
static public class Location extends TimelineHeader {
/**
* Location name
*/
String location;
/**
* Altitude relative to sea level in meters (0-65535)
*/
short altitude;
/**
* Latitude, EPSG:3857 (Google Maps, Openstreetmaps datum, 0-4294967295)
*/
int latitude;
/**
* Longitude, EPSG:3857 (Google Maps, Openstreetmaps datum, 0-4294967295)
*/
int longitude;
}
/**
* How humidity is stored
*/
static public class Humidity extends TimelineHeader {
/**
* Relative humidity, 0-100%
*/
byte humidity;
}
/**
* How air pressure is stored
*/
static public class Pressure extends TimelineHeader {
/**
* Air pressure in hectopascals (hPa, 0-65535)
*/
short pressure;
}
/**
* How special events are stored
*/
static public class Special extends TimelineHeader {
/**
* Special event's type
*/
SpecialType type;
}
/**
* How air quality is stored
* <p>
* These events are a bit more complex because the topic is not simple,
* the intention is to heavy-lift the annoying preprocessing from the watch
* this allows watchface or watchapp makers to generate accurate alerts and graphics
* <p>
* If this needs further enforced standardization, pull requests are welcome
*/
static public class AirQuality extends TimelineHeader {
/**
* The name of the pollution
* <p>
* for the sake of better compatibility with watchapps
* that might want to use this data for say visuals
* don't localize the name.
* <p>
* Ideally watchapp itself localizes the name, if it's at all needed.
* <p>
* E.g.
* For generic ones use "PM0.1", "PM5", "PM10"
* For chemical compounds use the molecular formula e.g. "NO2", "CO2", "O3"
* For pollen use the genus, e.g. "Betula" for birch or "Alternaria" for that mold's spores
*/
String polluter;
/**
* Amount of the pollution in SI units,
* otherwise it's going to be difficult to create UI, alerts
* and so on and for.
* <p>
* See more:
* https://ec.europa.eu/environment/air/quality/standards.htm
* http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf
* <p>
* Example units:
* count/m³ for pollen
* µgC/m³ for micrograms of organic carbon
* µg/m³ sulfates, PM0.1, PM1, PM2, PM10 and so on, dust
* mg/m³ CO2, CO
* ng/m³ for heavy metals
* <p>
* List is not comprehensive, should be improved.
* The current ones are what watchapps assume.
* <p>
* Note: ppb and ppm to concentration should be calculated on the companion, using
* the correct formula (taking into account temperature and air pressure)
* <p>
* Note2: The amount is off by times 100, for two decimal places of precision.
* E.g. 54.32µg/m³ is 5432
*/
int amount;
}
}

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2016-2021 Andreas Shimokawa, Carsten Pfeiffer, JF, Sebastian
/* Copyright (C) 2016-2022 Andreas Shimokawa, Carsten Pfeiffer, JF, Sebastian
Kranz, Taavi Eomäe
This file is part of Gadgetbridge.
@ -17,16 +17,25 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime;
import static nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather.WeatherData.mapOpenWeatherConditionToCloudCover;
import static nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather.WeatherData.mapOpenWeatherConditionToPineTimeObscuration;
import static nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather.WeatherData.mapOpenWeatherConditionToPineTimePrecipitation;
import static nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather.WeatherData.mapOpenWeatherConditionToPineTimeSpecial;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCharacteristic;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.widget.Toast;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Calendar;
@ -36,7 +45,8 @@ import java.util.Locale;
import java.util.TimeZone;
import java.util.UUID;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import co.nstant.in.cbor.CborBuilder;
import co.nstant.in.cbor.CborEncoder;
import no.nordicsemi.android.dfu.DfuLogListener;
import no.nordicsemi.android.dfu.DfuProgressListener;
import no.nordicsemi.android.dfu.DfuProgressListenerAdapter;
@ -55,6 +65,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeActivitySam
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeDFUService;
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeInstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFConstants;
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.weather.WeatherData;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.entities.PineTimeActivitySample;
import nodomain.freeyourgadget.gadgetbridge.entities.User;
@ -235,6 +246,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL
addSupportedService(GattService.UUID_SERVICE_DEVICE_INFORMATION);
addSupportedService(GattService.UUID_SERVICE_BATTERY_SERVICE);
addSupportedService(PineTimeJFConstants.UUID_SERVICE_MUSIC_CONTROL);
addSupportedService(PineTimeJFConstants.UUID_SERVICE_WEATHER);
addSupportedService(PineTimeJFConstants.UUID_CHARACTERISTIC_ALERT_NOTIFICATION_EVENT);
addSupportedService(PineTimeJFConstants.UUID_SERVICE_MOTION);
@ -481,6 +493,9 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL
batteryInfoProfile.requestBatteryInfo(builder);
batteryInfoProfile.enableNotify(builder, true);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
builder.requestMtu(256);
}
return builder;
}
@ -651,7 +666,249 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL
@Override
public void onSendWeather(WeatherSpec weatherSpec) {
if (this.firmwareVersionMajor != 1 || this.firmwareVersionMinor <= 7) {
// Not supported
return;
} else {
if (weatherSpec.location != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Location.value)
.put("Location", weatherSpec.location)
.put("Altitude", 0)
.put("Latitude", 0)
.put("Longitude", 0)
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
// Current condition
if (weatherSpec.currentCondition != null) {
// We can't do anything with this?
}
// Current humidity
if (weatherSpec.currentHumidity > 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 6) // 6h this should be the weather provider's interval, really
.put("EventType", WeatherData.EventType.Humidity.value)
.put("Humidity", (int) weatherSpec.currentHumidity)
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
// Current temperature
if (weatherSpec.currentTemp >= -273.15) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 6) // 6h this should be the weather provider's interval, really
.put("EventType", WeatherData.EventType.Temperature.value)
.put("Temperature", (int) (weatherSpec.currentTemp * 100))
.put("DewPoint", (int) (-32768))
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
// 24h temperature forecast
if (weatherSpec.todayMinTemp >= -273.15 &&
weatherSpec.todayMaxTemp >= -273.15) { // Some sanity checking, should really be nullable
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 24) // 24h, because the temperature is today's
.put("EventType", WeatherData.EventType.Temperature.value)
.put("Temperature", (int) (((weatherSpec.todayMinTemp + weatherSpec.todayMaxTemp) / 2) * 100))
.put("DewPoint", (int) (-32768))
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
// Wind speed
if (weatherSpec.windSpeed != 0.0f) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Wind.value)
.put("SpeedMin", (int) (weatherSpec.windSpeed / 60 / 60 * 1000))
.put("SpeedMax", (int) (weatherSpec.windSpeed / 60 / 60 * 1000))
.put("DirectionMin", (int) (0.71 * weatherSpec.windDirection))
.put("DirectionMax", (int) (0.71 * weatherSpec.windDirection))
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
// Current weather condition
if (mapOpenWeatherConditionToPineTimePrecipitation(weatherSpec.currentConditionCode) != WeatherData.PrecipitationType.Length) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Precipitation.value)
.put("Type", (int) mapOpenWeatherConditionToPineTimePrecipitation(weatherSpec.currentConditionCode).value)
.put("Amount", (int) 0)
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
if (mapOpenWeatherConditionToPineTimeObscuration(weatherSpec.currentConditionCode) != WeatherData.ObscurationType.Length) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Obscuration.value)
.put("Type", (int) mapOpenWeatherConditionToPineTimeObscuration(weatherSpec.currentConditionCode).value)
.put("Amount", (int) 65535)
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
if (mapOpenWeatherConditionToPineTimeSpecial(weatherSpec.currentConditionCode) != WeatherData.SpecialType.Length) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Special.value)
.put("Type", mapOpenWeatherConditionToPineTimeSpecial(weatherSpec.currentConditionCode).value)
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
if (mapOpenWeatherConditionToCloudCover(weatherSpec.currentConditionCode) != -1) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
new CborEncoder(baos).encode(new CborBuilder()
.startMap() // This map is not fixed-size, which is not great, but it might come in a library update
.put("Timestamp", System.currentTimeMillis() / 1000L)
.put("Expires", 60 * 60 * 6) // 6h
.put("EventType", WeatherData.EventType.Clouds.value)
.put("Amount", (int) (mapOpenWeatherConditionToCloudCover(weatherSpec.currentConditionCode)))
.end()
.build()
);
} catch (Exception e) {
LOG.warn(String.valueOf(e));
}
byte[] encodedBytes = baos.toByteArray();
TransactionBuilder builder = createTransactionBuilder("WeatherData");
safeWriteToCharacteristic(builder,
PineTimeJFConstants.UUID_CHARACTERISTIC_WEATHER_DATA,
encodedBytes);
builder.queue(getQueue());
}
LOG.debug("Wrote weather data");
}
}
/**
@ -671,6 +928,8 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL
if (characteristic != null &&
(characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
builder.write(characteristic, data);
} else {
LOG.warn("Tried to write to a characteristic that did not exist or was not writable!");
}
}
@ -688,7 +947,7 @@ public class PineTimeJFSupport extends AbstractBTLEDeviceSupport implements DfuL
versionCmd.hwVersion = info.getHardwareRevision();
versionCmd.fwVersion = info.getFirmwareRevision();
if(versionCmd.fwVersion != null && !versionCmd.fwVersion.isEmpty()) {
if (versionCmd.fwVersion != null && !versionCmd.fwVersion.isEmpty()) {
// FW version format : "major.minor.patch". Ex : "0.8.2"
String[] tokens = StringUtils.split(versionCmd.fwVersion, ".");
if (tokens.length == 3) {