diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java index d7943c2a9..c31610e16 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFConstants.java @@ -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"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java index 57f66a2fe..000d076ca 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/PineTimeJFCoordinator.java @@ -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 diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/weather/WeatherData.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/weather/WeatherData.java new file mode 100644 index 000000000..84915a2ff --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/pinetime/weather/WeatherData.java @@ -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 . */ +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. + *

+ * Even if they're absolutely not mutually exclusive. + *

+ * 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. + * + *

+ * Even if they're absolutely not mutually exclusive. + *

+ * 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. + *

+ * Even if they're absolutely not mutually exclusive. + * + *

+ * 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. + *

+ * Even if they're absolutely not mutually exclusive. + * + *

+ * 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 + *

+ * 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 + *

+ * 32 bits ought to be enough for everyone + *

+ * 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 + *

+ * In order to represent bursts of wind instead of constant wind, + * you have minimum and maximum speeds. + *

+ * 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 + *

+ * As it's annoying to figure out the dewpoint on the watch, + * please send it from the companion + *

+ * 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 + *

+ * 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. + *

+ * 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 + *

+ * 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 + *

+ * If this needs further enforced standardization, pull requests are welcome + */ + static public class AirQuality extends TimelineHeader { + /** + * The name of the pollution + *

+ * for the sake of better compatibility with watchapps + * that might want to use this data for say visuals + * don't localize the name. + *

+ * Ideally watchapp itself localizes the name, if it's at all needed. + *

+ * 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. + *

+ * See more: + * https://ec.europa.eu/environment/air/quality/standards.htm + * http://www.ourair.org/wp-content/uploads/2012-aaqs2.pdf + *

+ * 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 + *

+ * List is not comprehensive, should be improved. + * The current ones are what watchapps assume. + *

+ * Note: ppb and ppm to concentration should be calculated on the companion, using + * the correct formula (taking into account temperature and air pressure) + *

+ * Note2: The amount is off by times 100, for two decimal places of precision. + * E.g. 54.32µg/m³ is 5432 + */ + int amount; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java index 1950833ee..1884c0d3c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pinetime/PineTimeJFSupport.java @@ -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 . */ 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) {