diff --git a/bundles/binding/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/messages/DataModel.java b/bundles/binding/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/messages/DataModel.java index 5a35bcacf55..b127b2bd3ac 100644 --- a/bundles/binding/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/messages/DataModel.java +++ b/bundles/binding/org.openhab.binding.nest/src/main/java/org/openhab/binding/nest/internal/messages/DataModel.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2015, openHAB.org and others. + * Copyright (c) 2010-2016, openHAB.org and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 @@ -10,6 +10,7 @@ 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; @@ -18,24 +19,25 @@ 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.Structure.AwayState; -import org.openhab.binding.nest.internal.messages.Thermostat.HvacMode; -import org.openhab.binding.nest.internal.messages.Thermostat.HvacState; import org.openhab.binding.nest.internal.messages.SmokeCOAlarm.AlarmState; 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 API Reference * @author John Cocula * @since 1.7.0 @@ -43,547 +45,560 @@ @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).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 structures_by_id; - @JsonIgnore - private Map structures_by_name; - @JsonIgnore - Date last_connection; - - @JsonIgnoreProperties(ignoreUnknown = true) - public static class Devices extends AbstractMessagePart implements DataModelElement { - private Map thermostats_by_id; - @JsonIgnore - private Map thermostats_by_name; - private Map smoke_co_alarms_by_id; - @JsonIgnore - private Map smoke_co_alarms_by_name; - private Map cameras_by_id; - @JsonIgnore - private Map cameras_by_name; - - /** - * @return the thermostats_by_id - */ - @JsonProperty("thermostats") - public Map 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 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 getThermostats() { - return this.thermostats_by_name; - } - - /** - * @return the smoke_co_alarms_by_id - */ - @JsonProperty("smoke_co_alarms") - public Map 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 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 getSmoke_co_alarms() { - return this.smoke_co_alarms_by_name; - } - - /** - * @return the cameras_by_id - */ - @JsonProperty("cameras") - public Map 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 cameras_by_id) { - this.cameras_by_id = cameras_by_id; - } - - /** - * @return the cameras_by_name - */ - @JsonIgnore - public Map 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(); - 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(); - 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(); - 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} - */ - @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 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 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 getCameras() { - return (devices == null) ? null : devices.getCameras(); - } - - /** - * @return the structures - */ - @JsonProperty("structures") - public Map 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 structures_by_id) { - this.structures_by_id = structures_by_id; - } - - @JsonIgnore - public Map getStructures() { - return this.structures_by_name; - } - - /** - * @param structures_by_name - * the name-keyed structure map to set - */ - public void setStructures_by_name(final Map 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(); - 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(); - updateDataModel.structures_by_id.put(structureId, structure); - updateDataModel.structures_by_name = new HashMap(); - 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(); - updateDataModel.devices.thermostats_by_id.put(deviceId, thermostat); - updateDataModel.devices.thermostats_by_name = new HashMap(); - 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(); - updateDataModel.devices.smoke_co_alarms_by_id.put(deviceId, smokeCOAlarm); - updateDataModel.devices.smoke_co_alarms_by_name = new HashMap(); - 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(); - updateDataModel.devices.cameras_by_id.put(deviceId, camera); - updateDataModel.devices.cameras_by_name = new HashMap(); - 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(); - } + 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 structures_by_id; + @JsonIgnore + private Map structures_by_name; + @JsonIgnore + Date last_connection; + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Devices extends AbstractMessagePart implements DataModelElement { + private Map thermostats_by_id; + @JsonIgnore + private Map thermostats_by_name; + private Map smoke_co_alarms_by_id; + @JsonIgnore + private Map smoke_co_alarms_by_name; + private Map cameras_by_id; + @JsonIgnore + private Map cameras_by_name; + + /** + * @return the thermostats_by_id + */ + @JsonProperty("thermostats") + public Map 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 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 getThermostats() { + return this.thermostats_by_name; + } + + /** + * @return the smoke_co_alarms_by_id + */ + @JsonProperty("smoke_co_alarms") + public Map 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 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 getSmoke_co_alarms() { + return this.smoke_co_alarms_by_name; + } + + /** + * @return the cameras_by_id + */ + @JsonProperty("cameras") + public Map 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 cameras_by_id) { + this.cameras_by_id = cameras_by_id; + } + + /** + * @return the cameras_by_name + */ + @JsonIgnore + public Map 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(); + 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(); + 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(); + 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 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 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 getCameras() { + return (devices == null) ? null : devices.getCameras(); + } + + /** + * @return the structures + */ + @JsonProperty("structures") + public Map 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 structures_by_id) { + this.structures_by_id = structures_by_id; + } + + @JsonIgnore + public Map getStructures() { + return this.structures_by_name; + } + + /** + * @param structures_by_name + * the name-keyed structure map to set + */ + public void setStructures_by_name(final Map 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(); + 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(); + updateDataModel.structures_by_id.put(structureId, structure); + updateDataModel.structures_by_name = new HashMap(); + 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(); + updateDataModel.devices.thermostats_by_id.put(deviceId, thermostat); + updateDataModel.devices.thermostats_by_name = new HashMap(); + 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(); + updateDataModel.devices.smoke_co_alarms_by_id.put(deviceId, smokeCOAlarm); + updateDataModel.devices.smoke_co_alarms_by_name = new HashMap(); + 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(); + updateDataModel.devices.cameras_by_id.put(deviceId, camera); + updateDataModel.devices.cameras_by_name = new HashMap(); + 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(); + } }