mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-12-25 10:05:49 +01:00
Xiaomi: rework weather service
This commit is contained in:
parent
033e977491
commit
a5ff360497
@ -568,10 +568,6 @@ public abstract class XiaomiCoordinator extends AbstractBLEDeviceCoordinator {
|
||||
return new Prefs(GBApplication.getDeviceSpecificSharedPrefs(device.getAddress()));
|
||||
}
|
||||
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean supports(final GBDevice device, final String feature) {
|
||||
return getPrefs(device).getBoolean(feature, false);
|
||||
}
|
||||
|
@ -0,0 +1,186 @@
|
||||
/* Copyright (C) 2017-2024 Andreas Shimokawa, Yoran Vulker
|
||||
|
||||
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 <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.xiaomi;
|
||||
|
||||
public class XiaomiWeatherConditions {
|
||||
// Most of these conditions are the same on Huami and ZeppOS (see HuamiWeatherConditions), but
|
||||
// some have been renamed and a few were added. Nonetheless, some more detailed conditions,
|
||||
// such as drizzle, tornado, and tropical storm, do not have an equivalent
|
||||
// These conditions were mapped by sending the current condition and iterating over the condition
|
||||
// code.
|
||||
// At least the Xiaomi Watch S1 Active is known to crash if the condition cannot be mapped
|
||||
// to an icon while shown (above 33), so take that into account when adding new conditions below.
|
||||
public static final byte CLEAR_SKY = 0;
|
||||
public static final byte CLOUDY = 1; // may appear with a moon on some models
|
||||
public static final byte OVERCAST = 2;
|
||||
public static final byte SHOWER = 3; // 'light rain' (two drops) on MB8P
|
||||
public static final byte THUNDERSTORM = 4;
|
||||
public static final byte HAIL = 5;
|
||||
public static final byte SLEET = 6;
|
||||
public static final byte LIGHT_RAIN = 7; // 'light rain' (two drops) on MB8P
|
||||
public static final byte MODERATE_RAIN = 8; // 'moderate rain' (three drops) on MB8P
|
||||
public static final byte HEAVY_RAINFALL = 9; // 'heavy rain' (four drops) on MB8P
|
||||
public static final byte RAINSTORM = 10; // 'heavy rain' (four drops) on MB8P
|
||||
public static final byte DOWNPOUR = 11; // 'heavy rain' (four drops) on MB8P
|
||||
public static final byte HEAVY_RAINSTORM = 12; // 'heavy rain' (four drops) on MB8P
|
||||
public static final byte SNOW_SHOWERS = 13; // 'light snow' (1 flake) on MB8P
|
||||
public static final byte LIGHT_SNOW = 14; // 'light snow' (1 flake) on MB8P
|
||||
public static final byte MODERATE_SNOW = 15; // 'moderate snow' (1 large and 1 small flake) on MB8P
|
||||
public static final byte HEAVY_SNOW = 16; // 'heavy snow' (multiple flakes}) on MB8P
|
||||
public static final byte BLIZZARD = 17; // 'heavy snow' (multiple flakes}) on MB8P
|
||||
public static final byte MIST = 18;
|
||||
public static final byte FREEZING_RAIN = 19; // 'sleet' on MB8P
|
||||
public static final byte SANDSTORM = 20; // sandstorm
|
||||
public static final byte LIGHT_TO_MODERATE_RAIN = 21; // 'moderate rain' on MB8P
|
||||
public static final byte MODERATE_TO_HEAVY_RAIN = 22; // 'heavy rain' on MB8P
|
||||
public static final byte HEAVY_RAIN_TO_RAINSTORM = 23; // 'heavy rain' on MB8P
|
||||
public static final byte RAINSTORM_TO_DOWNPOUR = 24; // 'heavy rain' on MB8P
|
||||
public static final byte DOWNPOUR_TO_HEAVY_RAINSTORM = 25; // 'heavy rain' on MB8P
|
||||
public static final byte LIGHT_TO_MODERATE_SNOW = 26; // 'moderate snow' on MB8P
|
||||
public static final byte MODERATE_TO_HEAVY_SNOW = 27; // 'heavy snow' on MB8P
|
||||
public static final byte HEAVY_SNOW_TO_BLIZZARD = 28; // 'heavy snow' on MB8P
|
||||
public static final byte DUST = 29; // 'sandstorm' on MB8P
|
||||
public static final byte WINDY = 30; // 'sandstorm' on MB8P
|
||||
public static final byte STRONG_SANDSTORM = 31; // 'sandstorm' on MB8P
|
||||
public static final byte MODERATE_FOG = 32; // 'mist' on MB8P
|
||||
public static final byte SNOW = 33; // 'light snow' on MB8P
|
||||
// Please read the comment above before adding new condition codes here
|
||||
|
||||
public static byte convertOwmConditionToXiaomi(int openWeatherMapCondition) {
|
||||
// openweathermap.org conditions:
|
||||
// http://openweathermap.org/weather-conditions
|
||||
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 212: // heavy thunderstorm
|
||||
case 221: // ragged thunderstorm
|
||||
case 230: // thunderstorm with light drizzle
|
||||
case 231: // thunderstorm with drizzle
|
||||
case 232: // thunderstorm with heavy drizzle
|
||||
return 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
|
||||
// for lack of a better transcription, drizzle -> light rain
|
||||
//Group 5xx: Rain
|
||||
case 500: // light rain
|
||||
return LIGHT_RAIN;
|
||||
case 501: // moderate rain
|
||||
return MODERATE_RAIN;
|
||||
case 502: // heavy intensity rain
|
||||
return RAINSTORM;
|
||||
case 503: // very heavy rain
|
||||
return DOWNPOUR;
|
||||
case 504: // extreme rain
|
||||
return HEAVY_RAINSTORM;
|
||||
case 511: // freezing rain
|
||||
return FREEZING_RAIN;
|
||||
case 520: // light intensity shower rain
|
||||
case 521: // shower rain
|
||||
return SHOWER;
|
||||
case 522: // heavy intensity shower rain
|
||||
case 531: // ragged shower rain
|
||||
return HEAVY_RAINFALL;
|
||||
|
||||
//Group 6xx: Snow
|
||||
case 600: // light snow
|
||||
return LIGHT_SNOW;
|
||||
case 601: // snow
|
||||
return MODERATE_SNOW;
|
||||
case 602: // heavy snow
|
||||
return HEAVY_SNOW;
|
||||
case 611: // sleet
|
||||
case 612: // shower sleet
|
||||
return SLEET;
|
||||
case 615: // light rain and snow
|
||||
case 616: // rain and snow
|
||||
return SNOW_SHOWERS;
|
||||
case 620: // light shower snow
|
||||
case 621: // shower snow
|
||||
return HEAVY_SNOW;
|
||||
case 622: // heavy shower snow
|
||||
return BLIZZARD;
|
||||
|
||||
//Group 7xx: Atmosphere
|
||||
case 701: // mist
|
||||
case 711: // smoke
|
||||
case 721: // haze
|
||||
case 731: // sand/dust whirls
|
||||
case 741: // fog
|
||||
return MIST;
|
||||
case 751: // sand
|
||||
return SANDSTORM;
|
||||
case 761: // dust
|
||||
case 762: // volcanic ash
|
||||
case 771: // squalls
|
||||
return DUST;
|
||||
case 781: // tornado
|
||||
case 900: // tornado
|
||||
return WINDY;
|
||||
//Group 800: Clear
|
||||
case 800: // clear sky
|
||||
return CLEAR_SKY;
|
||||
//Group 80x: Clouds
|
||||
case 801: // few clouds
|
||||
case 802: // scattered clouds
|
||||
case 803: // broken clouds
|
||||
case 804: // overcast clouds
|
||||
return OVERCAST;
|
||||
//Group 90x: Extreme
|
||||
case 901: // tropical storm
|
||||
return WINDY;
|
||||
case 903: // cold
|
||||
case 904: // hot
|
||||
return CLEAR_SKY;
|
||||
case 905: // windy
|
||||
return WINDY;
|
||||
case 906: // hail
|
||||
return HAIL;
|
||||
//Group 9xx: Additional
|
||||
case 951: // calm
|
||||
case 952: // light breeze
|
||||
case 953: // gentle breeze
|
||||
case 954: // moderate breeze
|
||||
case 955: // fresh breeze
|
||||
return CLEAR_SKY;
|
||||
|
||||
case 956: // strong breeze
|
||||
case 957: // high wind/near gale
|
||||
case 958: // gale
|
||||
case 959: // severe gale
|
||||
case 960: // storm
|
||||
case 961: // violent storm
|
||||
case 902: // hurricane
|
||||
case 962: // hurricane
|
||||
return WINDY;
|
||||
|
||||
default:
|
||||
return CLEAR_SKY;
|
||||
}
|
||||
}
|
||||
}
|
@ -67,9 +67,4 @@ public class MiBand7ProCoordinator extends XiaomiCoordinator {
|
||||
// no PAI nor vitality score
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -78,9 +78,4 @@ public class MiBand8Coordinator extends XiaomiCoordinator {
|
||||
// FIXME still has some issues
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -78,9 +78,4 @@ public class MiBand8ActiveCoordinator extends XiaomiCoordinator {
|
||||
// FIXME still has some issues
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -67,11 +67,6 @@ public class RedmiWatch3Coordinator extends XiaomiCoordinator {
|
||||
return R.drawable.ic_device_amazfit_bip_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContactsSlotCount(final GBDevice device) {
|
||||
return 10; // TODO:verify
|
||||
|
@ -67,11 +67,6 @@ public class RedmiWatch3ActiveCoordinator extends XiaomiCoordinator {
|
||||
return R.drawable.ic_device_amazfit_bip_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getContactsSlotCount(final GBDevice device) {
|
||||
return 10; // TODO:verify
|
||||
|
@ -72,9 +72,4 @@ public class XiaomiWatchS1ActiveCoordinator extends XiaomiCoordinator {
|
||||
public int getDisabledIconResource() {
|
||||
return R.drawable.ic_device_miwatch_disabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsMultipleWeatherLocations() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,7 @@ public final class XiaomiPreferences {
|
||||
public static final String FEAT_SCREEN_ON_ON_NOTIFICATIONS = "feat_screen_on_on_notifications";
|
||||
public static final String FEAT_CAMERA_REMOTE = "feat_camera_remote";
|
||||
public static final String FEAT_WIDGETS = "feat_widgets";
|
||||
public static final String FEAT_MULTIPLE_WEATHER_LOCATIONS = "feat_multiple_weather_locations";
|
||||
|
||||
private XiaomiPreferences() {
|
||||
// util class
|
||||
|
@ -42,6 +42,7 @@ import java.util.UUID;
|
||||
import nodomain.freeyourgadget.gadgetbridge.BuildConfig;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.DeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiFWHelper;
|
||||
@ -399,7 +400,7 @@ public class XiaomiSupport extends AbstractDeviceSupport {
|
||||
|
||||
@Override
|
||||
public void onSendWeather(final ArrayList<WeatherSpec> weatherSpecs) {
|
||||
weatherService.onSendWeather(weatherSpecs.get(0));
|
||||
weatherService.onSendWeather(weatherSpecs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -519,6 +520,11 @@ public class XiaomiSupport extends AbstractDeviceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
public void setFeatureSupported(final String featureKey, final boolean supported) {
|
||||
LOG.debug("Setting feature {} -> {}", featureKey, supported ? "supported" : "not supported");
|
||||
evaluateGBDeviceEvent(new GBDeviceEventUpdatePreferences(featureKey, supported));
|
||||
}
|
||||
|
||||
private static final String[] EMOJI_SOURCE = new String[]{
|
||||
"\uD83D\uDE0D", // 😍
|
||||
"\uD83D\uDE18", // 😘
|
||||
|
@ -16,46 +16,220 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.services;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiWeatherConditions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.xiaomi.XiaomiWeatherConditions;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Weather;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.proto.xiaomi.XiaomiProto;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiPreferences;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xiaomi.XiaomiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class XiaomiWeatherService extends AbstractXiaomiService {
|
||||
public static final int COMMAND_TYPE = 10;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiWeatherService.class);
|
||||
private static final int CMD_TEMPERATURE_UNIT_GET = 9;
|
||||
private static final int CMD_TEMPERATURE_UNIT_SET = 10;
|
||||
|
||||
private static final int TEMPERATURE_SCALE_CELSIUS = 1;
|
||||
private static final int TEMPERATURE_SCALE_FAHRENHEIT = 2;
|
||||
|
||||
public static final int COMMAND_TYPE = 10;
|
||||
|
||||
private static final int CMD_SET_CURRENT_WEATHER = 0;
|
||||
private static final int CMD_SET_DAILY_WEATHER = 1;
|
||||
private static final int CMD_SET_CURRENT_LOCATION = 6;
|
||||
private static final int CMD_UPDATE_DAILY_FORECAST = 1;
|
||||
private static final int CMD_UPDATE_HOURLY_FORECAST = 2;
|
||||
private static final int CMD_REQUEST_CONDITIONS_FOR_LOCATION = 3;
|
||||
private static final int CMD_GET_LOCATIONS = 5;
|
||||
private static final int CMD_SET_LOCATIONS = 6;
|
||||
private static final int CMD_ADD_LOCATION = 7;
|
||||
private static final int CMD_REMOVE_LOCATIONS = 8;
|
||||
private static final int CMD_GET_WEATHER_PREFS = 9;
|
||||
private static final int CMD_SET_WEATHER_PREFS = 10;
|
||||
|
||||
private final Set<XiaomiProto.WeatherLocation> cachedWeatherLocations = new HashSet<>();
|
||||
|
||||
public XiaomiWeatherService(final XiaomiSupport support) {
|
||||
super(support);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
// TODO
|
||||
public void initialize() {
|
||||
// since temperature unit is app-wide instead of device-specific, update device setting during init
|
||||
setMeasurementSystem();
|
||||
|
||||
// determine whether multiple weather locations are supported by device
|
||||
// device should respond with status 1 if unsupported, we will then send cached weather
|
||||
getSupport().sendCommand("get weather locations", COMMAND_TYPE, CMD_GET_LOCATIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// TODO setMeasurementSystem();, or request
|
||||
public void handleCommand(final XiaomiProto.Command cmd) {
|
||||
if (cmd.hasStatus() && cmd.getStatus() != 0) {
|
||||
LOG.warn("Received Weather command {} with status code {}", cmd.getSubtype(), cmd.getStatus());
|
||||
}
|
||||
|
||||
switch (cmd.getSubtype()) {
|
||||
case CMD_UPDATE_DAILY_FORECAST: {
|
||||
if (!cmd.hasStatus()) {
|
||||
|
||||
LOG.warn("Received unexpected response to daily forecast update");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully sent daily forecast update");
|
||||
break;
|
||||
case 1:
|
||||
LOG.warn("Daily forecasts not supported by device");
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unexpected status code {} in response to daily forecast update", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CMD_UPDATE_HOURLY_FORECAST: {
|
||||
if (!cmd.hasStatus()) {
|
||||
LOG.warn("Received unexpected response to hourly forecast update");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully sent hourly forecast update");
|
||||
break;
|
||||
case 1:
|
||||
LOG.warn("Hourly forecasts not supported by device");
|
||||
break;
|
||||
default:
|
||||
LOG.error("Unexpected status code {} in response to hourly forecast update", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CMD_REQUEST_CONDITIONS_FOR_LOCATION: {
|
||||
onConditionRequestReceived(cmd);
|
||||
return;
|
||||
}
|
||||
case CMD_GET_LOCATIONS: {
|
||||
onWeatherLocationsReceived(cmd);
|
||||
return;
|
||||
}
|
||||
case CMD_SET_LOCATIONS: {
|
||||
if (!cmd.hasStatus()) {
|
||||
LOG.warn("Received unexpected response after setting locations");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully updated location list");
|
||||
break;
|
||||
case 1:
|
||||
LOG.warn("Device reported that setting the location list is not supported!");
|
||||
break;
|
||||
default:
|
||||
LOG.error("Received status code {} for setting location list", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CMD_ADD_LOCATION: {
|
||||
if (!cmd.hasStatus()) {
|
||||
LOG.warn("Unexpected response to add location command");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully added weather location");
|
||||
break;
|
||||
case 1:
|
||||
LOG.debug("Adding single weather location not supported on this device");
|
||||
break;
|
||||
case 3:
|
||||
LOG.debug("Failed to add single weather location; this location may have already been added");
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected status code {} in response to adding single weather location", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CMD_REMOVE_LOCATIONS: {
|
||||
if (!cmd.hasStatus()) {
|
||||
LOG.warn("Unexpected response to remove locations command");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully removed weather locations");
|
||||
break;
|
||||
case 1:
|
||||
LOG.debug("Removing weather locations not supported on this device");
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unexpected status code {} in response to removing weather locations", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
case CMD_SET_WEATHER_PREFS: {
|
||||
if (!cmd.hasStatus()) {
|
||||
LOG.warn("Received unexpected response after setting weather preferences");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (cmd.getStatus()) {
|
||||
case 0:
|
||||
LOG.debug("Successfully updated weather-related preferences");
|
||||
break;
|
||||
case 1:
|
||||
LOG.warn("Weather-related preferences are not supported on this device");
|
||||
break;
|
||||
default:
|
||||
LOG.debug("Unexpected status code {} received in response to weather prefs update request", cmd.getStatus());
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
LOG.warn("Unhandled weather service command {}", cmd.getSubtype());
|
||||
LOG.debug("Unhandled command content: {}", cmd);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSendConfiguration(final String config, final Prefs prefs) {
|
||||
// TODO add preference for warning notifications (if that has any effect at all)
|
||||
|
||||
switch (config) {
|
||||
case SettingsActivity.PREF_MEASUREMENT_SYSTEM:
|
||||
setMeasurementSystem();
|
||||
@ -65,144 +239,409 @@ public class XiaomiWeatherService extends AbstractXiaomiService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onSendWeather(final WeatherSpec weatherSpec) {
|
||||
String timestamp = unixTimetstampToISOWithColons(weatherSpec.timestamp);
|
||||
private static XiaomiProto.WeatherMetadata getWeatherMetaFromSpec(final WeatherSpec weatherSpec) {
|
||||
final String location = StringUtils.ensureNotNull(weatherSpec.location);
|
||||
return XiaomiProto.WeatherMetadata.newBuilder()
|
||||
.setPublicationTimestamp(unixTimestampToISOWithColons(weatherSpec.timestamp))
|
||||
.setCityName("")
|
||||
.setLocationName(location)
|
||||
.setLocationKey(getLocationKey(location)) // FIXME: placeholder because key is not present in spec
|
||||
.setIsCurrentLocation(weatherSpec.isCurrentLocation == 1)
|
||||
.build();
|
||||
}
|
||||
|
||||
final XiaomiCoordinator coordinator = getSupport().getCoordinator();
|
||||
private static XiaomiProto.WeatherLocation getWeatherLocationFromSpec(final WeatherSpec weatherSpec) {
|
||||
return XiaomiProto.WeatherLocation.newBuilder()
|
||||
.setCode(getLocationKey(weatherSpec.location))
|
||||
.setName(StringUtils.ensureNotNull(weatherSpec.location))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (coordinator.supportsMultipleWeatherLocations()) {
|
||||
// TODO actually support multiple locations (primary + 4 secondary)
|
||||
getSupport().sendCommand(
|
||||
"set current location",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_CURRENT_LOCATION)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setCurrentLocation(
|
||||
XiaomiProto.WeatherCurrentLocation.newBuilder()
|
||||
.setLocation(XiaomiProto.WeatherLocation.newBuilder()
|
||||
.setCode("accu:123456") // FIXME:AccuWeather code (we do not have it here)
|
||||
.setName(weatherSpec.location)
|
||||
)
|
||||
))
|
||||
.build()
|
||||
);
|
||||
private static String getLocationKey(final String locationName) {
|
||||
return String.format(Locale.ROOT, "accu:%d", Math.abs(StringUtils.ensureNotNull(locationName).hashCode()) % 1000000);
|
||||
}
|
||||
|
||||
private static XiaomiProto.WeatherUnitValue buildUnitValue(final int value, final String unit) {
|
||||
return XiaomiProto.WeatherUnitValue.newBuilder()
|
||||
.setUnit(unit)
|
||||
.setValue(value)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void addWeatherLocation(final XiaomiProto.WeatherLocation location) {
|
||||
LOG.debug("Adding weather location: code={}, name={}", location.getCode(), location.getName());
|
||||
|
||||
getSupport().sendCommand("add weather location", XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_ADD_LOCATION)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder()
|
||||
.setLocation(location))
|
||||
.build());
|
||||
}
|
||||
|
||||
private void addWeatherLocationFromSpec(final WeatherSpec spec) {
|
||||
addWeatherLocation(getWeatherLocationFromSpec(spec));
|
||||
}
|
||||
|
||||
public void sendCurrentConditions(final WeatherSpec weatherSpec) {
|
||||
LOG.debug("Sending current weather conditions for {}", weatherSpec.location);
|
||||
|
||||
XiaomiProto.Command command = XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_CURRENT_WEATHER)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setCurrent(
|
||||
XiaomiProto.WeatherCurrent.newBuilder()
|
||||
.setMetadata(getWeatherMetaFromSpec(weatherSpec))
|
||||
.setWeatherCondition(XiaomiWeatherConditions.convertOwmConditionToXiaomi(weatherSpec.currentConditionCode))
|
||||
.setTemperature(buildUnitValue(weatherSpec.currentTemp - 273, "℃"))
|
||||
.setHumidity(buildUnitValue(weatherSpec.currentHumidity, "%"))
|
||||
.setWind(buildUnitValue(weatherSpec.windSpeedAsBeaufort(), Integer.toString(weatherSpec.windDirection)))
|
||||
.setUv(buildUnitValue(Math.round(weatherSpec.uvIndex), "")) // This is sent as an sint but seems to be displayed with a decimal point
|
||||
.setAqi(buildUnitValue(
|
||||
weatherSpec.airQuality != null && weatherSpec.airQuality.aqi >= 0 ? weatherSpec.airQuality.aqi : 0,
|
||||
"Unknown" // some string like "Moderate"
|
||||
))
|
||||
.setWarning(XiaomiProto.WeatherWarnings.newBuilder()) // TODO add warnings when they become available through spec
|
||||
.setPressure(weatherSpec.pressure)
|
||||
))
|
||||
.build();
|
||||
|
||||
getSupport().sendCommand("set current weather", command);
|
||||
}
|
||||
|
||||
public void sendDailyForecast(final WeatherSpec weatherSpec) {
|
||||
final XiaomiProto.ForecastEntries.Builder entryListBuilder = XiaomiProto.ForecastEntries.newBuilder();
|
||||
final int daysToSend = Math.min(6, weatherSpec.forecasts.size());
|
||||
|
||||
// reconstruct first forecast element from current conditions, as the first forecast
|
||||
// is expected to apply to today
|
||||
{
|
||||
entryListBuilder.addEntry(XiaomiProto.ForecastEntry.newBuilder()
|
||||
.setAqi(buildUnitValue(
|
||||
weatherSpec.airQuality != null && weatherSpec.airQuality.aqi >= 0 ? weatherSpec.airQuality.aqi : 0,
|
||||
"Unknown" // TODO describe AQI level
|
||||
))
|
||||
.setTemperatureRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setFrom(weatherSpec.todayMinTemp - 273)
|
||||
.setTo(weatherSpec.todayMaxTemp - 273))
|
||||
// FIXME: should preferable be replaced with a best and worst case condition whenever that becomes available
|
||||
.setConditionRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setFrom(XiaomiWeatherConditions.convertOwmConditionToXiaomi(weatherSpec.currentConditionCode))
|
||||
.setTo(XiaomiWeatherConditions.convertOwmConditionToXiaomi(weatherSpec.currentConditionCode)))
|
||||
.setTemperatureSymbol("℃")
|
||||
.setSunriseSunset(XiaomiProto.WeatherSunriseSunset.newBuilder()
|
||||
.setSunrise(weatherSpec.sunRise != 0 ? unixTimestampToISOWithColons(weatherSpec.sunRise) : "")
|
||||
.setSunset(weatherSpec.sunSet != 0 ? unixTimestampToISOWithColons(weatherSpec.sunSet) : "")));
|
||||
}
|
||||
|
||||
getSupport().sendCommand(
|
||||
"set current weather",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_CURRENT_WEATHER)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setCurrent(
|
||||
XiaomiProto.WeatherCurrent.newBuilder()
|
||||
.setTimeLocation(XiaomiProto.WeatherCurrentTimeLocation.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setUnk2("")
|
||||
.setCurrentLocationString(weatherSpec.location)
|
||||
.setCurrentLocationCode("accu:123456") // FIXME:AccuWeather code (we do not have it here)
|
||||
.setUnk5(true)
|
||||
)
|
||||
.setWeatherCondition(HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.currentConditionCode)) // *SEEMS* to work
|
||||
.setTemperature(XiaomiProto.WeatherCurrentTemperature.newBuilder()
|
||||
.setDegrees(weatherSpec.currentTemp - 273) // TODO: support inches for weather
|
||||
.setSymbol("℃")
|
||||
)
|
||||
.setHumidity(XiaomiProto.WeatherCurrentHumidity.newBuilder()
|
||||
.setHumidity(weatherSpec.currentHumidity)
|
||||
.setSymbol("%")
|
||||
)
|
||||
.setWind(XiaomiProto.WeatherCurrentWind.newBuilder()
|
||||
.setWind(weatherSpec.windSpeedAsBeaufort())
|
||||
.setSymbol("")
|
||||
)
|
||||
.setUv(XiaomiProto.WeatherCurrentUVIndex.newBuilder()
|
||||
.setUnk1("")
|
||||
.setIndex(Math.round(weatherSpec.uvIndex)) // This is sent as an sint but seems to be displayed with a decimal point
|
||||
)
|
||||
.setAQI(XiaomiProto.WeatherCurrentAQI.newBuilder()
|
||||
.setAQIText("Unknown") // some string like "Moderate"
|
||||
.setAQI(weatherSpec.airQuality != null && weatherSpec.airQuality.aqi >= 0 ? weatherSpec.airQuality.aqi : 0)
|
||||
)
|
||||
.setWarning(XiaomiProto.WeatherCurrentWarning.newBuilder()
|
||||
.addCurrentWarning1(XiaomiProto.WeatherCurrentWarning1.newBuilder()
|
||||
.setCurrentWarningText("")
|
||||
.setCurrentWarningSeverityText("")
|
||||
)
|
||||
)
|
||||
.setPressure(weatherSpec.pressure)
|
||||
))
|
||||
.build()
|
||||
);
|
||||
// loop over available forecast entries in weatherSpec
|
||||
for (WeatherSpec.Daily currentEntry : weatherSpec.forecasts.subList(0, daysToSend)) {
|
||||
entryListBuilder.addEntry(XiaomiProto.ForecastEntry.newBuilder()
|
||||
.setAqi(buildUnitValue(
|
||||
currentEntry.airQuality != null && currentEntry.airQuality.aqi >= 0 ? currentEntry.airQuality.aqi : 0,
|
||||
"Unknown" // TODO describe AQI level
|
||||
))
|
||||
// FIXME should preferable be replaced with a best and worst case condition whenever that becomes available
|
||||
.setConditionRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setFrom(XiaomiWeatherConditions.convertOwmConditionToXiaomi(currentEntry.conditionCode))
|
||||
.setTo(XiaomiWeatherConditions.convertOwmConditionToXiaomi(currentEntry.conditionCode)))
|
||||
.setTemperatureRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setTo(currentEntry.maxTemp - 273)
|
||||
.setFrom(currentEntry.minTemp - 273))
|
||||
.setTemperatureSymbol("℃")
|
||||
.setSunriseSunset(XiaomiProto.WeatherSunriseSunset.newBuilder()
|
||||
.setSunrise(currentEntry.sunRise != 0 ? unixTimestampToISOWithColons(currentEntry.sunRise) : "")
|
||||
.setSunset(currentEntry.sunSet != 0 ? unixTimestampToISOWithColons(currentEntry.sunSet) : "")));
|
||||
}
|
||||
|
||||
LOG.debug("Sending daily forecast with {} days of info", entryListBuilder.getEntryCount());
|
||||
|
||||
if (weatherSpec.forecasts != null) {
|
||||
XiaomiProto.WeatherDailyList.Builder dailyListBuilder = XiaomiProto.WeatherDailyList.newBuilder();
|
||||
int daysToSend = Math.min(7, weatherSpec.forecasts.size());
|
||||
for (int i = 0; i < daysToSend; i++) {
|
||||
WeatherSpec.AirQuality airQuality = weatherSpec.forecasts.get(i).airQuality;
|
||||
XiaomiProto.Command command = XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_UPDATE_DAILY_FORECAST)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setForecast(
|
||||
XiaomiProto.WeatherForecast.newBuilder()
|
||||
.setMetadata(getWeatherMetaFromSpec(weatherSpec))
|
||||
.setEntries(entryListBuilder)))
|
||||
.build();
|
||||
|
||||
dailyListBuilder.addForecastDay(XiaomiProto.WeatherDailyForecastDay.newBuilder()
|
||||
.setAQI(XiaomiProto.DailyAQI.newBuilder()
|
||||
.setAQIText("")
|
||||
.setAQI(airQuality != null && airQuality.aqi >= 0 ? airQuality.aqi : 0)
|
||||
)
|
||||
.setUnk2(XiaomiProto.DailyUnk2.newBuilder()
|
||||
.setUnk1(HuamiWeatherConditions.mapToAmazfitBipWeatherCode(weatherSpec.forecasts.get(i).conditionCode)) // TODO: verify
|
||||
.setUnk2(0)
|
||||
)
|
||||
.setHighLowTemp(XiaomiProto.DailyHighLowTemp.newBuilder()
|
||||
.setHigh(weatherSpec.forecasts.get(i).maxTemp - 273)
|
||||
.setLow(weatherSpec.forecasts.get(i).minTemp - 273)
|
||||
)
|
||||
.setTemperatureSymbol("℃")
|
||||
.setSunriseSunset(XiaomiProto.DailySunriseSunset.newBuilder()
|
||||
.setSunrise(weatherSpec.forecasts.get(i).sunRise != 0 ? unixTimetstampToISOWithColons(weatherSpec.forecasts.get(i).sunRise) : "")
|
||||
.setSunset(weatherSpec.forecasts.get(i).sunSet != 0 ? unixTimetstampToISOWithColons(weatherSpec.forecasts.get(i).sunSet) : "")
|
||||
)
|
||||
);
|
||||
getSupport().sendCommand("set daily forecast", command);
|
||||
}
|
||||
|
||||
public void sendHourlyForecast(final WeatherSpec weatherSpec) {
|
||||
final XiaomiProto.ForecastEntries.Builder entriesBuilder = XiaomiProto.ForecastEntries.newBuilder();
|
||||
final int hoursToSend = Math.min(23, weatherSpec.hourly.size());
|
||||
|
||||
for (WeatherSpec.Hourly hourly : weatherSpec.hourly.subList(0, hoursToSend)) {
|
||||
entriesBuilder.addEntry(XiaomiProto.ForecastEntry.newBuilder()
|
||||
.setAqi(buildUnitValue(0, "Unknown")) // FIXME when available through spec
|
||||
.setTemperatureRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setFrom(0) // not set, but required
|
||||
.setTo(hourly.temp - 273))
|
||||
.setConditionRange(XiaomiProto.WeatherRange.newBuilder()
|
||||
.setFrom(0) // not set, but required
|
||||
.setTo(XiaomiWeatherConditions.convertOwmConditionToXiaomi(hourly.conditionCode)))
|
||||
.setTemperatureSymbol("℃")
|
||||
.setWind(buildUnitValue(hourly.windSpeedAsBeaufort(), Integer.toString(hourly.windDirection))));
|
||||
}
|
||||
|
||||
LOG.debug("Sending hourly forecast with {} hours of info", entriesBuilder.getEntryCount());
|
||||
|
||||
final XiaomiProto.Command command = XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_UPDATE_HOURLY_FORECAST)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder()
|
||||
.setForecast(XiaomiProto.WeatherForecast.newBuilder()
|
||||
.setMetadata(getWeatherMetaFromSpec(weatherSpec))
|
||||
.setEntries(entriesBuilder)))
|
||||
.build();
|
||||
|
||||
getSupport().sendCommand("update hourly forecast", command);
|
||||
}
|
||||
|
||||
private boolean supportsMultipleWeatherLocations() {
|
||||
return getDevicePrefs().getBoolean(XiaomiPreferences.FEAT_MULTIPLE_WEATHER_LOCATIONS, false);
|
||||
}
|
||||
|
||||
public void onSendWeather(@NonNull final List<WeatherSpec> weatherSpecList) {
|
||||
if (supportsMultipleWeatherLocations()) {
|
||||
sendWeatherSpecList(weatherSpecList);
|
||||
} else {
|
||||
if (!weatherSpecList.isEmpty() && weatherSpecList.get(0) != null) {
|
||||
final WeatherSpec specToSend = weatherSpecList.get(0);
|
||||
addWeatherLocationFromSpec(specToSend);
|
||||
sendWeatherSpec(specToSend);
|
||||
}
|
||||
|
||||
getSupport().sendCommand(
|
||||
"set daily forecast",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_DAILY_WEATHER)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setDaily(
|
||||
XiaomiProto.WeatherDaily.newBuilder()
|
||||
.setTimeLocation(XiaomiProto.WeatherCurrentTimeLocation.newBuilder()
|
||||
.setTimestamp(timestamp)
|
||||
.setUnk2("")
|
||||
.setCurrentLocationString(weatherSpec.location)
|
||||
.setCurrentLocationCode("accu:123456") // FIXME:AccuWeather code (we do not have it here)
|
||||
.setUnk5(true)
|
||||
)
|
||||
.setDailyList(dailyListBuilder)
|
||||
))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendWeatherSpecList(@NonNull final List<WeatherSpec> weatherSpecs) {
|
||||
// FIXME: the MB8P seems to support more locations than the original app allows, which is
|
||||
// undoubtedly also applicable to other devices
|
||||
List<WeatherSpec> specsToSend = weatherSpecs.subList(0, Math.min(5, weatherSpecs.size()));
|
||||
List<XiaomiProto.WeatherLocation> weatherLocations = new ArrayList<>(specsToSend.size());
|
||||
|
||||
LOG.debug("Updating weather for {} location(s): {}", specsToSend.size(), extractWeatherSpecLocations(specsToSend));
|
||||
|
||||
// find locations not present on device
|
||||
{
|
||||
for (final WeatherSpec spec : specsToSend) {
|
||||
final XiaomiProto.WeatherLocation location = getWeatherLocationFromSpec(spec);
|
||||
|
||||
if (!cachedWeatherLocations.contains(location)) {
|
||||
addWeatherLocation(location);
|
||||
|
||||
// assume adding location goes according to plan
|
||||
cachedWeatherLocations.add(location);
|
||||
}
|
||||
|
||||
weatherLocations.add(location);
|
||||
}
|
||||
}
|
||||
|
||||
// update order of locations list on device
|
||||
{
|
||||
getSupport().sendCommand("set weather locations order", XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_SET_LOCATIONS)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder()
|
||||
.setLocations(XiaomiProto.WeatherLocations.newBuilder()
|
||||
.addAllLocation(weatherLocations)))
|
||||
.build());
|
||||
}
|
||||
|
||||
for (WeatherSpec spec : specsToSend) {
|
||||
sendWeatherSpec(spec);
|
||||
}
|
||||
|
||||
// request current location list from device to remove dangling locations
|
||||
getSupport().sendCommand("request weather locations", COMMAND_TYPE, CMD_GET_LOCATIONS);
|
||||
}
|
||||
|
||||
private void sendWeatherSpec(@NonNull final WeatherSpec weatherSpec) {
|
||||
LOG.debug("Send weather for location {}", weatherSpec.location);
|
||||
|
||||
sendCurrentConditions(weatherSpec);
|
||||
sendDailyForecast(weatherSpec);
|
||||
sendHourlyForecast(weatherSpec);
|
||||
}
|
||||
|
||||
private void setMeasurementSystem() {
|
||||
final Prefs prefs = getDevicePrefs();
|
||||
final String measurementSystem = prefs.getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, "metric");
|
||||
final String metricScale = getSupport().getContext().getString(R.string.p_unit_metric);
|
||||
final String imperialScale = getSupport().getContext().getString(R.string.p_unit_imperial);
|
||||
final String measurementSystem = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, metricScale);
|
||||
LOG.info("Setting measurement system to {}", measurementSystem);
|
||||
|
||||
final int unitValue = "metric".equals(measurementSystem) ? 1 : 2;
|
||||
int unitValue = TEMPERATURE_SCALE_CELSIUS;
|
||||
|
||||
if (measurementSystem.equals(imperialScale)) {
|
||||
unitValue = TEMPERATURE_SCALE_FAHRENHEIT;
|
||||
} else if (!measurementSystem.equals(metricScale)) {
|
||||
LOG.warn("Unknown measurement system, defaulting to celsius");
|
||||
}
|
||||
|
||||
getSupport().sendCommand(
|
||||
"set temperature unit",
|
||||
"set temperature scale",
|
||||
XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_TEMPERATURE_UNIT_SET)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setTemperatureUnit(
|
||||
XiaomiProto.WeatherTemperatureUnit.newBuilder().setUnit(unitValue)
|
||||
.setSubtype(CMD_SET_WEATHER_PREFS)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder().setPrefs(
|
||||
XiaomiProto.WeatherPrefs.newBuilder().setTemperatureScale(unitValue)
|
||||
))
|
||||
.build()
|
||||
);
|
||||
}
|
||||
|
||||
private String unixTimetstampToISOWithColons(int timestamp) {
|
||||
private void onConditionRequestReceived(final XiaomiProto.Command command) {
|
||||
if (command.hasStatus() && command.getStatus() != 0) {
|
||||
LOG.warn("Received request for conditions with unexpected status code {}", command.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
if (command.hasWeather() && command.getWeather().hasLocation()) {
|
||||
final String locationKey = command.getWeather().getLocation().getCode();
|
||||
final String locationName = command.getWeather().getLocation().getName();
|
||||
|
||||
if (!TextUtils.isEmpty(locationKey) && !TextUtils.isEmpty(locationName)) {
|
||||
LOG.debug("Received request for conditions (location key = {}, name = {})", locationKey, locationName);
|
||||
|
||||
final List<WeatherSpec> knownWeathers = Weather.getInstance().getWeatherSpecs();
|
||||
for (WeatherSpec spec : knownWeathers) {
|
||||
if (TextUtils.equals(spec.location, locationName)) {
|
||||
sendWeatherSpec(spec);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG.debug("Received request for conditions at current location");
|
||||
}
|
||||
}
|
||||
|
||||
final WeatherSpec spec = Weather.getInstance().getWeatherSpec();
|
||||
|
||||
if (spec == null) {
|
||||
LOG.warn("Not sending weather conditions: active weather spec is null!");
|
||||
return;
|
||||
}
|
||||
|
||||
sendWeatherSpec(Weather.getInstance().getWeatherSpec());
|
||||
}
|
||||
|
||||
private static String[] weatherLocationsToStringArray(final Collection<XiaomiProto.WeatherLocation> locations) {
|
||||
final List<String> result = new ArrayList<>();
|
||||
|
||||
for (XiaomiProto.WeatherLocation l : locations) {
|
||||
result.add(String.format("{code=%s, name=%s}", l.getCode(), l.getName()));
|
||||
}
|
||||
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private static String[] extractWeatherSpecLocations(final Collection<WeatherSpec> specs) {
|
||||
final List<String> result = new ArrayList<>();
|
||||
|
||||
for (final WeatherSpec spec : specs) {
|
||||
result.add(StringUtils.ensureNotNull(spec.location));
|
||||
}
|
||||
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
private void onWeatherLocationsReceived(final XiaomiProto.Command cmd) {
|
||||
// status code 1 = explicitly not supported
|
||||
if (cmd.hasStatus() && cmd.getStatus() == 1) {
|
||||
LOG.warn("Multiple weather locations not supported by this device");
|
||||
getSupport().setFeatureSupported(XiaomiPreferences.FEAT_MULTIPLE_WEATHER_LOCATIONS, false);
|
||||
|
||||
// now that the feature flag has been updated, send cached weather
|
||||
onSendWeather(Weather.getInstance().getWeatherSpecs());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd.hasStatus() && cmd.getStatus() != 0) {
|
||||
LOG.warn("Received unexpected status code for configured weather locations request: {}", cmd.getStatus());
|
||||
return;
|
||||
}
|
||||
|
||||
getSupport().setFeatureSupported(XiaomiPreferences.FEAT_MULTIPLE_WEATHER_LOCATIONS, true);
|
||||
|
||||
if (!cmd.hasWeather() || !cmd.getWeather().hasLocations()) {
|
||||
LOG.warn("Received unexpected payload in response to configured weather locations request");
|
||||
LOG.debug("Unexpected weather locations command: {}", cmd);
|
||||
}
|
||||
|
||||
final List<XiaomiProto.WeatherLocation> retrievedLocations = cmd.getWeather().getLocations().getLocationList();
|
||||
|
||||
LOG.debug("Received {} weather locations: {}", retrievedLocations.size(), weatherLocationsToStringArray(retrievedLocations));
|
||||
|
||||
// remove any duplicate locations from device before caching locations
|
||||
{
|
||||
final Set<XiaomiProto.WeatherLocation> duplicateLocations = new HashSet<>();
|
||||
|
||||
for (XiaomiProto.WeatherLocation l : retrievedLocations) {
|
||||
if (Collections.frequency(retrievedLocations, l) > 1) {
|
||||
duplicateLocations.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
if (!duplicateLocations.isEmpty()) {
|
||||
LOG.debug("Removing {} locations which were found as duplicates: {}", duplicateLocations.size(), weatherLocationsToStringArray(duplicateLocations));
|
||||
|
||||
getSupport().sendCommand("remove duplicate weather locations", XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_REMOVE_LOCATIONS)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder()
|
||||
.setLocations(XiaomiProto.WeatherLocations.newBuilder()
|
||||
.addAllLocation(duplicateLocations)))
|
||||
.build());
|
||||
}
|
||||
}
|
||||
|
||||
final Set<XiaomiProto.WeatherLocation> specLocations = new HashSet<>();
|
||||
|
||||
for (final WeatherSpec s : Weather.getInstance().getWeatherSpecs()) {
|
||||
specLocations.add(getWeatherLocationFromSpec(s));
|
||||
}
|
||||
|
||||
// remove locations for which a cached weather spec cannot be found
|
||||
{
|
||||
final Set<XiaomiProto.WeatherLocation> locationsMissingSpec = new HashSet<>();
|
||||
|
||||
for (XiaomiProto.WeatherLocation l : retrievedLocations) {
|
||||
if (!specLocations.contains(l)) {
|
||||
locationsMissingSpec.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
if (!locationsMissingSpec.isEmpty()) {
|
||||
LOG.debug("Removing {} weather locations for which a weather spec is not found cached: {}",
|
||||
locationsMissingSpec.size(),
|
||||
weatherLocationsToStringArray(locationsMissingSpec));
|
||||
|
||||
getSupport().sendCommand("remove non-cached weather locations", XiaomiProto.Command.newBuilder()
|
||||
.setType(COMMAND_TYPE)
|
||||
.setSubtype(CMD_REMOVE_LOCATIONS)
|
||||
.setWeather(XiaomiProto.Weather.newBuilder()
|
||||
.setLocations(XiaomiProto.WeatherLocations.newBuilder()
|
||||
.addAllLocation(locationsMissingSpec))).build());
|
||||
}
|
||||
}
|
||||
|
||||
// cache location that were unique
|
||||
final Set<XiaomiProto.WeatherLocation> presentLocations = new HashSet<>();
|
||||
for (XiaomiProto.WeatherLocation l : retrievedLocations) {
|
||||
if (specLocations.contains(l) && Collections.frequency(retrievedLocations, l) == 1) {
|
||||
presentLocations.add(l);
|
||||
}
|
||||
}
|
||||
|
||||
LOG.debug("Caching {} weather locations: {}", presentLocations.size(), weatherLocationsToStringArray(presentLocations));
|
||||
cachedWeatherLocations.clear();
|
||||
cachedWeatherLocations.addAll(presentLocations);
|
||||
}
|
||||
|
||||
public static String unixTimestampToISOWithColons(int timestamp) {
|
||||
return new StringBuilder(
|
||||
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.US)
|
||||
.format(new Date(timestamp * 1000L)))
|
||||
|
@ -734,127 +734,95 @@ message NotificationIconPackage {
|
||||
|
||||
message Weather {
|
||||
optional WeatherCurrent current = 1;
|
||||
optional WeatherDaily daily = 2;
|
||||
optional WeatherForecast forecast = 2;
|
||||
|
||||
// 10, 6 request without payload?
|
||||
// response to 10, 5 (get location list) | payload of 10, 6 (set, update location list) | payload of 10, 8 (remove)
|
||||
optional WeatherLocations locations = 4;
|
||||
|
||||
// 10, 5 set current | 10, 7 create | 10, 8 delete
|
||||
optional WeatherCurrentLocation currentLocation = 4;
|
||||
// 10, 7 create
|
||||
optional WeatherLocation create = 5;
|
||||
// indication payload of 10, 3 (requested update) | payload of 10, 7 (set current, add to list)
|
||||
optional WeatherLocation location = 5;
|
||||
|
||||
// 10, 10
|
||||
optional WeatherTemperatureUnit temperatureUnit = 6;
|
||||
optional WeatherPrefs prefs = 6;
|
||||
}
|
||||
|
||||
message WeatherCurrent {
|
||||
optional WeatherCurrentTimeLocation timeLocation = 1;
|
||||
optional uint32 weatherCondition = 2;
|
||||
optional WeatherCurrentTemperature temperature = 3;
|
||||
optional WeatherCurrentHumidity humidity= 4;
|
||||
optional WeatherCurrentWind wind = 5;
|
||||
optional WeatherCurrentUVIndex uv = 6;
|
||||
optional WeatherCurrentAQI AQI = 7;
|
||||
optional WeatherCurrentWarning warning = 8; // Seems to be an array?
|
||||
optional WeatherMetadata metadata = 1;
|
||||
required uint32 weatherCondition = 2;
|
||||
optional WeatherUnitValue temperature = 3;
|
||||
optional WeatherUnitValue humidity = 4;
|
||||
optional WeatherUnitValue wind = 5;
|
||||
optional WeatherUnitValue uv = 6;
|
||||
optional WeatherUnitValue aqi = 7;
|
||||
optional WeatherWarnings warning = 8; // Seems to be an array?
|
||||
optional float pressure = 9;
|
||||
}
|
||||
message WeatherCurrentTimeLocation {
|
||||
optional string timestamp = 1;
|
||||
optional string unk2 = 2;
|
||||
optional string currentLocationString = 3;
|
||||
optional string currentLocationCode = 4;
|
||||
optional bool unk5 = 5; // default location?
|
||||
|
||||
message WeatherMetadata {
|
||||
required string publicationTimestamp = 1;
|
||||
required string cityName = 2;
|
||||
required string locationName = 3;
|
||||
optional string locationKey = 4;
|
||||
optional bool isCurrentLocation = 5; // default location?
|
||||
}
|
||||
|
||||
message WeatherCurrentTemperature {
|
||||
optional string symbol = 1;
|
||||
optional sint32 degrees = 2;
|
||||
message WeatherUnitValue {
|
||||
required string unit = 1;
|
||||
required sint32 value = 2;
|
||||
}
|
||||
|
||||
message WeatherCurrentHumidity {
|
||||
optional string symbol = 1;
|
||||
optional sint32 humidity = 2;
|
||||
message WeatherWarnings {
|
||||
repeated WeatherWarning warning = 1;
|
||||
}
|
||||
|
||||
message WeatherCurrentWind {
|
||||
optional string symbol = 1;
|
||||
optional sint32 wind = 2;
|
||||
message WeatherWarning {
|
||||
required string type = 1;
|
||||
required string level = 2;
|
||||
optional string title = 3;
|
||||
optional string description = 4;
|
||||
optional string id = 5;
|
||||
}
|
||||
|
||||
message WeatherCurrentUVIndex {
|
||||
optional string unk1 = 1;
|
||||
optional sint32 index = 2;
|
||||
message WeatherLocations {
|
||||
repeated WeatherLocation location = 1;
|
||||
}
|
||||
|
||||
message WeatherCurrentAQI {
|
||||
optional string AQIText = 1;
|
||||
optional sint32 AQI = 2;
|
||||
message WeatherForecast {
|
||||
required WeatherMetadata metadata = 1;
|
||||
required ForecastEntries entries = 2;
|
||||
}
|
||||
|
||||
message WeatherCurrentWarning {
|
||||
repeated WeatherCurrentWarning1 currentWarning1 = 1;
|
||||
message ForecastEntries {
|
||||
repeated ForecastEntry entry = 1;
|
||||
}
|
||||
|
||||
message WeatherCurrentWarning1 {
|
||||
optional string currentWarningText = 1;
|
||||
optional string currentWarningSeverityText = 2;
|
||||
optional string currentWarningTitle = 3;
|
||||
optional string currentWarningDescription = 4;
|
||||
optional string unk5 = 5;
|
||||
}
|
||||
|
||||
message WeatherCurrentLocation {
|
||||
optional WeatherLocation location = 1;
|
||||
}
|
||||
|
||||
message WeatherDaily {
|
||||
required WeatherCurrentTimeLocation timeLocation = 1;
|
||||
required WeatherDailyList dailyList = 2;
|
||||
}
|
||||
|
||||
message WeatherDailyList {
|
||||
repeated WeatherDailyForecastDay forecastDay = 1;
|
||||
}
|
||||
|
||||
message WeatherDailyForecastDay {
|
||||
optional DailyAQI AQI = 1;
|
||||
optional DailyUnk2 unk2 = 2;
|
||||
optional DailyHighLowTemp highLowTemp = 3;
|
||||
message ForecastEntry {
|
||||
optional WeatherUnitValue aqi = 1;
|
||||
optional WeatherRange conditionRange = 2;
|
||||
optional WeatherRange temperatureRange = 3;
|
||||
optional string temperatureSymbol = 4;
|
||||
optional DailySunriseSunset sunriseSunset = 5;
|
||||
optional WeatherSunriseSunset sunriseSunset = 5;
|
||||
optional WeatherUnitValue wind = 6;
|
||||
}
|
||||
|
||||
message DailyAQI {
|
||||
optional string AQIText = 1;
|
||||
optional sint32 AQI = 2;
|
||||
message WeatherRange {
|
||||
required sint32 from = 1;
|
||||
required sint32 to = 2;
|
||||
}
|
||||
|
||||
message DailyUnk2 {
|
||||
optional sint32 unk1 = 1;
|
||||
optional sint32 unk2 = 2;
|
||||
}
|
||||
|
||||
message DailyHighLowTemp {
|
||||
optional sint32 low = 1;
|
||||
optional sint32 high = 2;
|
||||
}
|
||||
|
||||
message DailySunriseSunset {
|
||||
optional string sunrise = 1;
|
||||
optional string sunset = 2;
|
||||
message WeatherSunriseSunset {
|
||||
required string sunrise = 1;
|
||||
required string sunset = 2;
|
||||
}
|
||||
|
||||
message WeatherLocation {
|
||||
optional string code = 1;
|
||||
required string code = 1;
|
||||
optional string name = 2;
|
||||
}
|
||||
|
||||
message WeatherUnknown1 {
|
||||
optional float unknown12 = 12;
|
||||
}
|
||||
|
||||
message WeatherTemperatureUnit {
|
||||
optional uint32 unit = 1; // 1 celsius 2 fahrenheit
|
||||
message WeatherPrefs {
|
||||
optional uint32 temperatureScale = 1; // 1 celsius 2 fahrenheit
|
||||
optional uint32 weatherWarningsEnabled = 2; // 0 = unsupported, 1 = enabled, 2 = disabled
|
||||
}
|
||||
|
||||
//
|
||||
|
Loading…
Reference in New Issue
Block a user