@@ -376,6 +386,7 @@ SensorGraph.propTypes = {
className: PropTypes.string,
loadAllNodeData: PropTypes.func,
loadWeather: PropTypes.func,
+ setRenderIaq: PropTypes.func,
sensors: PropTypes.object, // eslint-disable-line
buildingArea: PropTypes.object, // eslint-disable-line
weather: PropTypes.object, // eslint-disable-line
diff --git a/src/containers/Sensors/SensorGraphIAQ.js b/src/containers/Sensors/SensorGraphIAQ.js
new file mode 100644
index 0000000000000000000000000000000000000000..25f4fae7dcc4d204f8b4c5375e196d0c8261bd0d
--- /dev/null
+++ b/src/containers/Sensors/SensorGraphIAQ.js
@@ -0,0 +1,680 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { bindActionCreators } from 'redux';
+import { connect } from 'react-redux';
+import debounce from 'lodash.debounce';
+import {
+ Table, Card, Nav, NavItem, NavLink,
+} from 'reactstrap';
+import {
+ Charts,
+ ChartContainer,
+ ChartRow,
+ YAxis,
+ LineChart,
+ Baseline,
+ styler,
+ Legend,
+} from 'react-timeseries-charts';
+import { TimeSeries, TimeRange } from 'pondjs';
+import { Icon } from 'react-fa';
+import { loadAllNodeData } from './actions';
+import { loadWeather } from '../Weather/actions';
+import { subtractDaysFromNow } from '../../utils/date';
+import { GRAPH_COLORS } from './colors';
+import Loading from '../../components/Loading';
+import {
+ SENSOR_TYPES,
+} from '../../components/SensorInstall/constants';
+
+
+class SensorGraphIAQ extends Component {
+ NUM_DAYS = 60; // eslint-disable-line
+
+ constructor(props) {
+ super(props);
+ const today = new Date();
+
+ this.state = {
+ temperatureTimeseries: [],
+ humidityTimeseries: [],
+ co2Timeseries: [],
+ chemicalsTimeseries: [],
+ dustTimeseries: [],
+ latestReadings: {},
+ timerange: new TimeRange([subtractDaysFromNow(this.NUM_DAYS - 1), today]),
+ from: subtractDaysFromNow(this.NUM_DAYS),
+ today,
+ lineStyles: {},
+ collapseLatest: false,
+ activeTab: '',
+ };
+ }
+
+ componentDidMount() {
+ this.props.loadAllNodeData({
+ building_id: this.props.buildingId,
+ nodes: '',
+ data: '',
+ from: this.state.from.toUTCString(),
+ });
+ this.props.loadWeather({
+ measurement: 'temperature',
+ interval: 'hourly',
+ location: 'New_York:NY',
+ });
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps === this.props) {
+ return;
+ }
+ this.generateData(nextProps);
+ }
+
+ sensorDataLoading = (props) => (
+ props.sensors.loading || props.sensors.sensorData.length === 0
+ )
+
+ generateData = (props) => {
+ if (this.sensorDataLoading(props)) {
+ return;
+ }
+
+ const { sensorData } = props.sensors;
+ const latestReadings = {};
+ const lines = sensorData.filter(gateway => (
+ gateway.sensor_type === SENSOR_TYPES.awair && gateway.data.length > 0
+ )).reduce((acc, gateway) => {
+ const timeSeriesData = this.generateTimeSeriesFromGateway(gateway, props);
+ Object.keys(timeSeriesData.timeseries).map((key) => {
+ acc[key].push(timeSeriesData.timeseries[key]);
+ return key;
+ });
+ // Add the latest readings
+ latestReadings[timeSeriesData.latestReading.id] = timeSeriesData.latestReading.reading;
+ return acc;
+ }, {
+ temperature: [],
+ humidity: [],
+ co2: [],
+ chemicals: [],
+ dust: [],
+ });
+ this.setState({ latestReadings });
+
+ const weatherPoints = props.weather.weatherData.map(val => (
+ [new Date(val.time), val.fields.value]
+ ));
+ if (weatherPoints.length > 0) {
+ lines.temperature.push(new TimeSeries({
+ name: 'Outdoor Temperature',
+ columns: ['time', 'Outdoor Temperature'],
+ points: weatherPoints,
+ }));
+ }
+
+ if (Object.keys(lines).length === 0) {
+ return;
+ }
+ const styles = {};
+ Object.keys(lines).forEach((type) => {
+ lines[type].forEach((line, index) => {
+ styles[line.name()] = this.generateLineStyle(line, index);
+ });
+ });
+
+ this.setState({
+ temperatureTimeseries: lines.temperature,
+ humidityTimeseries: lines.humidity,
+ co2Timeseries: lines.co2,
+ chemicalsTimeseries: lines.chemicals,
+ dustTimeseries: lines.dust,
+ lineStyles: styles,
+ });
+ }
+
+ findDeviceInArea = (incArea, node) => {
+ let spaceInArea = null;
+ let spaceArea = null;
+
+ // find space in area
+ incArea.every(area => {
+
+ spaceInArea = area.spaces.reduce((acc, spc) => {
+ if (spc.id === node.space_id) {
+ return spc;
+ }
+ return acc;
+ }, {});
+
+ if (Object.keys(spaceInArea).length !== 0) {
+ spaceArea = area;
+ return false;
+ }
+
+ return true;
+ });
+
+ return { spaceArea, spaceInArea };
+ }
+
+ findDeviceLocation = (sensorNode, props) => {
+ const { buildingArea } = props;
+ let spaceOfNode = null;
+ let deviceName = '';
+
+ if (!buildingArea.apartmentsLoading) {
+ const temp = this.findDeviceInArea(buildingArea.apartments, sensorNode);
+
+ if (temp.spaceArea) {
+ spaceOfNode = temp;
+ deviceName = `Apt ${spaceOfNode.spaceArea.number} ${spaceOfNode.spaceInArea.description}`;
+ }
+ }
+
+ if (!buildingArea.commonAreasLoading) {
+ const temp = this.findDeviceInArea(buildingArea.commonAreas, sensorNode);
+
+ if (temp.spaceArea) {
+ spaceOfNode = temp;
+ deviceName = `Common area ${spaceOfNode.spaceInArea.description}`;
+ }
+ }
+
+
+ if (!buildingArea.serviceAreasLoading) {
+ const temp = this.findDeviceInArea(buildingArea.serviceAreas, sensorNode);
+
+ if (temp.spaceArea) {
+ spaceOfNode = temp;
+ deviceName = `Service area ${spaceOfNode.spaceInArea.description}`;
+ }
+ }
+
+ return { spaceOfNode, deviceName };
+ }
+
+ generateTimeSeriesFromGateway = (gateway, props) => {
+ /* eslint-disable quote-props */
+ const unitToMeasurement = {
+ '°F': 'temperature',
+ '%': 'humidity',
+ 'ppm': 'co2',
+ 'ppb': 'chemicals',
+ 'mcg3': 'dust',
+ };
+ const measurements = gateway.data.reduce((acc, i) => {
+ acc[unitToMeasurement[i.unit]].push(i);
+ return acc;
+ }, {
+ temperature: [],
+ humidity: [],
+ co2: [],
+ chemicals: [],
+ dust: [],
+ });
+ const latestReading = {};
+ let name = '';
+ const timeseries = Object.keys(measurements).reduce((acc, measurement) => {
+ const device = {
+ space_id: gateway.space_id,
+ id: gateway.id,
+ data: measurements[measurement],
+ };
+ const spaceOfNode = this.findDeviceLocation(device, props);
+ name = spaceOfNode.deviceName ? spaceOfNode.deviceName : `device ${device.id}`;
+
+ // Get the latest reading
+ latestReading[measurement] = {
+ ts: device.data[device.data.length - 1].ts,
+ value: device.data[device.data.length - 1].value,
+ };
+ latestReading.name = name;
+
+ // Generate the time series
+ acc[measurement] = this.generateTimeSeries(
+ device.data,
+ `${name} - ${measurement}`,
+ );
+ return acc;
+ }, {});
+ return {
+ timeseries,
+ latestReading: {
+ id: gateway.id,
+ reading: latestReading,
+ },
+ };
+ }
+
+ generateTimeSeries = (data, deviceName) => {
+ // Get all points
+ let prevPoint = null;
+ const points = data.reduce((acc, i) => {
+ if (prevPoint) {
+ const curDate = new Date(i.ts);
+ const prevDate = new Date(prevPoint.ts);
+ const diffSeconds = (curDate.getTime() - prevDate.getTime()) / 1000;
+ // If no data for more than 15 minutes, add null values
+ if (diffSeconds > 900) {
+ // Create a new date object 5 minutes ahead of the previous date
+ let newDate = new Date(prevDate.getTime() + (300 * 1000));
+ while (newDate.getTime() < curDate.getTime()) {
+ acc.push([newDate, NaN]);
+ newDate = new Date(newDate.getTime() + (300 * 1000));
+ }
+ }
+ }
+ acc.push([new Date(i.ts), i.value]);
+
+ prevPoint = i;
+ return acc;
+ }, []);
+
+ return new TimeSeries({
+ name: deviceName,
+ columns: ['time', deviceName],
+ points,
+ });
+ }
+
+ generateLineStyle = (line, index) => ({
+ key: line.name(),
+ color: GRAPH_COLORS[index % GRAPH_COLORS.length],
+ width: 1,
+ })
+
+ fetchData = debounce((from) => {
+ this.props.loadAllNodeData({
+ building_id: this.props.buildingId,
+ nodes: '',
+ data: '',
+ from: from.toUTCString(),
+ });
+ }, 500);
+
+ updateTimeRange = (timerange) => {
+ if (this.state.timeseries.length === 0) {
+ return;
+ }
+
+ const endDate = timerange.end() > this.state.today ? this.state.today : timerange.end();
+
+ if (timerange.begin() < this.state.from) {
+ this.fetchData(timerange.begin());
+ }
+
+ this.setState({
+ timerange: new TimeRange([timerange.begin(), endDate]),
+ from: timerange.begin(),
+ });
+ }
+
+ renderLineGraph = (timeseries) => {
+ if (timeseries.length === 0) {
+ return (
+
+ );
+ }
+
+
+ const lineGraph = timeseries.map(line => (
+
+ ));
+
+ return lineGraph;
+ }
+
+ renderLatestReadings = () => {
+ const { latestReadings } = this.state;
+ if (Object.keys(latestReadings).length === 0) {
+ return null;
+ }
+ const deviceIds = Object.keys(latestReadings);
+ // The id of the device to displayed
+ const latestReadingDeviceId = this.state.activeTab !== '' ? this.state.activeTab : deviceIds[0];
+ const latestReadingDevice = latestReadings[latestReadingDeviceId];
+ return (
+
+
Latest Readings
+
+
+
+
+
+
+ | Timestamp |
+ Measurement |
+ Value |
+
+
+
+
+ | {latestReadingDevice.temperature.ts} |
+ °F Temperature |
+ {latestReadingDevice.temperature.value} |
+
+
+ | {latestReadingDevice.humidity.ts} |
+ % Humidity |
+ {latestReadingDevice.humidity.value} |
+
+
+ | {latestReadingDevice.co2.ts} |
+ PPM CO2 |
+ {latestReadingDevice.co2.value} |
+
+
+ | {latestReadingDevice.chemicals.ts} |
+ PPB Chemicals |
+ {latestReadingDevice.chemicals.value} |
+
+
+ | {latestReadingDevice.dust.ts} |
+ mcg3 Dust |
+ {latestReadingDevice.dust.value} |
+
+
+
+
+
+
+ );
+ }
+
+ render() {
+ if (this.props.sensors.loading) {
+ return
;
+ }
+ return (
+
+
+ {this.renderLatestReadings()}
+
+
+
Temperature
+
+
+ {this.state.temperatureTimeseries.length > 0 &&
+
+
+ {this.props.sensors.loading && !this.props.sensors.error && }
+
+
+
+
+
+
+
+
+ {this.renderLineGraph(this.state.temperatureTimeseries)}
+
+
+
+
+
+
+
+
+
+
Humidity
+
+
+ {this.state.humidityTimeseries.length > 0 &&
+
+
+ {this.props.sensors.loading && !this.props.sensors.error && }
+
+
+
+
+
+
+
+
+ {this.renderLineGraph(this.state.humidityTimeseries)}
+
+
+
+
+
+
+
+
+
CO2
+
+
+ {this.state.co2Timeseries.length > 0 &&
+
+
+ {this.props.sensors.loading && !this.props.sensors.error && }
+
+
+
+
+
+
+
+
+ {this.renderLineGraph(this.state.co2Timeseries)}
+
+
+
+
+
+
+
+
+
Chemicals
+
+
+ {this.state.chemicalsTimeseries.length > 0 &&
+
+
+ {this.props.sensors.loading && !this.props.sensors.error && }
+
+
+
+
+
+
+
+
+ {this.renderLineGraph(this.state.chemicalsTimeseries)}
+
+
+
+
+
+
+
+
+
Dust
+
+
+ {this.state.dustTimeseries.length > 0 &&
+
+
+ {this.props.sensors.loading && !this.props.sensors.error && }
+
+
+
+
+
+
+
+
+ {this.renderLineGraph(this.state.dustTimeseries)}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+SensorGraphIAQ.propTypes = {
+ buildingId: PropTypes.string,
+ className: PropTypes.string,
+ loadAllNodeData: PropTypes.func,
+ loadWeather: PropTypes.func,
+ sensors: PropTypes.object, // eslint-disable-line
+ buildingArea: PropTypes.object, // eslint-disable-line
+ weather: PropTypes.object, // eslint-disable-line
+};
+
+SensorGraphIAQ.defaultProps = {
+ className: '',
+};
+
+const mapStateToProps = state => (
+ {
+ sensors: state.sensors,
+ buildingArea: state.buildingArea,
+ weather: state.weather,
+ }
+);
+
+const mapDispatchToProps = dispatch => (
+ bindActionCreators({
+ loadAllNodeData,
+ loadWeather,
+ }, dispatch)
+);
+
+export default connect(mapStateToProps, mapDispatchToProps)(SensorGraphIAQ);
diff --git a/src/containers/Sensors/Sensors.js b/src/containers/Sensors/Sensors.js
index 3927a9eb1884b98375d34b1c516145cd2ed8013e..b11cec5d5ca59cbd3b14da353128ce8002f66351 100644
--- a/src/containers/Sensors/Sensors.js
+++ b/src/containers/Sensors/Sensors.js
@@ -7,7 +7,11 @@ import BuildingAreaTable from '../BuildingArea/BuildingAreaTable';
class Sensors extends Component {
- state = { }
+ state = { renderIaq: false }
+
+ setRenderIaq = (renderIaq) => {
+ this.setState({ renderIaq });
+ }
render() {
return (
@@ -18,16 +22,26 @@ class Sensors extends Component {
{ name: 'Building Overview', url: '' },
{ name: 'Sensors', url: 'null' },
]}
- links={[{
- name: 'Install',
- url: `/buildings/${this.props.buildingId}/sensors/install`,
- tags: '',
- internalLink: true,
- }]}
+ links={[
+ ...this.state.renderIaq ?
+ [{
+ name: 'IAQ',
+ url: `/buildings/${this.props.buildingId}/sensors/iaq`,
+ tags: '',
+ internalLink: true,
+ }] : [],
+ {
+ name: 'Install',
+ url: `/buildings/${this.props.buildingId}/sensors/install`,
+ tags: '',
+ internalLink: true,
+ },
+ ]}
/>
+
+
+
+
+ );
+ }
+}
+
+SensorsIAQ.propTypes = {
+ buildingId: PropTypes.string,
+};
+
+export default connect(null, null)(SensorsIAQ);
diff --git a/src/routes.js b/src/routes.js
index 381b8699d32f795b53981ae64b23c82a131b7e24..ca9a6d86b597064597c2cbd31f1183d358cd5cab 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -20,6 +20,7 @@ import ReportsHome from './screens/ReportsHome';
import Utilities from './components/Utilities';
import Envelope from './containers/Envelope';
import Sensors from './containers/Sensors/Sensors';
+import SensorsIAQ from './containers/Sensors/SensorsIAQ';
import SensorInstall from './containers/Sensors/SensorInstall';
import GatewayList from './components/SensorInstall/GatewayList';
import BuildingReports from './containers/BuildingReports';
@@ -55,6 +56,7 @@ export default (