programming-the-iot / book-exercise-tasks

This repo is for issues / tasks ONLY. All programming and related exercises for each chapter of 'Programming the Internet of Things' are listed here.
Other
11 stars 12 forks source link

PIOT-GDA-10-003: Update DeviceDataManager to analyze messages from the CDA and take an appropriate action #91

Open labbenchstudios opened 4 years ago

labbenchstudios commented 4 years ago

Description

Review the README

Estimated effort may vary greatly

Actions

Threshold Crossing Rules - Configuration File Updates

NOTE: The example code provided here assumes there's a single CDA and GDA in the test loop.

min seconds between readings before triggering actuation event

humidityMaxTimePastThreshold = 300

ideal average humidity level (% relative)

nominalHumiditySetting = 40.0

min value before turning on humidifier (% relative)

triggerHumidifierFloor = 30.0

max value before turning off humidifier (% relative)

triggerHumidifierCeiling = 50.0


***Threshold Crossing Logic - `DeviceDataManager` Updates***
- Add the requisite class-scoped threshold crossing properties:
```java
private ActuatorData   latestHumidifierActuatorData = null;
private ActuatorData   latestHumidifierActuatorResponse = null;
private SensorData     latestHumiditySensorData = null;
private OffsetDateTime latestHumiditySensorTimeStamp = null;

private boolean handleHumidityChangeOnDevice = false; // optional
private int     lastKnownHumidifierCommand   = ConfigConst.OFF_COMMAND;

// TODO: Load these from PiotConfig.props
private long    humidityMaxTimePastThreshold = 300; // seconds
private float   nominalHumiditySetting   = 40.0f;
private float   triggerHumidifierFloor   = 30.0f;
private float   triggerHumidifierCeiling = 50.0f;

// TODO: add these to ConfigConst this.handleHumidityChangeOnDevice = configUtil.getBoolean( ConfigConst.GATEWAY_DEVICE, "handleHumidityChangeOnDevice");

this.humidityMaxTimePastThreshold = configUtil.getInteger( ConfigConst.GATEWAY_DEVICE, "humidityMaxTimePastThreshold");

this.nominalHumiditySetting = configUtil.getFloat( ConfigConst.GATEWAY_DEVICE, "nominalHumiditySetting");

this.triggerHumidifierFloor = configUtil.getFloat( ConfigConst.GATEWAY_DEVICE, "triggerHumidifierFloor");

this.triggerHumidifierCeiling = configUtil.getFloat( ConfigConst.GATEWAY_DEVICE, "triggerHumidifierCeiling");

// TODO: basic validation for timing - add other validators for remaining values if (this.humidityMaxTimePastThreshold < 10 || this.humidityMaxTimePastThreshold > 7200) { this.humidityMaxTimePastThreshold = 300; }


- Update `handleSensorData(ResourceNameEnum, SensorData data)` to invoke the analysis functionality. 
  - NOTE 1: Your code may look much different than that given below.
  - NOTE 2: Be sure to add an implementation for 'handleUpstreamTransmission(resourceName, jsonDat, qos)`

```java
@Override
public boolean handleSensorMessage(ResourceNameEnum resourceName, SensorData data)
{
    if (data != null) {
        _Logger.fine("Handling sensor message: " + data.getName());

        if (data.hasError()) {
            _Logger.warning("Error flag set for SensorData instance.");
        }

        String jsonData = DataUtil.getInstance().sensorDataToJson(data);

        _Logger.fine("JSON [SensorData] -> " + jsonData);

        // TODO: retrieve this from config file
        int qos = ConfigConst.DEFAULT_QOS;

        if (this.enablePersistenceClient && this.persistenceClient != null) {
            this.persistenceClient.storeData(resourceName.getResourceName(), qos, data);
        }

        this.handleIncomingDataAnalysis(resourceName, data);

        this.handleUpstreamTransmission(resourceName, jsonData, qos);

        return true;
    } else {
        return false;
    }
}
private void handleUpstreamTransmission(ResourceNameEnum resource, String jsonData, int qos)
{
    // NOTE: This will be implemented in Part 04
    _Logger.info("TODO: Send JSON data to cloud service: " + resource);
}   
private void handleIncomingDataAnalysis(ResourceNameEnum resource, SensorData data)
{
    // check either resource or SensorData for type
    if (data.getTypeID() == ConfigConst.HUMIDITY_SENSOR_TYPE) {
        handleHumiditySensorAnalysis(resource, data);
    }
}

private void handleHumiditySensorAnalysis(ResourceNameEnum resource, SensorData data)
{
    //
    // NOTE: INCOMPLETE and VERY BASIC CODE SAMPLE. Not intended to provide a solution.
    //

    _Logger.fine("Analyzing humidity data from CDA: " + data.getLocationID() + ". Value: " + data.getValue());

    boolean isLow  = data.getValue() < this.triggerHumidifierFloor;
    boolean isHigh = data.getValue() > this.triggerHumidifierCeiling;

    if (isLow || isHigh) {
        _Logger.fine("Humidity data from CDA exceeds nominal range.");

        if (this.latestHumiditySensorData == null) {
            // set properties then exit - nothing more to do until the next sample
            this.latestHumiditySensorData = data;
            this.latestHumiditySensorTimeStamp = getDateTimeFromData(data);

            _Logger.fine(
                "Starting humidity nominal exception timer. Waiting for seconds: " +
                this.humidityMaxTimePastThreshold);

            return;
        } else {
            OffsetDateTime curHumiditySensorTimeStamp = getDateTimeFromData(data);

            long diffSeconds =
                ChronoUnit.SECONDS.between(
                    this.latestHumiditySensorTimeStamp, curHumiditySensorTimeStamp);

            _Logger.fine("Checking Humidity value exception time delta: " + diffSeconds);

            if (diffSeconds >= this.humidityMaxTimePastThreshold) {
                ActuatorData ad = new ActuatorData();
                ad.setName(ConfigConst.HUMIDIFIER_ACTUATOR_NAME);
                ad.setLocationID(data.getLocationID());
                ad.setTypeID(ConfigConst.HUMIDIFIER_ACTUATOR_TYPE);
                ad.setValue(this.nominalHumiditySetting);

                if (isLow) {
                    ad.setCommand(ConfigConst.ON_COMMAND);
                } else if (isHigh) {
                    ad.setCommand(ConfigConst.OFF_COMMAND);
                }

                _Logger.info(
                    "Humidity exceptional value reached. Sending actuation event to CDA: " +
                    ad);

                this.lastKnownHumidifierCommand = ad.getCommand();
                sendActuatorCommandtoCda(ResourceNameEnum.CDA_ACTUATOR_CMD_RESOURCE, ad);

                // set ActuatorData and reset SensorData (and timestamp)
                this.latestHumidifierActuatorData = ad;
                this.latestHumiditySensorData = null;
                this.latestHumiditySensorTimeStamp = null;
            }
        }
    } else if (this.lastKnownHumidifierCommand == ConfigConst.ON_COMMAND) {
        // check if we need to turn off the humidifier
        if (this.latestHumidifierActuatorData != null) {
            // check the value - if the humidifier is on, but not yet at nominal, keep it on
            if (this.latestHumidifierActuatorData.getValue() >= this.nominalHumiditySetting) {
                this.latestHumidifierActuatorData.setCommand(ConfigConst.OFF_COMMAND);

                _Logger.info(
                    "Humidity nominal value reached. Sending OFF actuation event to CDA: " +
                    this.latestHumidifierActuatorData);

                sendActuatorCommandtoCda(
                    ResourceNameEnum.CDA_ACTUATOR_CMD_RESOURCE, this.latestHumidifierActuatorData);

                // reset ActuatorData and SensorData (and timestamp)
                this.lastKnownHumidifierCommand = this.latestHumidifierActuatorData.getCommand();
                this.latestHumidifierActuatorData = null;
                this.latestHumiditySensorData = null;
                this.latestHumiditySensorTimeStamp = null;
            } else {
                _Logger.fine("Humidifier is still on. Not yet at nominal levels (OK).");
            }
        } else {
            // shouldn't happen, unless some other logic
            // nullifies the class-scoped ActuatorData instance
            _Logger.warning(
                "ERROR: ActuatorData for humidifier is null (shouldn't be). Can't send command.");
        }
    }
}

private void sendActuatorCommandtoCda(ResourceNameEnum resource, ActuatorData data)
{
    // NOTE: This is how an ActuatorData command will get passed to the CDA
    // when the GDA is providing the CoAP server and hosting the appropriate
    // ActuatorData resource. It will typically be used when the OBSERVE
    // client (the CDA, assuming the GDA is the server and CDA is the client)
    // has sent an OBSERVE GET request to the ActuatorData resource.
    if (this.actuatorDataListener != null) {
        this.actuatorDataListener.onActuatorDataUpdate(data);
    }

    // NOTE: This is how an ActuatorData command will get passed to the CDA
    // when using MQTT to communicate between the GDA and CDA
    if (this.enableMqttClient && this.mqttClient != null) {
        String jsonData = DataUtil.getInstance().actuatorDataToJson(data);

        if (this.mqttClient.publishMessage(resource, jsonData, ConfigConst.DEFAULT_QOS)) {
            _Logger.info(
                "Published ActuatorData command from GDA to CDA: " + data.getCommand());
        } else {
            _Logger.warning(
                "Failed to publish ActuatorData command from GDA to CDA: " + data.getCommand());
        }
    }
}

private OffsetDateTime getDateTimeFromData(BaseIotData data)
{
    OffsetDateTime odt = null;

    try {
        odt = OffsetDateTime.parse(data.getTimeStamp());
    } catch (Exception e) {
        _Logger.warning(
            "Failed to extract ISO 8601 timestamp from IoT data. Using local current time.");

        // TODO: this won't be accurate, but should be reasonably close, as the CDA will
        // most likely have recently sent the data to the GDA
        odt = OffsetDateTime.now();
    }

    return odt;
}

Estimate

Tests

Simple Test Setup

/**
 * Test method for running the DeviceDataManager.
 * 
 */
@Test
public void testSendActuationEventsToCda()
{
    DeviceDataManager devDataMgr = new DeviceDataManager();

    // NOTE: Be sure your PiotConfig.props is setup properly
    // to connect with the CDA
    devDataMgr.startManager();

    ConfigUtil cfgUtil = ConfigUtil.getInstance();

    // TODO: add these to ConfigConst
    float nominalVal = cfgUtil.getFloat(ConfigConst.GATEWAY_DEVICE,   "nominalHumiditySetting");
    float lowVal     = cfgUtil.getFloat(ConfigConst.GATEWAY_DEVICE,   "triggerHumidifierFloor");
    float highVal    = cfgUtil.getFloat(ConfigConst.GATEWAY_DEVICE,   "triggerHumidifierCeiling");
    int   delay      = cfgUtil.getInteger(ConfigConst.GATEWAY_DEVICE, "humidityMaxTimePastThreshold");

    // Test Sequence No. 1
    generateAndProcessHumiditySensorDataSequence(
        devDataMgr, nominalVal, lowVal, highVal, delay);

    // TODO: Add more test sequences if desired.

    devDataMgr.stopManager();
}

private void generateAndProcessHumiditySensorDataSequence(
    DeviceDataManager ddm, float nominalVal, float lowVal, float highVal, int delay)
{
    SensorData sd = new SensorData();
    sd.setName("My Test Humidity Sensor");
    sd.setLocationID("constraineddevice001");
    sd.setTypeID(ConfigConst.HUMIDITY_SENSOR_TYPE);

    sd.setValue(nominalVal);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(2);

    sd.setValue(nominalVal);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(2);

    sd.setValue(lowVal - 2);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(delay + 1);

    sd.setValue(lowVal - 1);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(delay + 1);

    sd.setValue(lowVal + 1);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(delay + 1);

    sd.setValue(nominalVal);
    ddm.handleSensorMessage(ResourceNameEnum.CDA_SENSOR_MSG_RESOURCE, sd);
    waitForSeconds(delay + 1);
}

private void waitForSeconds(int seconds)
{
    try {
        Thread.sleep(seconds * 1000);
    } catch (InterruptedException e) {
        // ignore
    }
}

Integration Test Setup

tangyisheng2 commented 1 year ago

Hey, I found a typo in DeviceDataManagerSimpleCdaActuationTest

float lowVal     = cfgUtil.getFloat(ConfigConst.GATEWAY_DEVICE,   "triggerHumidiferFloor");

Should be triggerHumidifierFloor

Same typo also in DeviceDataManager

this.triggerHumidiferFloor = configUtil.getFloat(ConfigConst.GATEWAY_DEVICE, "triggerHumidifierFloor");
labbenchstudios commented 1 year ago

Now corrected; fixed throughout code samples.