/**
* 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.nest.internal.messages;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.net.URLDecoder;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.beanutils.BeanUtilsBean;
import org.apache.commons.beanutils.ConvertUtilsBean;
import org.apache.commons.beanutils.Converter;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.beanutils.PropertyUtilsBean;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.openhab.binding.nest.internal.messages.SmokeCOAlarm.BatteryHealth;
import org.openhab.binding.nest.internal.messages.SmokeCOAlarm.ColorState;
import org.openhab.binding.nest.internal.messages.Structure.AwayState;
import org.openhab.binding.nest.internal.messages.Thermostat.HvacMode;
import org.openhab.binding.nest.internal.messages.Thermostat.HvacState;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
/**
* The DataModel Java Bean represents the entire Nest API data model.
*
* @see <a href="https://developer.nest.com/documentation/api-reference">API Reference</a>
* @author John Cocula
* @since 1.7.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class DataModel extends AbstractMessagePart {
private static BeanUtilsBean beanUtils;
private static PropertyUtilsBean propertyUtils;
static {
/**
* Configure BeanUtilsBean to use our converters and resolver.
*/
ConvertUtilsBean convertUtils = new ConvertUtilsBean();
// Register bean type converters
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return AwayState.forValue(value.toString());
} else {
return null;
}
}
}, AwayState.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return HvacMode.forValue(value.toString());
} else {
return null;
}
}
}, HvacMode.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return HvacState.forValue(value.toString());
} else {
return null;
}
}
}, HvacState.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return BatteryHealth.forValue(value.toString());
} else {
return null;
}
}
}, BatteryHealth.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return AlarmState.forValue(value.toString());
} else {
return null;
}
}
}, AlarmState.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof StringType) {
return ColorState.forValue(value.toString());
} else {
return null;
}
}
}, ColorState.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof DecimalType) {
return ((DecimalType) value).toBigDecimal();
} else {
return null;
}
}
}, BigDecimal.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof DecimalType) {
return ((DecimalType) value).intValue();
} else {
return null;
}
}
}, Integer.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
if (value instanceof OnOffType) {
return ((OnOffType) value) == OnOffType.ON;
} else {
return null;
}
}
}, Boolean.class);
convertUtils.register(new Converter() {
@SuppressWarnings("rawtypes")
@Override
public Object convert(Class type, Object value) {
return value.toString();
}
}, String.class);
propertyUtils = new PropertyUtilsBean();
propertyUtils.setResolver(new DataModelPropertyResolver());
beanUtils = new BeanUtilsBean(convertUtils, propertyUtils);
}
@JsonProperty("devices")
private Devices devices;
@JsonProperty("structures")
private Map<String, Structure> structures_by_id;
@JsonIgnore
private Map<String, Structure> structures_by_name;
@JsonIgnore
Date last_connection;
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Devices extends AbstractMessagePart implements DataModelElement {
private Map<String, Thermostat> thermostats_by_id;
@JsonIgnore
private Map<String, Thermostat> thermostats_by_name;
private Map<String, SmokeCOAlarm> smoke_co_alarms_by_id;
@JsonIgnore
private Map<String, SmokeCOAlarm> smoke_co_alarms_by_name;
private Map<String, Camera> cameras_by_id;
@JsonIgnore
private Map<String, Camera> cameras_by_name;
/**
* @return the thermostats_by_id
*/
@JsonProperty("thermostats")
public Map<String, Thermostat> getThermostats_by_id() {
return this.thermostats_by_id;
}
/**
* @param thermostats
* the thermostats to set (mapped by ID)
*/
@JsonProperty("thermostats")
public void setThermostats_by_id(Map<String, Thermostat> thermostats_by_id) {
this.thermostats_by_id = thermostats_by_id;
}
/**
* Return the thermostats map, mapped by name.
*
* @return the thermostats_by_name;
*/
@JsonIgnore
public Map<String, Thermostat> getThermostats() {
return this.thermostats_by_name;
}
/**
* @return the smoke_co_alarms_by_id
*/
@JsonProperty("smoke_co_alarms")
public Map<String, SmokeCOAlarm> getSmoke_co_alarms_by_id() {
return this.smoke_co_alarms_by_id;
}
/**
* @param smoke_co_alarms
* the smoke_co_alarms to set (mapped by ID)
*/
@JsonProperty("smoke_co_alarms")
public void setSmoke_co_alarms_by_id(Map<String, SmokeCOAlarm> smoke_co_alarms_by_id) {
this.smoke_co_alarms_by_id = smoke_co_alarms_by_id;
}
/**
* @return the smoke_co_alarms_by_name
*/
@JsonIgnore
public Map<String, SmokeCOAlarm> getSmoke_co_alarms() {
return this.smoke_co_alarms_by_name;
}
/**
* @return the cameras_by_id
*/
@JsonProperty("cameras")
public Map<String, Camera> getCameras_by_id() {
return this.cameras_by_id;
}
/**
* @param cameras_by_id
* the cameras to set (mapped by ID)
*/
@JsonProperty("cameras")
public void setCameras_by_id(Map<String, Camera> cameras_by_id) {
this.cameras_by_id = cameras_by_id;
}
/**
* @return the cameras_by_name
*/
@JsonIgnore
public Map<String, Camera> getCameras() {
return this.cameras_by_name;
}
/**
* {@inheritDoc}
*/
@Override
public void sync(DataModel dataModel) {
// Create our map of Thermostat names to objects
this.thermostats_by_name = new HashMap<String, Thermostat>();
if (this.thermostats_by_id != null) {
for (Thermostat thermostat : this.thermostats_by_id.values()) {
thermostat.sync(dataModel);
this.thermostats_by_name.put(thermostat.getName(), thermostat);
}
}
// Create our map of SmokeCOAlarm names to objects
this.smoke_co_alarms_by_name = new HashMap<String, SmokeCOAlarm>();
if (this.smoke_co_alarms_by_id != null) {
for (SmokeCOAlarm smoke_co_alarm : this.smoke_co_alarms_by_id.values()) {
smoke_co_alarm.sync(dataModel);
this.smoke_co_alarms_by_name.put(smoke_co_alarm.getName(), smoke_co_alarm);
}
}
// Create our map of Camera names to objects
this.cameras_by_name = new HashMap<String, Camera>();
if (this.cameras_by_id != null) {
for (Camera camera : this.cameras_by_id.values()) {
camera.sync(dataModel);
this.cameras_by_name.put(camera.getName(), camera);
}
}
}
@Override
public String toString() {
final ToStringBuilder builder = createToStringBuilder();
builder.appendSuper(super.toString());
builder.append("thermostats", this.thermostats_by_id);
builder.append("smoke_co_alarms", this.smoke_co_alarms_by_id);
builder.append("cameras", this.cameras_by_id);
return builder.toString();
}
}
public DataModel() {
}
/**
* Use a dialect of the BeanUtils property resolver that URL-decodes the keys that are used to retrieve mapped
* objects.
*/
public static class DataModelPropertyResolver extends org.apache.commons.beanutils.expression.DefaultResolver {
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("deprecation")
public String getKey(String expression) {
String key = super.getKey(expression);
if (key != null) {
try {
return URLDecoder.decode(key, "UTF-8");
} catch (UnsupportedEncodingException ex) {
return URLDecoder.decode(key);
}
}
return null;
}
}
/**
* Return a JavaBean property by name.
*
* @param name
* the named property to return
* @return the named property's value
* @see PropertyUtils#getProperty()
* @throws IllegalAccessException
* if the caller does not have access to the property accessor method
* @throws InvocationTargetException
* if the property accessor method throws an exception
* @throws NoSuchMethodException
* if the accessor method for this property cannot be found
*/
public Object getProperty(String name)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
return propertyUtils.getProperty(this, name);
}
/**
* Set the specified property value, performing type conversions as required to conform to the type of the
* destination property.
*
* @param name
* property name (can be nested/indexed/mapped/combo)
* @param value
* value to be set
* @throws IllegalAccessException
* if the caller does not have access to the property accessor method
* @throws InvocationTargetException
* if the property accessor method throws an exception
*/
public void setProperty(String name, Object value) throws IllegalAccessException, InvocationTargetException {
beanUtils.setProperty(this, name, value);
}
/**
* @return the devices
*/
@JsonProperty("devices")
public Devices getDevices() {
return this.devices;
}
/**
* @param devices
* the devices to set
*/
@JsonProperty("devices")
public void setDevices(Devices devices) {
this.devices = devices;
}
/**
* Convenience method so property specs don't have to include "devices." in each one.
*
* @return name-based map of thermostats
*/
@JsonIgnore
public Map<String, Thermostat> getThermostats() {
return (devices == null) ? null : devices.getThermostats();
}
/**
* Convenience method so property specs don't have to include "devices." in each one.
*
* @return name-based map of smoke_co_alarms
*/
@JsonIgnore
public Map<String, SmokeCOAlarm> getSmoke_co_alarms() {
return (devices == null) ? null : devices.getSmoke_co_alarms();
}
/**
* Convenience method so property specs don't have to include "devices." in each one.
*
* @return name-based map of cameras
*/
@JsonIgnore
public Map<String, Camera> getCameras() {
return (devices == null) ? null : devices.getCameras();
}
/**
* @return the structures
*/
@JsonProperty("structures")
public Map<String, Structure> getStructures_by_id() {
return this.structures_by_id;
}
/**
* @param structures_by_id
* the ID-keyed structure map to set
*/
@JsonProperty("structures")
public void setStructures_by_id(Map<String, Structure> structures_by_id) {
this.structures_by_id = structures_by_id;
}
@JsonIgnore
public Map<String, Structure> getStructures() {
return this.structures_by_name;
}
/**
* @param structures_by_name
* the name-keyed structure map to set
*/
public void setStructures_by_name(final Map<String, Structure> structures_by_name) {
this.structures_by_name = structures_by_name;
}
/**
* @param last_connection
* the last time we obtained the data model from the Nest API
*/
@JsonIgnore
public void setLast_connection(final Date last_connection) {
this.last_connection = last_connection;
}
/**
* @return the last_connection
*/
@JsonIgnore
public Date getLast_connection() {
return this.last_connection;
}
/**
* Visit each data model element and call its sync method with the root data model element (this). Do this right
* after it's been unmarshalled from JSON.
*/
public void sync() {
this.structures_by_name = new HashMap<String, Structure>();
if (this.structures_by_id != null) {
for (Structure st : this.structures_by_id.values()) {
this.structures_by_name.put(st.getName(), st);
st.sync(this);
}
}
if (this.devices != null) {
this.devices.sync(this);
}
}
/**
* This method returns a new data model containing only the affected Structure, Thermostat, SmokeCOAlarm or Camera,
* and only the property of the bean that was changed. This new DataModel object can be sent to the Nest API in
* order to perform an update via HTTP PUT.
*
* @param property
* the property to change
* @param newState
* the new state to set for the property
* @return a new data model that only contains the affected bean and only the property set
* @throws IllegalAccessException
* @throws InvocationTargetException
* @throws NoSuchMethodException
*/
public DataModel updateDataModel(String property, Object newState)
throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
/**
* Find the Structure, Thermostat, SmokeCOAlarm or Camera that the given property is trying to update.
*/
Object oldObject = null;
String beanProperty = property;
do {
Object obj = this.getProperty(beanProperty);
if (obj instanceof Structure || obj instanceof Thermostat || obj instanceof SmokeCOAlarm
|| obj instanceof Camera) {
oldObject = obj;
break;
}
if (beanProperty.indexOf('.') != -1) {
beanProperty = beanProperty.substring(0, beanProperty.lastIndexOf('.'));
} else {
break;
}
} while (beanProperty.length() > 0);
/**
* Now based on the type of the object, create a new DataModel that has an empty one, properly mapped by ID and
* by name.
*/
DataModel updateDataModel = null;
if (oldObject != null) {
if (oldObject instanceof Structure) {
String structureId = ((Structure) oldObject).getStructure_id();
String structureName = ((Structure) oldObject).getName();
updateDataModel = new DataModel();
Structure structure = new Structure(null);
updateDataModel.structures_by_id = new HashMap<String, Structure>();
updateDataModel.structures_by_id.put(structureId, structure);
updateDataModel.structures_by_name = new HashMap<String, Structure>();
updateDataModel.structures_by_name.put(structureName, structure);
} else if (oldObject instanceof Thermostat) {
String deviceId = ((Thermostat) oldObject).getDevice_id();
String deviceName = ((Thermostat) oldObject).getName();
updateDataModel = new DataModel();
updateDataModel.devices = new Devices();
Thermostat thermostat = new Thermostat(null);
updateDataModel.devices.thermostats_by_id = new HashMap<String, Thermostat>();
updateDataModel.devices.thermostats_by_id.put(deviceId, thermostat);
updateDataModel.devices.thermostats_by_name = new HashMap<String, Thermostat>();
updateDataModel.devices.thermostats_by_name.put(deviceName, thermostat);
} else if (oldObject instanceof SmokeCOAlarm) {
String deviceId = ((SmokeCOAlarm) oldObject).getDevice_id();
String deviceName = ((SmokeCOAlarm) oldObject).getName();
updateDataModel = new DataModel();
updateDataModel.devices = new Devices();
SmokeCOAlarm smokeCOAlarm = new SmokeCOAlarm(null);
updateDataModel.devices.smoke_co_alarms_by_id = new HashMap<String, SmokeCOAlarm>();
updateDataModel.devices.smoke_co_alarms_by_id.put(deviceId, smokeCOAlarm);
updateDataModel.devices.smoke_co_alarms_by_name = new HashMap<String, SmokeCOAlarm>();
updateDataModel.devices.smoke_co_alarms_by_name.put(deviceName, smokeCOAlarm);
} else if (oldObject instanceof Camera) {
String deviceId = ((Camera) oldObject).getDevice_id();
String deviceName = ((Camera) oldObject).getName();
updateDataModel = new DataModel();
updateDataModel.devices = new Devices();
Camera camera = new Camera(null);
updateDataModel.devices.cameras_by_id = new HashMap<String, Camera>();
updateDataModel.devices.cameras_by_id.put(deviceId, camera);
updateDataModel.devices.cameras_by_name = new HashMap<String, Camera>();
updateDataModel.devices.cameras_by_name.put(deviceName, camera);
}
}
/**
* Lastly, set the property into the update data model
*
* TODO: cannot update a binding string of the form "=[structures(Name).thermostats(Name).X]" or
* "=[structures(Name).smoke_co_alarms(Name).X]" because the name-based map of structures is not present in the
* updateDataModel
*/
if (updateDataModel != null) {
updateDataModel.setProperty(property, newState);
updateDataModel.structures_by_name = null;
if (updateDataModel.devices != null) {
updateDataModel.devices.smoke_co_alarms_by_name = null;
updateDataModel.devices.thermostats_by_name = null;
updateDataModel.devices.cameras_by_name = null;
}
}
return updateDataModel;
}
@Override
public String toString() {
final ToStringBuilder builder = createToStringBuilder();
builder.appendSuper(super.toString());
builder.append("devices", this.devices);
builder.append("structures", this.structures_by_id);
return builder.toString();
}
}