/**
* Copyright (c) 2010-2016 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.openhab.binding.netatmo.internal.weather;
import static org.openhab.binding.netatmo.internal.weather.MeasurementRequest.createKey;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.openhab.binding.netatmo.NetatmoBindingProvider;
import org.openhab.binding.netatmo.internal.NetatmoBinding;
import org.openhab.binding.netatmo.internal.NetatmoException;
import org.openhab.binding.netatmo.internal.authentication.OAuthCredentials;
import org.openhab.binding.netatmo.internal.messages.NetatmoError;
import org.openhab.binding.netatmo.internal.weather.GetStationsDataResponse.Device;
import org.openhab.binding.netatmo.internal.weather.GetStationsDataResponse.Module;
import org.openhab.core.events.EventPublisher;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Binding that gets measurements from the Netatmo API every couple of minutes.
*
* @author Andreas Brenk
* @author Thomas.Eichstaedt-Engelen
* @author Gaƫl L'hopital
* @author Rob Nielsen
* @author Ing. Peter Weiss
* @since 1.4.0
*/
public class NetatmoWeatherBinding {
private static final String WIND = "Wind";
private static final Logger logger = LoggerFactory.getLogger(NetatmoWeatherBinding.class);
protected static final String CONFIG_PRESSURE_UNIT = "pressureunit";
protected static final String CONFIG_UNIT_SYSTEM = "unitsystem";
private Map<Device, PointType> stationPositions = new HashMap<Device, PointType>();
private NetatmoPressureUnit pressureUnit = NetatmoPressureUnit.DEFAULT_PRESSURE_UNIT;
private NetatmoUnitSystem unitSystem = NetatmoUnitSystem.DEFAULT_UNIT_SYSTEM;
/**
* Execute the weather binding from Netatmo Binding Class
*
* @param oauthCredentials
* @param providers
* @param eventPublisher
*/
public void execute(OAuthCredentials oauthCredentials, Collection<NetatmoBindingProvider> providers,
EventPublisher eventPublisher) {
logger.debug("Querying Netatmo Weather API");
try {
GetStationsDataResponse stationsDataResponse = processGetStationsData(
oauthCredentials, providers, eventPublisher);
if (stationsDataResponse == null) {
return;
}
DeviceMeasureValueMap deviceMeasureValueMap = processMeasurements(oauthCredentials, providers,
eventPublisher);
if (deviceMeasureValueMap == null) {
return;
}
for (final NetatmoBindingProvider provider : providers) {
for (final String itemName : provider.getItemNames()) {
final String deviceId = provider.getDeviceId(itemName);
final String moduleId = provider.getModuleId(itemName);
final NetatmoMeasureType measureType = provider.getMeasureType(itemName);
final NetatmoScale scale = provider.getNetatmoScale(itemName);
State state = null;
if (measureType != null) {
switch (measureType) {
case MODULENAME:
if (moduleId == null) {
for (Device device : stationsDataResponse.getDevices()) {
if (device.getId().equals(deviceId)) {
state = new StringType(device.getModuleName());
break;
}
}
} else {
for (Device device : stationsDataResponse.getDevices()) {
for (Module module : device.getModules()) {
if (module.getId().equals(moduleId)) {
state = new StringType(module.getModuleName());
break;
}
}
}
}
break;
case TIMESTAMP:
state = deviceMeasureValueMap.timeStamp;
break;
case TEMPERATURE:
case CO2:
case HUMIDITY:
case NOISE:
case PRESSURE:
case RAIN:
case MIN_TEMP:
case MAX_TEMP:
case MIN_HUM:
case MAX_HUM:
case MIN_PRESSURE:
case MAX_PRESSURE:
case MIN_NOISE:
case MAX_NOISE:
case MIN_CO2:
case MAX_CO2:
case SUM_RAIN:
case WINDSTRENGTH:
case WINDANGLE:
case GUSTSTRENGTH:
case GUSTANGLE: {
BigDecimal value = getValue(deviceMeasureValueMap, measureType,
createKey(deviceId, moduleId, scale));
// Protect that sometimes Netatmo returns null where
// numeric value is awaited (issue #1848)
if (value != null) {
if (NetatmoMeasureType.isTemperature(measureType)) {
value = unitSystem.convertTemp(value);
} else if (NetatmoMeasureType.isRain(measureType)) {
value = unitSystem.convertRain(value);
} else if (NetatmoMeasureType.isPressure(measureType)) {
value = pressureUnit.convertPressure(value);
} else if (NetatmoMeasureType.isWind(measureType)) {
value = unitSystem.convertWind(value);
}
state = new DecimalType(value);
}
}
break;
case DATE_MIN_TEMP:
case DATE_MAX_TEMP:
case DATE_MIN_HUM:
case DATE_MAX_HUM:
case DATE_MIN_PRESSURE:
case DATE_MAX_PRESSURE:
case DATE_MIN_NOISE:
case DATE_MAX_NOISE:
case DATE_MIN_CO2:
case DATE_MAX_CO2:
case DATE_MAX_GUST: {
final BigDecimal value = getValue(deviceMeasureValueMap, measureType,
createKey(deviceId, moduleId, scale));
if (value != null) {
final Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(value.longValue() * 1000);
state = new DateTimeType(calendar);
}
}
break;
case BATTERYPERCENT:
case BATTERYSTATUS:
case BATTERYVP:
case RFSTATUS:
for (Device device : stationsDataResponse.getDevices()) {
for (Module module : device.getModules()) {
if (module.getId().equals(moduleId)) {
switch (measureType) {
case BATTERYPERCENT:
case BATTERYVP:
state = new DecimalType(module.getBatteryPercentage());
break;
case BATTERYSTATUS:
state = new DecimalType(module.getBatteryLevel());
break;
case RFSTATUS:
state = new DecimalType(module.getRfLevel());
break;
case MODULENAME:
state = new StringType(module.getModuleName());
break;
default:
break;
}
}
}
}
break;
case ALTITUDE:
case LATITUDE:
case LONGITUDE:
case WIFISTATUS:
case COORDINATE:
case STATIONNAME:
for (Device device : stationsDataResponse.getDevices()) {
if (device.getId().equals(deviceId)) {
if (stationPositions.get(device) == null) {
DecimalType altitude = DecimalType.ZERO;
if (device.getAltitude() != null) {
altitude = new DecimalType(device.getAltitude());
}
stationPositions.put(device,
new PointType(
new DecimalType(new BigDecimal(device.getLatitude())
.setScale(6, BigDecimal.ROUND_HALF_UP)),
new DecimalType(new BigDecimal(device.getLongitude()).setScale(6,
BigDecimal.ROUND_HALF_UP)), altitude));
}
switch (measureType) {
case LATITUDE:
state = stationPositions.get(device).getLatitude();
break;
case LONGITUDE:
state = stationPositions.get(device).getLongitude();
break;
case ALTITUDE:
state = new DecimalType(Math.round(unitSystem.convertAltitude(
stationPositions.get(device).getAltitude().doubleValue())));
break;
case WIFISTATUS:
state = new DecimalType(device.getWifiLevel());
break;
case COORDINATE:
state = stationPositions.get(device);
break;
case STATIONNAME:
state = new StringType(device.getStationName());
break;
default:
break;
}
}
}
break;
}
}
if (state != null) {
eventPublisher.postUpdate(itemName, state);
}
}
}
} catch (NetatmoException ne) {
logger.error(ne.getMessage());
}
}
private BigDecimal getValue(DeviceMeasureValueMap deviceMeasureValueMap, final NetatmoMeasureType measureType,
final String requestKey) {
Map<String, BigDecimal> map = deviceMeasureValueMap.get(requestKey);
return map != null ? map.get(measureType.getMeasure()) : null;
}
static class DeviceMeasureValueMap extends HashMap<String, Map<String, BigDecimal>> {
/**
*
*/
private static final long serialVersionUID = 1L;
DateTimeType timeStamp = null;
}
private DeviceMeasureValueMap processMeasurements(OAuthCredentials oauthCredentials,
Collection<NetatmoBindingProvider> providers, EventPublisher eventPublisher) {
DeviceMeasureValueMap deviceMeasureValueMap = new DeviceMeasureValueMap();
for (final MeasurementRequest request : createMeasurementRequests(providers)) {
final MeasurementResponse response = request.execute();
logger.debug("Request: {}", request);
logger.debug("Response: {}", response);
if (response.isError()) {
final NetatmoError error = response.getError();
if (error.isAccessTokenExpired() || error.isTokenNotVaid()) {
logger.debug("Token is expired or is not valid, refreshing: code = {} message = {}",
error.getCode(), error.getMessage());
oauthCredentials.refreshAccessToken();
execute(oauthCredentials, providers, eventPublisher);
return null;
} else {
logger.error("Error sending measurement request: code = {} message = {}", error.getCode(),
error.getMessage());
throw new NetatmoException(error.getMessage());
}
} else {
processMeasurementResponse(request, response, deviceMeasureValueMap);
}
}
return deviceMeasureValueMap;
}
private GetStationsDataResponse processGetStationsData(OAuthCredentials oauthCredentials, Collection<NetatmoBindingProvider> providers,
EventPublisher eventPublisher) {
GetStationsDataRequest stationsDataRequest = new GetStationsDataRequest(oauthCredentials.getAccessToken());
logger.debug("Request: {}", stationsDataRequest);
GetStationsDataResponse stationsDataResponse = stationsDataRequest.execute();
logger.debug("Response: {}", stationsDataResponse);
if (stationsDataResponse.isError()) {
final NetatmoError error = stationsDataResponse.getError();
if (error.isAccessTokenExpired() || error.isTokenNotVaid()) {
logger.debug("Token is expired or is not valid, refreshing: code = {} message = {}", error.getCode(),
error.getMessage());
oauthCredentials.refreshAccessToken();
execute(oauthCredentials, providers, eventPublisher);
} else {
logger.error("Error processing device list: code = {} message = {}", error.getCode(),
error.getMessage());
throw new NetatmoException(error.getMessage());
}
return null; // abort processing
} else {
processGetStationsDataResponse(stationsDataResponse, providers);
}
return stationsDataResponse;
}
/**
* Processes an incoming {@link GetStationsDataResponse}.
* <p>
*/
private void processGetStationsDataResponse(final GetStationsDataResponse response,
Collection<NetatmoBindingProvider> providers) {
// Prepare a map of all known device measurements
final Map<String, Device> deviceMap = new HashMap<String, Device>();
final Map<String, Set<String>> deviceMeasurements = new HashMap<String, Set<String>>();
for (final Device device : response.getDevices()) {
final String deviceId = device.getId();
deviceMap.put(deviceId, device);
for (final String measurement : device.getMeasurements()) {
if (!deviceMeasurements.containsKey(deviceId)) {
deviceMeasurements.put(deviceId, new HashSet<String>());
}
deviceMeasurements.get(deviceId).add(measurement);
}
}
// Prepare a map of all known module measurements
final Map<String, Module> moduleMap = new HashMap<String, Module>();
final Map<String, Set<String>> moduleMeasurements = new HashMap<String, Set<String>>();
final Map<String, String> mainDeviceMap = new HashMap<String, String>();
for (final Device device : response.getDevices()) {
final String deviceId = device.getId();
for (final Module module : device.getModules()) {
final String moduleId = module.getId();
moduleMap.put(moduleId, module);
for (final String measurement : module.getMeasurements()) {
if (!moduleMeasurements.containsKey(moduleId)) {
moduleMeasurements.put(moduleId, new HashSet<String>());
mainDeviceMap.put(moduleId, deviceId);
}
moduleMeasurements.get(moduleId).add(measurement);
}
}
}
// Remove all configured items from the maps
for (final NetatmoBindingProvider provider : providers) {
for (final String itemName : provider.getItemNames()) {
final String deviceId = provider.getDeviceId(itemName);
final String moduleId = provider.getModuleId(itemName);
final NetatmoMeasureType measureType = provider.getMeasureType(itemName);
final Set<String> measurements;
if (moduleId != null) {
measurements = moduleMeasurements.get(moduleId);
} else {
measurements = deviceMeasurements.get(deviceId);
}
if (measurements != null) {
String measure = measureType != NetatmoMeasureType.WINDSTRENGTH ? measureType.getMeasure() : WIND;
measurements.remove(measure);
}
}
}
// Log all unconfigured measurements
final StringBuilder message = new StringBuilder();
for (Entry<String, Set<String>> entry : deviceMeasurements.entrySet()) {
final String deviceId = entry.getKey();
final Device device = deviceMap.get(deviceId);
for (String measurement : entry.getValue()) {
message.append("\t" + deviceId + "#" + measurement + " (" + device.getModuleName() + ")\n");
}
}
for (Entry<String, Set<String>> entry : moduleMeasurements.entrySet()) {
final String moduleId = entry.getKey();
final Module module = moduleMap.get(moduleId);
for (String measurement : entry.getValue()) {
if (measurement.equals(WIND)) {
measurement = NetatmoMeasureType.WINDSTRENGTH.toString().toLowerCase();
}
message.append("\t" + mainDeviceMap.get(moduleId) + "#" + moduleId + "#" + measurement + " ("
+ module.getModuleName() + ")\n");
}
}
if (message.length() > 0) {
message.insert(0, "The following Netatmo measurements are not yet configured:\n");
logger.info(message.toString());
}
}
/**
* Creates the necessary requests to query the Netatmo API for all measures
* that have a binding. One request can query all measures of a single
* device or module.
*/
private Collection<MeasurementRequest> createMeasurementRequests(Collection<NetatmoBindingProvider> providers) {
final Map<String, MeasurementRequest> requests = new HashMap<String, MeasurementRequest>();
for (final NetatmoBindingProvider provider : providers) {
for (final String itemName : provider.getItemNames()) {
final NetatmoMeasureType measureType = provider.getMeasureType(itemName);
if (measureType != null) {
switch (measureType) {
case TEMPERATURE:
case CO2:
case HUMIDITY:
case NOISE:
case PRESSURE:
case RAIN:
case MIN_TEMP:
case MAX_TEMP:
case MIN_HUM:
case MAX_HUM:
case MIN_PRESSURE:
case MAX_PRESSURE:
case MIN_NOISE:
case MAX_NOISE:
case MIN_CO2:
case MAX_CO2:
case SUM_RAIN:
case DATE_MIN_TEMP:
case DATE_MAX_TEMP:
case DATE_MIN_HUM:
case DATE_MAX_HUM:
case DATE_MIN_PRESSURE:
case DATE_MAX_PRESSURE:
case DATE_MIN_NOISE:
case DATE_MAX_NOISE:
case DATE_MIN_CO2:
case DATE_MAX_CO2:
case WINDSTRENGTH:
case WINDANGLE:
case GUSTSTRENGTH:
case GUSTANGLE:
case DATE_MAX_GUST:
final NetatmoScale scale = provider.getNetatmoScale(itemName);
addMeasurement(requests, provider, itemName, measureType, scale);
break;
default:
break;
}
}
}
}
return requests.values();
}
private void addMeasurement(final Map<String, MeasurementRequest> requests, final NetatmoBindingProvider provider,
final String itemName, final NetatmoMeasureType measureType, final NetatmoScale scale) {
final String userid = provider.getUserid(itemName);
final OAuthCredentials oauthCredentials = NetatmoBinding.getOAuthCredentials(userid);
if (oauthCredentials != null) {
final String deviceId = provider.getDeviceId(itemName);
final String moduleId = provider.getModuleId(itemName);
final String requestKey = createKey(deviceId, moduleId, scale);
if (!requests.containsKey(requestKey)) {
requests.put(requestKey,
new MeasurementRequest(oauthCredentials.getAccessToken(), deviceId, moduleId, scale));
}
requests.get(requestKey).addMeasure(measureType);
}
}
private void processMeasurementResponse(final MeasurementRequest request, final MeasurementResponse response,
DeviceMeasureValueMap deviceMeasureValueMap) {
final List<BigDecimal> values = response.getBody().get(0).getValues().get(0);
Map<String, BigDecimal> valueMap = deviceMeasureValueMap.get(request.getKey());
if (valueMap == null) {
valueMap = new HashMap<String, BigDecimal>();
deviceMeasureValueMap.put(request.getKey(), valueMap);
deviceMeasureValueMap.timeStamp = new DateTimeType(response.getTimeStamp());
}
int index = 0;
for (final String measure : request.getMeasures()) {
final BigDecimal value = values.get(index);
valueMap.put(measure, value);
index++;
}
}
public NetatmoPressureUnit getPressureUnit() {
return pressureUnit;
}
public void setPressureUnit(NetatmoPressureUnit pressureUnit) {
this.pressureUnit = pressureUnit;
}
public NetatmoUnitSystem getUnitSystem() {
return unitSystem;
}
public void setUnitSystem(NetatmoUnitSystem unitSystem) {
this.unitSystem = unitSystem;
}
}