/* Copyright (C) 2024 Martin.JM 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.huawei.packets; import androidx.annotation.NonNull; import java.util.ArrayList; import java.util.Date; import java.util.List; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV; public class Weather { public static final byte id = 0x0f; public static class Settings { // WeatherSupport public boolean weatherSupported = false; public boolean windSupported = false; public boolean pm25Supported = false; public boolean temperatureSupported = false; public boolean locationNameSupported = false; public boolean currentTemperatureSupported = false; public boolean unitSupported = false; public boolean airQualityIndexSupported = false; // WeatherExtendedSupport public boolean timeSupported = false; public boolean sourceSupported = false; public boolean weatherIconSupported = false; // WeatherSunMoonSupport public boolean sunRiseSetSupported = false; public boolean moonPhaseSupported = false; // ExpandCapabilityRequest public boolean uvIndexSupported = false; } public enum WeatherIcon { // Also used for the text on the watch SUNNY, CLOUDY, OVERCAST, SHOWERS, THUNDERSTORMS, THUNDER_AND_HAIL, SLEET, LIGHT_RAIN, RAIN, HEAVY_RAIN, RAIN_STORM, HEAVY_RAIN_STORMS, SEVERE_RAIN_STORMS, SNOW_FLURRIES, LIGHT_SNOW, SNOW, HEAVY_SNOW, SNOWSTORMS, FOG, FREEZING_RAIN, DUST_STORM, LIGHT_TO_MODERATE_RAIN, MODERATE_TO_HEAVY_RAIN, HEAVY_TO_SEVERE_RAIN, HEAVY_TO_TORRENTIAL_RAIN, SEVERE_TO_TORRENTIAL_RAIN, LIGHT_TO_MODERATE_SNOW, MODERATE_TO_HEAVY_SNOW, HEAVY_SNOW_TO_BLIZZARD, DUST, SAND, SANDSTORMS, FREEZING, // misses small/non-moving icon HOT, // misses small/non-moving icon COLD, // misses small/non-moving icon WINDY, HAZY, UNKNOWN // Good to have probably } private static byte iconToByte(WeatherIcon weatherIcon) { switch (weatherIcon) { case SUNNY: return 0x00; case CLOUDY: return 0x01; case OVERCAST: return 0x02; case SHOWERS: return 0x03; case THUNDERSTORMS: return 0x04; case THUNDER_AND_HAIL: return 0x05; case SLEET: return 0x06; case LIGHT_RAIN: return 0x07; case RAIN: return 0x08; case HEAVY_RAIN: return 0x09; case RAIN_STORM: return 0x0a; case HEAVY_RAIN_STORMS: return 0x0b; case SEVERE_RAIN_STORMS: return 0x0c; case SNOW_FLURRIES: return 0x0d; case LIGHT_SNOW: return 0x0e; case SNOW: return 0x0f; case HEAVY_SNOW: return 0x10; case SNOWSTORMS: return 0x11; case FOG: return 0x12; case FREEZING_RAIN: return 0x13; case DUST_STORM: return 0x14; case LIGHT_TO_MODERATE_RAIN: return 0x15; case MODERATE_TO_HEAVY_RAIN: return 0x16; case HEAVY_TO_SEVERE_RAIN: return 0x17; case HEAVY_TO_TORRENTIAL_RAIN: return 0x18; case SEVERE_TO_TORRENTIAL_RAIN: return 0x19; case LIGHT_TO_MODERATE_SNOW: return 0x1a; case MODERATE_TO_HEAVY_SNOW: return 0x1b; case HEAVY_SNOW_TO_BLIZZARD: return 0x1c; case DUST: return 0x1d; case SAND: return 0x1e; case SANDSTORMS: return 0x1f; case FREEZING: return 0x20; case HOT: return 0x21; case COLD: return 0x22; case WINDY: return 0x23; case HAZY: return 0x35; default: return 0x63; // Any higher and the current weather breaks } } private static WeatherIcon byteToIcon(byte weatherIcon) { switch (weatherIcon) { case 0x00: return WeatherIcon.SUNNY; case 0x01: return WeatherIcon.CLOUDY; case 0x02: return WeatherIcon.OVERCAST; case 0x03: return WeatherIcon.SHOWERS; case 0x04: return WeatherIcon.THUNDERSTORMS; case 0x05: return WeatherIcon.THUNDER_AND_HAIL; case 0x06: return WeatherIcon.SLEET; case 0x07: return WeatherIcon.LIGHT_RAIN; case 0x08: return WeatherIcon.RAIN; case 0x09: return WeatherIcon.HEAVY_RAIN; case 0x0a: return WeatherIcon.RAIN_STORM; case 0x0b: return WeatherIcon.HEAVY_RAIN_STORMS; case 0x0c: return WeatherIcon.SEVERE_RAIN_STORMS; case 0x0d: return WeatherIcon.SNOW_FLURRIES; case 0x0e: return WeatherIcon.LIGHT_SNOW; case 0x0f: return WeatherIcon.SNOW; case 0x10: return WeatherIcon.HEAVY_SNOW; case 0x11: return WeatherIcon.SNOWSTORMS; case 0x12: return WeatherIcon.FOG; case 0x13: return WeatherIcon.FREEZING_RAIN; case 0x14: return WeatherIcon.DUST_STORM; case 0x15: return WeatherIcon.LIGHT_TO_MODERATE_RAIN; case 0x16: return WeatherIcon.MODERATE_TO_HEAVY_RAIN; case 0x17: return WeatherIcon.HEAVY_TO_SEVERE_RAIN; case 0x18: return WeatherIcon.HEAVY_TO_TORRENTIAL_RAIN; case 0x19: return WeatherIcon.SEVERE_TO_TORRENTIAL_RAIN; case 0x1a: return WeatherIcon.LIGHT_TO_MODERATE_SNOW; case 0x1b: return WeatherIcon.MODERATE_TO_HEAVY_SNOW; case 0x1c: return WeatherIcon.HEAVY_SNOW_TO_BLIZZARD; case 0x1d: return WeatherIcon.DUST; case 0x1e: return WeatherIcon.SAND; case 0x1f: return WeatherIcon.SANDSTORMS; case 0x20: return WeatherIcon.FREEZING; case 0x21: return WeatherIcon.HOT; case 0x22: return WeatherIcon.COLD; case 0x23: return WeatherIcon.WINDY; case 0x35: return WeatherIcon.HAZY; default: return WeatherIcon.UNKNOWN; } } public enum HuaweiTemperatureFormat { CELSIUS, FAHRENHEIT } private static byte temperatureFormatToByte(HuaweiTemperatureFormat temperatureFormat) { if (temperatureFormat == HuaweiTemperatureFormat.FAHRENHEIT) return 1; return 0; } public enum MoonPhase { NEW_MOON, WAXING_CRESCENT, FIRST_QUARTER, WAXING_GIBBOUS, FULL_MOON, WANING_GIBBOUS, THIRD_QUARTER, WANING_CRESCENT } public static MoonPhase degreesToMoonPhase(int degrees) { final int leeway = 6; // Give some leeway for the new moon, first quarter, full moon, and third quarter if (degrees < 0 || degrees > 360) return null; else if (degrees >= 360 - leeway || degrees <= leeway) return MoonPhase.NEW_MOON; else if (degrees < 90) return MoonPhase.WAXING_CRESCENT; else if (degrees <= 90 + leeway) return MoonPhase.FIRST_QUARTER; else if (degrees < 180 - leeway) return MoonPhase.WAXING_GIBBOUS; else if (degrees <= 180 + leeway) return MoonPhase.FULL_MOON; else if (degrees < 270 - leeway) return MoonPhase.WANING_GIBBOUS; else if (degrees <= 270 + leeway) return MoonPhase.THIRD_QUARTER; else return MoonPhase.WANING_CRESCENT; } private static byte moonPhaseToByte (MoonPhase moonPhase) { switch (moonPhase) { case NEW_MOON: return 1; case WAXING_CRESCENT: return 2; case FIRST_QUARTER: return 3; case WAXING_GIBBOUS: return 4; case FULL_MOON: return 5; case WANING_GIBBOUS: return 6; case THIRD_QUARTER: return 7; case WANING_CRESCENT: return 8; default: return -1; } } private static MoonPhase byteToMoonPhase(byte moonPhase) { switch (moonPhase) { case 1: return MoonPhase.NEW_MOON; case 2: return MoonPhase.WAXING_CRESCENT; case 3: return MoonPhase.FIRST_QUARTER; case 4: return MoonPhase.WAXING_GIBBOUS; case 5: return MoonPhase.FULL_MOON; case 6: return MoonPhase.WANING_GIBBOUS; case 7: return MoonPhase.THIRD_QUARTER; case 8: return MoonPhase.WANING_CRESCENT; default: return null; } } public enum ErrorCode { NETWORK_ERROR, GPS_PERMISSION_ERROR, WEATHER_DISABLED } private static byte errorCodeToByte(ErrorCode errorCode) { switch (errorCode) { case NETWORK_ERROR: return 0; case GPS_PERMISSION_ERROR: return 1; case WEATHER_DISABLED: return 2; } throw new RuntimeException(); // Shouldn't happen } public static class CurrentWeatherRequest extends HuaweiPacket { public static final byte id = 0x01; public CurrentWeatherRequest( ParamsProvider paramsProvider, Settings settings, WeatherIcon icon, Byte windDirection, Byte windSpeed, Byte lowestTemperature, Byte highestTemperature, Short pm25, // TODO: might be float? String locationName, Byte currentTemperature, HuaweiTemperatureFormat temperatureUnit, Short airQualityIndex, Integer observationTime, Float uvIndex, String sourceName ) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV(); HuaweiTLV tlv81 = new HuaweiTLV(); if (icon != null && settings.weatherIconSupported) { tlv81.put(0x02, iconToByte(icon)); } if (settings.windSupported) { short wind = 0; if (windSpeed != null) wind = (short) windSpeed; if (windDirection != null) { if (windDirection > 0) wind |= (short) (((short) (windDirection * 8 / 360)) << 8); else wind |= (short) (((short) (360 + windDirection) * 8 / 360) << 8); } tlv81.put(0x03, wind); } if (settings.weatherIconSupported || settings.windSupported) this.tlv.put(0x81, tlv81); if (lowestTemperature != null && highestTemperature != null && settings.temperatureSupported) { this.tlv.put(0x85, new HuaweiTLV() .put(0x06, lowestTemperature) .put(0x07, highestTemperature) ); } if (windDirection != null && windSpeed != null && settings.windSupported) this.tlv.put(0x03, (short) ((((short) windDirection) << 8) | ((short) windSpeed))); if (pm25 != null && settings.pm25Supported) this.tlv.put(0x04, pm25); if (locationName != null && settings.locationNameSupported) this.tlv.put(0x08, locationName); if (currentTemperature != null && settings.currentTemperatureSupported) this.tlv.put(0x09, currentTemperature); if (temperatureUnit != null && settings.unitSupported) this.tlv.put(0x0a, temperatureFormatToByte(temperatureUnit)); if (airQualityIndex != null && settings.airQualityIndexSupported) this.tlv.put(0x0b, airQualityIndex); if (observationTime != null && settings.timeSupported) this.tlv.put(0x0c, observationTime); if (sourceName != null && settings.sourceSupported) this.tlv.put(0x0e, sourceName); if (uvIndex != null && settings.uvIndexSupported) this.tlv.put(0x0f, uvIndex.byteValue()); this.isEncrypted = true; this.complete = true; } } public static class WeatherSupport { public static final byte id = 0x02; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01); this.isEncrypted = true; this.complete = true; } } public static class Response extends HuaweiPacket { public byte supportedBitmap = 0; public boolean weatherSupported = false; public boolean windSupported = false; public boolean pm25Supported = false; public boolean temperatureSupported = false; public boolean locationNameSupported = false; public boolean currentTemperatureSupported = false; public boolean unitSupported = false; public boolean airQualityIndexSupported = false; public Response(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; } @Override public void parseTlv() throws ParseException { if (!this.tlv.contains(0x01)) throw new MissingTagException(0x01); this.supportedBitmap = this.tlv.getByte(0x01); this.weatherSupported = (this.supportedBitmap & 0x01) != 0; this.windSupported = (this.supportedBitmap & 0x02) != 0; this.pm25Supported = (this.supportedBitmap & 0x04) != 0; this.temperatureSupported = (this.supportedBitmap & 0x08) != 0; this.locationNameSupported = (this.supportedBitmap & 0x10) != 0; this.currentTemperatureSupported = (this.supportedBitmap & 0x20) != 0; this.unitSupported = (this.supportedBitmap & 0x40) != 0; this.airQualityIndexSupported = (this.supportedBitmap & 0x80) != 0; } } } public static class WeatherDeviceRequest extends HuaweiPacket { public static final byte id = 0x04; public WeatherDeviceRequest(ParamsProvider paramsProvider, int responseValue) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01, responseValue); this.isEncrypted = false; this.complete = true; } } public static class WeatherUnitRequest extends HuaweiPacket { public static final byte id = 0x05; public WeatherUnitRequest(ParamsProvider paramsProvider, HuaweiTemperatureFormat temperatureFormat) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01, temperatureFormatToByte(temperatureFormat)); this.isEncrypted = true; this.complete = true; } } public static class WeatherExtendedSupport { public static final byte id = 0x06; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01); this.isEncrypted = true; this.complete = true; } } public static class Response extends HuaweiPacket { public short supportedBitmap = 0; public boolean timeSupported = false; public boolean sourceSupported = false; public boolean weatherIconSupported = false; public Response(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; } @Override public void parseTlv() throws ParseException { if (!this.tlv.contains(0x01)) throw new MissingTagException(0x01); this.supportedBitmap = this.tlv.getShort(0x01); this.timeSupported = (this.supportedBitmap & 0x01) != 0; this.sourceSupported = (this.supportedBitmap & 0x02) != 0; this.weatherIconSupported = (this.supportedBitmap & 0x04) != 0; } } } public static class WeatherErrorSimple { public static final byte id = 0x07; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider, ErrorCode errorCode) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01, errorCodeToByte(errorCode)); this.isEncrypted = true; this.complete = true; } } } public static class WeatherForecastData { public static final byte id = 0x08; public static class TimeData { public int timestamp; public WeatherIcon icon; public Byte temperature; @NonNull @Override public String toString() { String timestampStr = new Date(timestamp * 1000L).toString(); return "TimeData{" + "timestamp=" + timestamp + ", timestamp=" + timestampStr + ", icon=" + icon + ", temperature=" + temperature + '}'; } } public static class DayData { public int timestamp; public WeatherIcon icon; public Byte highTemperature; public Byte lowTemperature; public Integer sunriseTime; public Integer sunsetTime; public Integer moonRiseTime; public Integer moonSetTime; public MoonPhase moonPhase; @NonNull @Override public String toString() { String timestampStr = new Date(timestamp * 1000L).toString(); return "DayData{" + "timestamp=" + timestamp + ", timestamp=" + timestampStr + ", icon=" + icon + ", highTemperature=" + highTemperature + ", lowTemperature=" + lowTemperature + ", sunriseTime=" + sunriseTime + ", sunsetTime=" + sunsetTime + ", moonRiseTime=" + moonRiseTime + ", moonSetTime=" + moonSetTime + ", moonPhase=" + moonPhase + '}'; } } public static class Request extends HuaweiPacket { public Request( ParamsProvider paramsProvider, Settings settings, List timeDataList, List dayDataList ) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV(); if (timeDataList != null && !timeDataList.isEmpty()) { HuaweiTLV timeDataTlv = new HuaweiTLV(); for (TimeData timeData : timeDataList) { HuaweiTLV timeTlv = new HuaweiTLV(); timeTlv.put(0x03, timeData.timestamp); if (timeData.icon != null && settings.weatherIconSupported) timeTlv.put(0x04, iconToByte(timeData.icon)); if (timeData.temperature != null && (settings.temperatureSupported || settings.currentTemperatureSupported)) timeTlv.put(0x05, timeData.temperature); timeDataTlv.put(0x82, timeTlv); } this.tlv.put(0x81, timeDataTlv); } if (dayDataList != null && !dayDataList.isEmpty()) { HuaweiTLV dayDataTlv = new HuaweiTLV(); for (DayData dayData : dayDataList) { HuaweiTLV dayTlv = new HuaweiTLV(); dayTlv.put(0x12, dayData.timestamp); if (dayData.icon != null && settings.weatherIconSupported) dayTlv.put(0x13, iconToByte(dayData.icon)); if (settings.temperatureSupported) { if (dayData.highTemperature != null) dayTlv.put(0x14, dayData.highTemperature); if (dayData.lowTemperature != null) dayTlv.put(0x15, dayData.lowTemperature); } if (settings.sunRiseSetSupported) { if (dayData.sunriseTime != null && dayData.sunriseTime != 0) dayTlv.put(0x16, dayData.sunriseTime); if (dayData.sunsetTime != null && dayData.sunsetTime != 0) dayTlv.put(0x17, dayData.sunsetTime); if (dayData.moonRiseTime != null && dayData.moonRiseTime != 0) dayTlv.put(0x1a, dayData.moonRiseTime); if (dayData.moonSetTime != null && dayData.moonSetTime != 0) dayTlv.put(0x1b, dayData.moonSetTime); } if (dayData.moonPhase != null && settings.moonPhaseSupported) dayTlv.put(0x1e, moonPhaseToByte(dayData.moonPhase)); dayDataTlv.put(0x91, dayTlv); } this.tlv.put(0x90, dayDataTlv); } this.isEncrypted = true; this.isSliced = true; this.complete = true; } } public static class OutgoingRequest extends HuaweiPacket { List timeDataList; List dayDataList; public OutgoingRequest(ParamsProvider paramsProvider) { super(paramsProvider); this.complete = false; } @Override public void parseTlv() throws ParseException { timeDataList = new ArrayList<>(this.tlv.getObject(0x81).getObjects(0x82).size()); for (HuaweiTLV timeTlv : this.tlv.getObject(0x81).getObjects(0x82)) { TimeData timeData = new TimeData(); timeData.timestamp = timeTlv.getInteger(0x03); timeData.icon = byteToIcon(timeTlv.getByte(0x04)); timeData.temperature = timeTlv.getByte(0x05); timeDataList.add(timeData); } dayDataList = new ArrayList<>(this.tlv.getObject(0x90).getObjects(0x91).size()); for (HuaweiTLV dayTlv : this.tlv.getObject(0x90).getObjects(0x91)) { DayData dayData = new DayData(); dayData.timestamp = dayTlv.getInteger(0x12); dayData.icon = byteToIcon(dayTlv.getByte(0x13)); dayData.highTemperature = dayTlv.getByte(0x14); dayData.lowTemperature = dayTlv.getByte(0x15); dayData.sunriseTime = dayTlv.getInteger(0x16); dayData.sunsetTime = dayTlv.getInteger(0x17); dayData.moonRiseTime = dayTlv.getInteger(0x1a); dayData.moonSetTime = dayTlv.getInteger(0x1b); dayData.moonPhase = byteToMoonPhase(dayTlv.getByte(0x1e)); dayDataList.add(dayData); } } } } public static class WeatherStart { public static final byte id = 0x09; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01, (byte) 0x03); // TODO: find out what this means this.isEncrypted = true; this.complete = true; } } public static class Response extends HuaweiPacket { public int successCode = -1; public boolean success = false; public Response(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; } @Override public void parseTlv() throws ParseException { this.successCode = this.tlv.getInteger(0x7f); this.success = this.successCode == 0x000186A0 || this.successCode == 0x000186A3; } } } public static class WeatherSunMoonSupport { public static final byte id = 0x0a; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; this.tlv = new HuaweiTLV().put(0x01); this.isEncrypted = true; this.complete = true; } } public static class Response extends HuaweiPacket { public int supportedBitmap = 0; public boolean sunRiseSetSupported = false; public boolean moonPhaseSupported = false; public Response(ParamsProvider paramsProvider) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; } @Override public void parseTlv() throws ParseException { if (!this.tlv.contains(0x01)) throw new MissingTagException(0x01); this.supportedBitmap = this.tlv.getInteger(0x01); this.sunRiseSetSupported = (this.supportedBitmap & 0x01) != 0; this.moonPhaseSupported = (this.supportedBitmap & 0x02) != 0; } } } public static class WeatherErrorExtended { public static final byte id = 0x0c; public static class Request extends HuaweiPacket { public Request(ParamsProvider paramsProvider, ErrorCode errorCode, boolean serverError) { super(paramsProvider); this.serviceId = Weather.id; this.commandId = id; HuaweiTLV innerTlv = new HuaweiTLV(); innerTlv.put(0x02, errorCodeToByte(errorCode)); if (errorCode == ErrorCode.NETWORK_ERROR && serverError) innerTlv.put(0x03, (byte) 0x01); this.tlv = new HuaweiTLV().put(0x81, innerTlv); this.isEncrypted = true; this.complete = true; } } } }