/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.binding.yahooweather.handler;
import static org.eclipse.smarthome.binding.yahooweather.YahooWeatherBindingConstants.*;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.eclipse.smarthome.binding.yahooweather.internal.ExpiringCache;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.config.core.status.ConfigStatusMessage;
import org.eclipse.smarthome.core.library.types.DecimalType;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingStatus;
import org.eclipse.smarthome.core.thing.ThingStatusDetail;
import org.eclipse.smarthome.core.thing.binding.ConfigStatusThingHandler;
import org.eclipse.smarthome.core.types.Command;
import org.eclipse.smarthome.core.types.RefreshType;
import org.eclipse.smarthome.core.types.State;
import org.eclipse.smarthome.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link YahooWeatherHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Kai Kreuzer - Initial contribution
* @author Stefan Bußweiler - Integrate new thing status handling
* @author Thomas Höfer - Added config status provider
*/
public class YahooWeatherHandler extends ConfigStatusThingHandler {
private static final String LOCATION_PARAM = "location";
private final Logger logger = LoggerFactory.getLogger(YahooWeatherHandler.class);
private final int MAX_DATA_AGE = 3 * 60 * 60 * 1000; // 3h
private final int CACHE_EXPIRY = 10 * 1000; // 10s
private long lastUpdateTime;
private BigDecimal location;
private BigDecimal refresh;
private String weatherData = null;
ScheduledFuture<?> refreshJob;
public YahooWeatherHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
logger.debug("Initializing YahooWeather handler.");
Configuration config = getThing().getConfiguration();
location = (BigDecimal) config.get(LOCATION_PARAM);
try {
refresh = (BigDecimal) config.get("refresh");
} catch (Exception e) {
logger.debug("Cannot set refresh parameter.", e);
}
if (refresh == null) {
// let's go for the default
refresh = new BigDecimal(60);
}
startAutomaticRefresh();
}
@Override
public void dispose() {
refreshJob.cancel(true);
}
private void startAutomaticRefresh() {
refreshJob = scheduler.scheduleAtFixedRate(() -> {
try {
boolean success = updateWeatherData();
if (success) {
updateState(new ChannelUID(getThing().getUID(), CHANNEL_TEMPERATURE), getTemperature());
updateState(new ChannelUID(getThing().getUID(), CHANNEL_HUMIDITY), getHumidity());
updateState(new ChannelUID(getThing().getUID(), CHANNEL_PRESSURE), getPressure());
}
} catch (Exception e) {
logger.debug("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
}
}, 0, refresh.intValue(), TimeUnit.SECONDS);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
boolean success = updateWeatherData();
if (success) {
switch (channelUID.getId()) {
case CHANNEL_TEMPERATURE:
updateState(channelUID, getTemperature());
break;
case CHANNEL_HUMIDITY:
updateState(channelUID, getHumidity());
break;
case CHANNEL_PRESSURE:
updateState(channelUID, getPressure());
break;
default:
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
break;
}
}
} else {
logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
}
}
@Override
public Collection<ConfigStatusMessage> getConfigStatus() {
Collection<ConfigStatusMessage> configStatus = new ArrayList<>();
try {
String locationData = getWeatherData(
"SELECT location FROM weather.forecast WHERE woeid = " + location.toPlainString());
String city = getValue(locationData, "location", "city");
if (city == null) {
configStatus.add(ConfigStatusMessage.Builder.error(LOCATION_PARAM)
.withMessageKeySuffix("location-not-found").withArguments(location.toPlainString()).build());
}
} catch (IOException e) {
logger.debug("Communication error occurred while getting Yahoo weather information.", e);
}
return configStatus;
}
private synchronized boolean updateWeatherData() {
try {
String data = getWeatherData(
"SELECT * FROM weather.forecast WHERE u = 'c' AND woeid = " + location.toPlainString());
if (data != null) {
if (data.contains("\"results\":null")) {
if (isCurrentDataExpired()) {
weatherData = null;
logger.trace(
"The Yahoo Weather API did not return any data. Omiting the old result because it became too old.");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.no-data");
return false;
} else {
// simply keep the old data
logger.trace("The Yahoo Weather API did not return any data. Keeping the old result.");
return false;
}
} else {
lastUpdateTime = System.currentTimeMillis();
weatherData = data;
}
updateStatus(ThingStatus.ONLINE);
return true;
}
} catch (IOException e) {
logger.warn("Error accessing Yahoo weather: {}", e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.location [\"" + location.toPlainString() + "\"");
}
weatherData = null;
return false;
}
private boolean isCurrentDataExpired() {
return lastUpdateTime + MAX_DATA_AGE < System.currentTimeMillis();
}
private final ExpiringCache<String, String> CACHE = new ExpiringCache<String, String>(CACHE_EXPIRY,
new ExpiringCache.LoadAction<String, String>() {
@Override
public String load(String query) throws IOException {
try {
URL url = new URL("https://query.yahooapis.com/v1/public/yql?format=json&q="
+ URLEncoder.encode(query, StandardCharsets.UTF_8.toString()));
URLConnection connection = url.openConnection();
return IOUtils.toString(connection.getInputStream());
} catch (MalformedURLException e) {
logger.debug("Constructed query url '{}' is not valid: {}", query, e.getMessage());
throw e;
}
}
});
private String getWeatherData(String query) throws IOException {
return CACHE.get(query);
}
private State getHumidity() {
if (weatherData != null) {
String humidity = getValue(weatherData, "atmosphere", "humidity");
if (humidity != null) {
return new DecimalType(humidity);
}
}
return UnDefType.UNDEF;
}
private State getPressure() {
if (weatherData != null) {
String pressure = getValue(weatherData, "atmosphere", "pressure");
if (pressure != null) {
DecimalType ret = new DecimalType(pressure);
if (ret.doubleValue() > 10000.0) {
// Unreasonably high, record so far was 1085,8 hPa
// The Yahoo API currently returns inHg values although it claims they are mbar - therefore convert
ret = new DecimalType(BigDecimal.valueOf((long) (ret.doubleValue() / 0.3386388158), 2));
}
return ret;
}
}
return UnDefType.UNDEF;
}
private State getTemperature() {
if (weatherData != null) {
String temp = getValue(weatherData, "condition", "temp");
if (temp != null) {
return new DecimalType(temp);
}
}
return UnDefType.UNDEF;
}
private String getValue(String data, String element, String param) {
String tmp = StringUtils.substringAfter(data, element);
if (tmp != null) {
return StringUtils.substringBetween(tmp, param + "\":\"", "\"");
}
return null;
}
}