/* 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.service.devices.huawei; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket; import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Weather; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.Request; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendGpsAndTimeToDeviceRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherCurrentRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherDeviceRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherErrorRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherExtendedSupportRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherForecastRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherStartRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherSunMoonSupportRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherSupportRequest; import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendWeatherUnitRequest; import nodomain.freeyourgadget.gadgetbridge.util.DateTimeUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class HuaweiWeatherManager { private static final Logger LOG = LoggerFactory.getLogger(HuaweiWeatherManager.class); private final HuaweiSupportProvider supportProvider; private boolean syncInProgress; public HuaweiWeatherManager(HuaweiSupportProvider supportProvider) { this.supportProvider = supportProvider; this.syncInProgress = false; } public Weather.WeatherIcon openWeatherMapConditionCodeToHuaweiIcon(int conditionCode) { // More exact first, groups after switch (conditionCode) { case 500: return Weather.WeatherIcon.LIGHT_RAIN; case 501: return Weather.WeatherIcon.RAIN; case 502: return Weather.WeatherIcon.HEAVY_RAIN; case 503: return Weather.WeatherIcon.RAIN_STORM; case 504: return Weather.WeatherIcon.SEVERE_RAIN_STORMS; case 511: return Weather.WeatherIcon.FREEZING_RAIN; case 600: return Weather.WeatherIcon.LIGHT_SNOW; case 601: return Weather.WeatherIcon.SNOW; case 602: return Weather.WeatherIcon.HEAVY_SNOW; case 611: return Weather.WeatherIcon.SLEET; case 701: case 741: return Weather.WeatherIcon.FOG; case 721: return Weather.WeatherIcon.HAZY; case 751: return Weather.WeatherIcon.SAND; case 761: return Weather.WeatherIcon.DUST; case 800: return Weather.WeatherIcon.SUNNY; case 801: case 802: return Weather.WeatherIcon.CLOUDY; case 803: case 804: return Weather.WeatherIcon.OVERCAST; } if (conditionCode >= 200 && conditionCode < 300) return Weather.WeatherIcon.THUNDERSTORMS; if (conditionCode >= 300 && conditionCode < 400) return Weather.WeatherIcon.LIGHT_RAIN; if (conditionCode >= 500 && conditionCode < 600) return Weather.WeatherIcon.RAIN; if (conditionCode >= 600 && conditionCode < 700) return Weather.WeatherIcon.SNOW; return Weather.WeatherIcon.UNKNOWN; } private void handleException(Request.ResponseParseException e) { LOG.error("Error in weather sending", e); synchronized (this) { // Allow a new sync to start again this.syncInProgress = false; } } // This is to reset the syncInProgress flag when an error occurs private final Request.RequestCallback errorHandler = new Request.RequestCallback() { @Override public void handleException(Request.ResponseParseException e) { HuaweiWeatherManager.this.handleException(e); } }; // This is to reset the syncInProgress flag when everything goes well, or an error occurs // Only for the last request in the chain private final Request.RequestCallback lastHandler = new Request.RequestCallback() { @Override public void call() { synchronized (this) { HuaweiWeatherManager.this.syncInProgress = false; } } @Override public void handleException(Request.ResponseParseException e) { HuaweiWeatherManager.this.handleException(e); } }; private boolean timeOutOfDateInterval(int check, Date start, Date end) { return check * 1000L <= start.getTime() || check * 1000L > end.getTime(); } /** * Some erroneous data will stop the watch from showing any weather data at all, this attempts * to remove such data. */ private void fixupWeather(WeatherSpec weatherSpec) { // Ignore sun/moon data if they are not in the correct day // If this is wrong the watch does not display any weather data at all Date currentDay = DateTimeUtils.dayStart(DateTimeUtils.todayUTC()); Date nextDay = DateTimeUtils.shiftByDays(currentDay, 1); if (timeOutOfDateInterval(weatherSpec.sunRise, currentDay, nextDay)) { LOG.warn("Sun rise for today out of bounds: {}", DateTimeUtils.parseTimeStamp(weatherSpec.sunRise)); weatherSpec.sunRise = 0; } if (timeOutOfDateInterval(weatherSpec.sunSet, currentDay, nextDay)) { LOG.warn("Sun set for today out of bounds: {}", DateTimeUtils.parseTimeStamp(weatherSpec.sunSet)); weatherSpec.sunSet = 0; } if (timeOutOfDateInterval(weatherSpec.moonRise, currentDay, nextDay)) { LOG.warn("Moon rise for today out of bounds: {}", DateTimeUtils.parseTimeStamp(weatherSpec.moonRise)); weatherSpec.moonRise = 0; } if (timeOutOfDateInterval(weatherSpec.moonSet, currentDay, nextDay)) { LOG.warn("Moon set for today out of bounds: {}", DateTimeUtils.parseTimeStamp(weatherSpec.moonSet)); weatherSpec.moonSet = 0; } for (WeatherSpec.Daily point : weatherSpec.forecasts) { currentDay = nextDay; nextDay = DateTimeUtils.shiftByDays(currentDay, 1); if (timeOutOfDateInterval(point.sunRise, currentDay, nextDay)) { LOG.warn("Sun rise for {} out of bounds: {}", currentDay, DateTimeUtils.parseTimeStamp(point.sunRise)); point.sunRise = 0; } if (timeOutOfDateInterval(point.sunSet, currentDay, nextDay)) { LOG.warn("Sun set for {} out of bounds: {}", currentDay, DateTimeUtils.parseTimeStamp(point.sunSet)); point.sunSet = 0; } if (timeOutOfDateInterval(point.moonRise, currentDay, nextDay)) { LOG.warn("Moon rise for {} out of bounds: {}", currentDay, DateTimeUtils.parseTimeStamp(point.moonRise)); point.moonRise = 0; } if (timeOutOfDateInterval(point.moonSet, currentDay, nextDay)) { LOG.warn("Moon set for {} out of bounds: {}", currentDay, DateTimeUtils.parseTimeStamp(point.moonSet)); point.moonSet = 0; } } } public void sendWeather(WeatherSpec weatherSpec) { // Initialize weather settings and send weather if (!supportProvider.getHuaweiCoordinator().supportsWeather()) { LOG.error("onSendWeather called while weather is not supported."); return; } // Do not allow sending weather if it's still going synchronized (this) { if (syncInProgress) { LOG.warn("Weather sync already in progress, ignoring next one"); return; } syncInProgress = true; } fixupWeather(weatherSpec); Weather.Settings weatherSettings = new Weather.Settings(); weatherSettings.uvIndexSupported = supportProvider.getHuaweiCoordinator().supportsWeatherUvIndex(); SendWeatherStartRequest weatherStartRequest = new SendWeatherStartRequest(supportProvider, weatherSettings); weatherStartRequest.setFinalizeReq(errorHandler); weatherStartRequest.setupTimeoutUntilNext(1000); Request lastRequest = weatherStartRequest; if (supportProvider.getHuaweiCoordinator().supportsWeatherUnit()) { SendWeatherUnitRequest weatherUnitRequest = new SendWeatherUnitRequest(supportProvider); weatherUnitRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(weatherUnitRequest); lastRequest = weatherUnitRequest; } SendWeatherSupportRequest weatherSupportRequest = new SendWeatherSupportRequest(supportProvider, weatherSettings); weatherSupportRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(weatherSupportRequest); lastRequest = weatherSupportRequest; if (supportProvider.getHuaweiCoordinator().supportsWeatherExtended()) { SendWeatherExtendedSupportRequest weatherExtendedSupportRequest = new SendWeatherExtendedSupportRequest(supportProvider, weatherSettings); weatherExtendedSupportRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(weatherExtendedSupportRequest); lastRequest = weatherExtendedSupportRequest; } if (supportProvider.getHuaweiCoordinator().supportsWeatherMoonRiseSet()) { SendWeatherSunMoonSupportRequest weatherSunMoonSupportRequest = new SendWeatherSunMoonSupportRequest(supportProvider, weatherSettings); weatherSunMoonSupportRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(weatherSunMoonSupportRequest); lastRequest = weatherSunMoonSupportRequest; } // End of initialization and start of actually sending weather SendWeatherCurrentRequest sendWeatherCurrentRequest = new SendWeatherCurrentRequest(supportProvider, weatherSettings, weatherSpec); sendWeatherCurrentRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(sendWeatherCurrentRequest); lastRequest = sendWeatherCurrentRequest; SendGpsAndTimeToDeviceRequest sendGpsAndTimeToDeviceRequest = new SendGpsAndTimeToDeviceRequest(supportProvider); sendGpsAndTimeToDeviceRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(sendGpsAndTimeToDeviceRequest); lastRequest = sendGpsAndTimeToDeviceRequest; if (supportProvider.getHuaweiCoordinator().supportsWeatherForecasts()) { SendWeatherForecastRequest sendWeatherForecastRequest = new SendWeatherForecastRequest(supportProvider, weatherSettings, weatherSpec); sendWeatherForecastRequest.setFinalizeReq(errorHandler); lastRequest.nextRequest(sendWeatherForecastRequest); lastRequest = sendWeatherForecastRequest; } lastRequest.setFinalizeReq(lastHandler); try { weatherStartRequest.doPerform(); } catch (IOException e) { // TODO: Use translatable string GB.toast(supportProvider.getContext(), "Failed to send weather", Toast.LENGTH_SHORT, GB.ERROR, e); LOG.error("Failed to send weather", e); } } public void handleAsyncMessage(HuaweiPacket response) { if (response.getTlv().getInteger(0x7f, -1) == 0x000186AA) { // Send weather final ArrayList specs = new ArrayList<>(nodomain.freeyourgadget.gadgetbridge.model.Weather.getInstance().getWeatherSpecs()); if (specs.isEmpty()) { LOG.debug("Weather specs empty, returning that weather is disabled."); try { new SendWeatherErrorRequest(supportProvider, Weather.ErrorCode.WEATHER_DISABLED).doPerform(); } catch (IOException e) { LOG.error("Could not send weather error request.", e); } return; } this.sendWeather(specs.get(0)); return; } // Send back ok try { SendWeatherDeviceRequest sendWeatherDeviceRequest = new SendWeatherDeviceRequest(supportProvider); sendWeatherDeviceRequest.doPerform(); } catch (IOException e) { LOG.error("Could not send weather device request", e); } } }