/* Copyright (C) 2022-2024 Andreas Shimokawa, Arjan Schrijver, José Rebelo, rany 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.service.devices.huami.zeppos; import android.location.Location; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import net.e175.klaus.solarpositioning.DeltaT; import net.e175.klaus.solarpositioning.SPA; import net.e175.klaus.solarpositioning.SunriseTransitSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.lang.reflect.Type; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions; import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.webview.CurrentPosition; /** * The weather models that the bands expect as an http response to weather requests. Base URL usually * is https://api-mifit.huami.com. */ public class ZeppOsWeather { private static final Logger LOG = LoggerFactory.getLogger(ZeppOsWeather.class); private static final Gson GSON = new GsonBuilder() .serializeNulls() .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") // for pubTimes .registerTypeAdapter(LocalDate.class, new LocalDateSerializer()) // These serializers keep the fields in order. Not sure if this is needed, but the debug // logs show some errors when deserializing the json payloads. .registerTypeAdapter(ForecastResponse.class, new ForecastResponse.Serializer()) .registerTypeAdapter(MoonRiseSet.class, new MoonRiseSet.Serializer()) .registerTypeAdapter(Range.class, new Range.Serializer()) .registerTypeAdapter(IndexResponse.class, new IndexResponse.Serializer()) .registerTypeAdapter(IndexEntry.class, new IndexEntry.Serializer()) .registerTypeAdapter(CurrentResponse.class, new CurrentResponse.Serializer()) .registerTypeAdapter(CurrentWeatherModel.class, new CurrentWeatherModel.Serializer()) .registerTypeAdapter(AqiModel.class, new AqiModel.Serializer()) .registerTypeAdapter(UnitValue.class, new UnitValue.Serializer()) .registerTypeAdapter(HourlyResponse.class, new HourlyResponse.Serializer()) .registerTypeAdapter(AlertsResponse.class, new AlertsResponse.Serializer()) .registerTypeAdapter(TideResponse.class, new TideResponse.Serializer()) .registerTypeAdapter(TideDataEntry.class, new TideDataEntry.Serializer()) .registerTypeAdapter(TideTableEntry.class, new TideTableEntry.Serializer()) .registerTypeAdapter(TideHourlyEntry.class, new TideHourlyEntry.Serializer()) .create(); public static Response handleHttpRequest(final String path, final Map query) { final WeatherSpec weatherSpec = Weather.getInstance().getWeatherSpec(); if (weatherSpec == null) { LOG.error("No weather in weather instance"); return new ZeppOsWeather.ErrorResponse(404, -2001, "Not found"); } switch (path) { case "/weather/v2/forecast": final int forecastDays = getQueryNum(query, "days", 10); return new ForecastResponse(weatherSpec, forecastDays); case "/weather/index": final int indexDays = getQueryNum(query, "days", 3); return new IndexResponse(weatherSpec, indexDays); case "/weather/current": return new CurrentResponse(weatherSpec); case "/weather/forecast/hourly": final int hours = getQueryNum(query, "hours", 72); return new HourlyResponse(weatherSpec, hours); case "/weather/alerts": return new AlertsResponse(weatherSpec); case "/weather/tide": final int tideDays = getQueryNum(query, "days", 10); return new TideResponse(weatherSpec, tideDays); } LOG.error("Unknown weather path {}", path); return new ZeppOsWeather.ErrorResponse(404, -2001, "Not found"); } private static int getQueryNum(final Map query, final String key, final int defaultValue) { final String daysStr = query.get(key); if (daysStr != null) { return Integer.parseInt(daysStr); } else { return defaultValue; } } private static class RawJsonStringResponse extends Response { private final String content; public RawJsonStringResponse(final String content) { this.content = content; } public String toJson() { return content; } } public static class ErrorResponse extends Response { private final int httpStatusCode; private final int errorCode; private final String message; public ErrorResponse(final int httpStatusCode, final int errorCode, final String message) { this.httpStatusCode = httpStatusCode; this.errorCode = errorCode; this.message = message; } @Override public int getHttpStatusCode() { return httpStatusCode; } public int getErrorCode() { return errorCode; } public String getMessage() { return message; } } public static abstract class Response { public int getHttpStatusCode() { return 200; } public String toJson() { return GSON.toJson(this); } } // /weather/v2/forecast // // locale=zh_CN // deviceSource=11 // days=10 // isGlobal=true // locationKey=00.000,-0.000,xiaomi_accu:000000 public static class ForecastResponse extends Response { public Date pubTime; public List humidity = new ArrayList<>(); // int public List temperature = new ArrayList<>(); // ints public List weather = new ArrayList<>(); public List windDirection = new ArrayList<>(); public List sunRiseSet = new ArrayList<>(); public List windSpeed = new ArrayList<>(); public MoonRiseSet moonRiseSet = new MoonRiseSet(); public List airQualities = new ArrayList<>(); public ForecastResponse(final WeatherSpec weatherSpec, final int days) { final int actualDays = Math.min(weatherSpec.forecasts.size(), days - 1); // leave one slot for the first day pubTime = new Date(weatherSpec.timestamp * 1000L); final Calendar calendar = GregorianCalendar.getInstance(); calendar.setTime(pubTime); final Location lastKnownLocation = new CurrentPosition().getLastKnownLocation(); final GregorianCalendar sunriseDate = new GregorianCalendar(); sunriseDate.setTime(calendar.getTime()); // First one is for the current day temperature.add(new Range(weatherSpec.todayMinTemp - 273, weatherSpec.todayMaxTemp - 273)); final String currentWeatherCode = String.valueOf(mapToZeppOsWeatherCode(weatherSpec.currentConditionCode)); weather.add(new Range(currentWeatherCode, currentWeatherCode)); if (weatherSpec.sunRise != 0 && weatherSpec.sunSet != 0) { sunRiseSet.add(getSunriseSunset(new Date(weatherSpec.sunRise * 1000L), new Date(weatherSpec.sunSet * 1000L))); } else if (weatherSpec.getLocation() != null) { sunRiseSet.add(getSunriseSunset(sunriseDate, weatherSpec.getLocation())); } else { sunRiseSet.add(getSunriseSunset(sunriseDate, lastKnownLocation)); } sunriseDate.add(Calendar.DAY_OF_MONTH, 1); windDirection.add(new Range(weatherSpec.windDirection, weatherSpec.windDirection)); windSpeed.add(new Range(Math.round(weatherSpec.windSpeed), Math.round(weatherSpec.windSpeed))); moonRiseSet.add(weatherSpec.moonRise, weatherSpec.moonSet, weatherSpec.moonPhase); for (int i = 0; i < actualDays; i++) { final WeatherSpec.Daily forecast = weatherSpec.forecasts.get(i); temperature.add(new Range(forecast.minTemp - 273, forecast.maxTemp - 273)); final String weatherCode = String.valueOf(mapToZeppOsWeatherCode(forecast.conditionCode)); weather.add(new Range(weatherCode, weatherCode)); if (forecast.sunRise != 0 && forecast.sunSet != 0) { sunRiseSet.add(getSunriseSunset(new Date(forecast.sunRise * 1000L), new Date(forecast.sunSet * 1000L))); } else { sunRiseSet.add(getSunriseSunset(sunriseDate, lastKnownLocation)); } sunriseDate.add(Calendar.DAY_OF_MONTH, 1); if (forecast.windDirection != -1) { windDirection.add(new Range(forecast.windDirection, forecast.windDirection)); } else { windDirection.add(new Range(0, 0)); } if (forecast.windSpeed != -1) { windSpeed.add(new Range(Math.round(forecast.windSpeed), Math.round(forecast.windSpeed))); } else { windSpeed.add(new Range(0, 0)); } moonRiseSet.add(forecast.moonRise, forecast.moonSet, forecast.moonPhase); } } private Range getSunriseSunset(final GregorianCalendar date, final Location location) { final SunriseTransitSet sunriseTransitSet = SPA.calculateSunriseTransitSet( date.toZonedDateTime(), location.getLatitude(), location.getLongitude(), DeltaT.estimate(date.toZonedDateTime().toLocalDate()) ); if (sunriseTransitSet.getSunrise() != null && sunriseTransitSet.getSunset() != null) { return getSunriseSunset( Date.from(sunriseTransitSet.getSunrise().toInstant()), Date.from(sunriseTransitSet.getSunset().toInstant()) ); } return getSunriseSunset(new Date(), new Date()); } private Range getSunriseSunset(final Date sunRise, final Date sunSet) { final SimpleDateFormat sunRiseSetSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); sunRiseSetSdf.setTimeZone(TimeZone.getDefault()); final String from = sunRiseSetSdf.format(sunRise); final String to = sunRiseSetSdf.format(sunSet); return new Range(from, to); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final ForecastResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("pubTime", context.serialize(obj.pubTime)); object.add("humidity", context.serialize(obj.humidity)); object.add("temperature", context.serialize(obj.temperature)); object.add("weather", context.serialize(obj.weather)); object.add("windDirection", context.serialize(obj.windDirection)); object.add("sunRiseSet", context.serialize(obj.sunRiseSet)); object.add("windSpeed", context.serialize(obj.windSpeed)); object.add("moonRiseSet", context.serialize(obj.moonRiseSet)); object.add("airQualities", context.serialize(obj.airQualities)); return object; } } } private static class MoonRiseSet { public List moonPhaseValue = new ArrayList<>(); // numbers? 20 21 23... public List moonRise = new ArrayList<>(); // yyyy-MM-dd HH:mm:ss public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final MoonRiseSet obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("moonPhaseValue", context.serialize(obj.moonPhaseValue)); object.add("moonRise", context.serialize(obj.moonRise)); return object; } } public void add(final int rise, final int set, final int phase) { moonPhaseValue.add(String.valueOf(phase)); final SimpleDateFormat moonRiseSetSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); moonRiseSetSdf.setTimeZone(TimeZone.getDefault()); final String from = moonRiseSetSdf.format(new Date(rise * 1000L)); final String to = moonRiseSetSdf.format(new Date(set * 1000L)); moonRise.add(new Range(from, to)); } } private static class Range { public String from; public String to; public Range(final String from, final String to) { this.from = from; this.to = to; } public Range(final int from, final int to) { this.from = String.valueOf(from); this.to = String.valueOf(to); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final Range obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("from", context.serialize(obj.from)); object.add("to", context.serialize(obj.to)); return object; } } } // /weather/index // // locale=zh_CN // deviceSource=11 // days=3 // isGlobal=true // locationKey=00.000,-0.000,xiaomi_accu:000000 public static class IndexResponse extends Response { public Date pubTime; public List dataList = new ArrayList<>(); public IndexResponse(final WeatherSpec weatherSpec, final int days) { pubTime = new Date(weatherSpec.timestamp * 1000L); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final IndexResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("pubTime", context.serialize(obj.pubTime)); object.add("dataList", context.serialize(obj.dataList)); return object; } } } private static class IndexEntry { public LocalDate date; public String osi; // int public String uvi; // int public Object pai; public String cwi; // int public String fi; // int public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final IndexEntry obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("date", context.serialize(obj.date)); object.add("osi", context.serialize(obj.osi)); object.add("uvi", context.serialize(obj.uvi)); object.add("pai", context.serialize(obj.pai)); object.add("cwi", context.serialize(obj.cwi)); object.add("fi", context.serialize(obj.fi)); return object; } } } // /weather/current // // locale=zh_CN // deviceSource=11 // isGlobal=true // locationKey=00.000,-0.000,xiaomi_accu:000000 public static class CurrentResponse extends Response { public CurrentWeatherModel currentWeatherModel; public AqiModel aqiModel; public CurrentResponse(final WeatherSpec weatherSpec) { this.currentWeatherModel = new CurrentWeatherModel(weatherSpec); this.aqiModel = new AqiModel(weatherSpec); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final CurrentResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("currentWeatherModel", context.serialize(obj.currentWeatherModel)); object.add("aqiModel", context.serialize(obj.aqiModel)); return object; } } } private static class CurrentWeatherModel { public UnitValue humidity; public UnitValue pressure; public Date pubTime; public UnitValue temperature; public String uvIndex; public UnitValue visibility; public String weather; public Wind wind; public CurrentWeatherModel(final WeatherSpec weatherSpec) { humidity = new UnitValue(Unit.PERCENTAGE, weatherSpec.currentHumidity); pressure = new UnitValue(Unit.PRESSURE_MB, Math.round(weatherSpec.pressure)); pubTime = new Date(weatherSpec.timestamp * 1000L); temperature = new UnitValue(Unit.TEMPERATURE_C, weatherSpec.currentTemp - 273); uvIndex = String.valueOf(Math.round(weatherSpec.uvIndex)); visibility = new UnitValue(Unit.KM, Math.round(weatherSpec.visibility / 1000)); weather = String.valueOf(mapToZeppOsWeatherCode(weatherSpec.currentConditionCode)); wind = new Wind(weatherSpec.windDirection, Math.round(weatherSpec.windSpeed)); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final CurrentWeatherModel obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("humidity", context.serialize(obj.humidity)); object.add("pressure", context.serialize(obj.pressure)); object.add("pubTime", context.serialize(obj.pubTime)); object.add("temperature", context.serialize(obj.temperature)); object.add("uvIndex", context.serialize(obj.uvIndex)); object.add("visibility", context.serialize(obj.visibility)); object.add("weather", context.serialize(obj.weather)); object.add("wind", context.serialize(obj.wind)); return object; } } } private static class AqiModel { public String aqi; // int public String co; // float public String no2; // float public String o3; // float public String pm10; // int public String pm25; // int public Date pubTime; // 2023-05-14T12:00:00-0400 public String so2; // float public AqiModel(final WeatherSpec weatherSpec) { if (weatherSpec.airQuality == null) { return; } this.aqi = String.valueOf(weatherSpec.airQuality.aqi); this.co = String.format(Locale.ROOT, "%.1f", (float) weatherSpec.airQuality.coAqi); this.no2 = String.format(Locale.ROOT, "%.1f", (float) weatherSpec.airQuality.no2Aqi); this.o3 = String.format(Locale.ROOT, "%.1f", (float) weatherSpec.airQuality.o3Aqi); this.pm10 = String.valueOf(Math.round((float) weatherSpec.airQuality.pm10Aqi)); this.pm25 = String.valueOf(Math.round((float) weatherSpec.airQuality.pm25Aqi)); this.pubTime = new Date(weatherSpec.timestamp * 1000L); this.so2 = String.format(Locale.ROOT, "%.1f", (float) weatherSpec.airQuality.so2Aqi); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final AqiModel obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("aqi", context.serialize(obj.aqi)); object.add("co", context.serialize(obj.co)); object.add("no2", context.serialize(obj.no2)); object.add("o3", context.serialize(obj.o3)); object.add("pm10", context.serialize(obj.pm10)); object.add("pm25", context.serialize(obj.pm25)); object.add("pubTime", context.serialize(obj.pubTime)); object.add("so2", context.serialize(obj.so2)); return object; } } } // /weather/tide // // locale=en_US // deviceSource=7930113 // days=10 // isGlobal=true // latitude=00.000 // longitude=-00.000 private static class TideResponse extends Response { public Date pubTime; public String poiName; // poi tide station name public String poiKey; // lat,lon,POI_ID public List tideData = new ArrayList<>(); public TideResponse(final WeatherSpec weatherSpec, int tideDays) { pubTime = new Date(weatherSpec.timestamp * 1000L); // Fill all entries, even if without data final Calendar pubTimeDate = Calendar.getInstance(); pubTimeDate.setTime(pubTime); LocalDate tideDate = LocalDate.of(pubTimeDate.get(Calendar.YEAR), pubTimeDate.get(Calendar.MONTH) + 1, pubTimeDate.get(Calendar.DAY_OF_MONTH)); for (int i = 0; i < tideDays; i++, tideDate = tideDate.plusDays(1)) { final TideDataEntry tideDataEntry = new TideDataEntry(); tideDataEntry.date = tideDate; tideData.add(tideDataEntry); } } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final TideResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("pubTime", context.serialize(obj.pubTime)); object.add("poiName", context.serialize(obj.poiName)); object.add("poiKey", context.serialize(obj.poiKey)); object.add("tideData", context.serialize(obj.tideData)); return object; } } } private static class TideDataEntry { public LocalDate date; public List tideTable = new ArrayList<>(); public List tideHourly = new ArrayList<>(); public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final TideDataEntry obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("date", context.serialize(obj.date)); object.add("tideTable", context.serialize(obj.tideTable)); object.add("tideHourly", context.serialize(obj.tideHourly)); return object; } } } private static class TideTableEntry { public Date fxTime; // pubTime format public String height; // float, x.xx public String type; // H / L public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final TideTableEntry obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("fxTime", context.serialize(obj.fxTime)); object.add("height", context.serialize(obj.height)); object.add("type", context.serialize(obj.type)); return object; } } } private static class TideHourlyEntry { public Date fxTime; // pubTime format public String height; // float, x.xx public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final TideHourlyEntry obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("fxTime", context.serialize(obj.fxTime)); object.add("height", context.serialize(obj.height)); return object; } } } private enum Unit { PRESSURE_MB("mb"), PERCENTAGE("%"), TEMPERATURE_C("℃"), // e2 84 83 in UTF-8 WIND_DEGREES("°"), // c2 b0 in UTF-8 KM("km"), KPH("km/h"), ; private final String value; Unit(final String value) { this.value = value; } public String getValue() { return value; } } private static class UnitValue { public String unit; public String value; public UnitValue(final Unit unit, final String value) { this.unit = unit.getValue(); this.value = value; } public UnitValue(final Unit unit, final int value) { this.unit = unit.getValue(); this.value = String.valueOf(value); } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final UnitValue obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("unit", context.serialize(obj.unit)); object.add("value", context.serialize(obj.value)); return object; } } } private static class Wind { public UnitValue direction; public UnitValue speed; public Wind(final int direction, final int speed) { this.direction = new UnitValue(Unit.WIND_DEGREES, direction); this.speed = new UnitValue(Unit.KPH, Math.round(speed)); } } // /weather/forecast/hourly // // locale=zh_CN // deviceSource=11 // hourly=72 // isGlobal=true // locationKey=00.000,-0.000,xiaomi_accu:000000 public static class HourlyResponse extends Response { public Date pubTime; // One entry in each list per hour public List weather = new ArrayList<>(); public List temperature = new ArrayList<>(); public List humidity = new ArrayList<>(); public List fxTime = new ArrayList<>(); // pubTime format public List windDirection = new ArrayList<>(); public List windSpeed = new ArrayList<>(); public List windScale = new ArrayList<>(); // each element in the form of 1-2 public HourlyResponse(final WeatherSpec weatherSpec, final int hours) { pubTime = new Date(weatherSpec.timestamp * 1000L); if (weatherSpec.hourly == null || weatherSpec.hourly.isEmpty()) { // We don't have hourly data, but some devices refuse to open the weather app without it final Calendar fxTimeCalendar = Calendar.getInstance(); fxTimeCalendar.setTime(pubTime); fxTimeCalendar.set(Calendar.MINUTE, 0); fxTimeCalendar.set(Calendar.SECOND, 0); fxTimeCalendar.set(Calendar.MILLISECOND, 0); fxTimeCalendar.add(Calendar.HOUR, 1); for (int i = 0; i < hours; i++) { weather.add(String.valueOf(mapToZeppOsWeatherCode(weatherSpec.currentConditionCode))); temperature.add(String.valueOf(weatherSpec.currentTemp - 273)); humidity.add(String.valueOf(weatherSpec.currentHumidity)); fxTime.add(fxTimeCalendar.getTime()); windDirection.add(String.valueOf(weatherSpec.windDirection)); windSpeed.add(String.valueOf(Math.round(weatherSpec.windSpeed))); windScale.add("1-2"); fxTimeCalendar.add(Calendar.HOUR, 1); } } else { int i = 0; for (final WeatherSpec.Hourly hourly : weatherSpec.hourly) { if (hourly.timestamp < weatherSpec.timestamp) { continue; } weather.add(String.valueOf(mapToZeppOsWeatherCode(hourly.conditionCode))); temperature.add(String.valueOf(hourly.temp - 273)); humidity.add(String.valueOf(hourly.humidity)); fxTime.add(new Date(hourly.timestamp * 1000L)); windDirection.add(String.valueOf(hourly.windDirection)); windSpeed.add(String.valueOf(Math.round(hourly.windSpeed))); windScale.add(hourly.windSpeedAsBeaufort() + "-" + hourly.windSpeedAsBeaufort()); if (++i >= hours) { break; } } } } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final HourlyResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("pubTime", context.serialize(obj.pubTime)); object.add("weather", context.serialize(obj.weather)); object.add("temperature", context.serialize(obj.temperature)); object.add("humidity", context.serialize(obj.humidity)); object.add("fxTime", context.serialize(obj.fxTime)); object.add("windDirection", context.serialize(obj.windDirection)); object.add("windSpeed", context.serialize(obj.windSpeed)); object.add("windScale", context.serialize(obj.windScale)); return object; } } } // /weather/alerts // // locale=zh_CN // deviceSource=11 // days=3 // isGlobal=true // locationKey=00.000,-0.000,xiaomi_accu:000000 public static class AlertsResponse extends Response { public List alerts = new ArrayList<>(); public AlertsResponse(final WeatherSpec weatherSpec) { } public static class Serializer implements JsonSerializer { @Override public JsonElement serialize(final AlertsResponse obj, final Type type, final JsonSerializationContext context) { final JsonObject object = new JsonObject(); object.add("alerts", context.serialize(obj.alerts)); return object; } } } private static class LocalDateSerializer implements JsonSerializer { @Override public JsonElement serialize(final LocalDate src, final Type typeOfSrc, final JsonSerializationContext context) { // Serialize as "YYYY-MM-DD" string return new JsonPrimitive(src.format(DateTimeFormatter.ISO_LOCAL_DATE)); } } public static final int SUNNY = 0; public static final int SOME_CLOUDS = 1; public static final int CLOUDY = 2; public static final int RAIN_WITH_SUN = 3; public static final int THUNDERSTORM = 4; public static final int HAIL = 5; public static final int SLEET = 6; public static final int LIGHT_RAIN = 7; public static final int MODERATE_RAIN = 8; public static final int HEAVY_RAIN = 9; public static final int RAINSTORM = 10; public static final int HEAVY_RAINSTORM = 11; public static final int EXTRAORDINARY_RAINSTORM = 12; public static final int SNOW_SHOWER_WITH_SOME_SUN = 13; public static final int LIGHT_SNOW = 14; public static final int MODERATE_SNOW = 15; public static final int HEAVY_SNOW = 16; public static final int SNOWSTORM = 17; public static final int FOG = 18; public static final int FREEZING_RAIN = 19; public static final int SANDSTORM = 20; public static final int LIGHT_TO_MODERATE_RAIN = 21; public static final int MODERATE_TO_HEAVY_RAIN = 22; public static final int HEAVY_RAIN_TO_RAINSTORM = 23; public static final int RAINSTORM_TO_HEAVY_RAIN = 24; public static final int HEAVY_TO_SEVERE_STORM = 25; public static final int LIGHT_TO_MODERATE_SNOW = 26; public static final int MODERATE_TO_HEAVY_SNOW = 27; public static final int HEAVY_SNOW_TO_SNOWSTORM = 28; public static final int DUST = 29; public static final int SAND_BLOWING = 30; public static final int STRONG_SANDSTORM = 31; public static final int DENSE_FOG = 32; public static final int SNOW = 33; public static int mapToZeppOsWeatherCode(final int openWeatherMapCondition) { // openweathermap.org conditions: // http://openweathermap.org/weather-conditions switch (openWeatherMapCondition) { case 511: //freezing rain: //13d return FREEZING_RAIN; case 731: //sand/dust whirls: //50d case 751: //sand: //50d return SAND_BLOWING; case 761: //dust: //50d case 762: //volcanic ash: //50d return DUST; case 741: //fog: //50d return DENSE_FOG; default: return HuamiWeatherConditions.mapToAmazfitBipWeatherCode(openWeatherMapCondition) & 0xff; } } }