From a71820ef6bcadff56baf4ec9d05ed7e79910ccec Mon Sep 17 00:00:00 2001
From: Conrad
Date: Wed, 11 Apr 2018 17:28:33 -0400
Subject: [PATCH 1/4] Add sensor IAQ page
---
src/components/SensorInstall/Gateway.js | 140 +++-
src/components/SensorInstall/constants.js | 5 +
.../BuildingArea/BuildingAreaTable.js | 10 +-
src/containers/Sensors/SensorGraph.js | 2 +-
src/containers/Sensors/SensorGraphIAQ.js | 679 ++++++++++++++++++
src/containers/Sensors/Sensors.js | 20 +-
src/containers/Sensors/SensorsIAQ.js | 41 ++
src/routes.js | 2 +
8 files changed, 860 insertions(+), 39 deletions(-)
create mode 100644 src/containers/Sensors/SensorGraphIAQ.js
create mode 100644 src/containers/Sensors/SensorsIAQ.js
diff --git a/src/components/SensorInstall/Gateway.js b/src/components/SensorInstall/Gateway.js
index d4715b06..bab31a61 100644
--- a/src/components/SensorInstall/Gateway.js
+++ b/src/components/SensorInstall/Gateway.js
@@ -12,19 +12,15 @@ import 'box-ui-elements/dist/preview.css';
import { sensewareNodeURL } from '../../utils/restServices';
import SensewareNode from './Nodes/SensewareNode';
import SensorImageUpload from './SensorImageUpload';
-import { ENTITY_TYPES } from './constants';
+import { ENTITY_TYPES, SENSOR_TYPES } from './constants';
import documentsPropType from '../../containers/Documents/propTypes';
import ErrorAlert from '../../components/ErrorAlert';
import BoxLogin from '../../components/BoxLogin';
import userPropType from '../../containers/User/propTypes';
+import SpacePicker from '../../components/SpacePicker/SpacePicker';
import './styles.css';
class Gateway extends Component {
- SENSOR_TYPES = { // eslint-disable-line
- senseware: 1,
- awair: 2,
- other: 3,
- }
HEATING_SYSTEM_TYPES = { // eslint-disable-line
hydronic: 1,
steam: 2,
@@ -62,6 +58,7 @@ class Gateway extends Component {
gateway_id: this.props.form.gateway_id,
installer_name: this.props.form.installer_name,
placement_type: this.props.form.placement_type,
+ space_id: this.props.form.space_id,
notes: this.props.form.notes,
},
display_name: displayName,
@@ -204,30 +201,36 @@ class Gateway extends Component {
value = value.replace(/[^a-zA-Z0-9]+/g, '').toUpperCase();
break;
case 'gateway_id':
- value = value.replace(/\D/g, '');
+ if (this.state.form.sensor_type === SENSOR_TYPES.senseware) {
+ value = value.replace(/\D/g, '');
+ }
break;
default:
break;
}
- this.setState({
- form: {
- ...this.state.form,
- [event.target.name]: value,
- },
- display_name: displayName,
- lastEdited: Date.now(),
- // Only set the saved flag if the gateway is created
- saved: !this.state.gatewayCreated || this.state.offlineCreated,
- });
- if (
- this.state.gatewayCreated &&
- !this.state.offlineCreated &&
- !this.state.offlineSaving
- ) {
- // Save in a half a second, unless more input changes are made
- clearTimeout(this.saveSoon);
- this.saveSoon = setTimeout(this.handleSave, 500);
- }
+ this.saveInputChange(
+ { ...this.state.form, [event.target.name]: value },
+ displayName,
+ );
+ }
+ }
+
+ saveInputChange = (form, displayName) => {
+ this.setState({
+ form,
+ display_name: displayName,
+ lastEdited: Date.now(),
+ // Only set the saved flag if the gateway is created
+ saved: !this.state.gatewayCreated || this.state.offlineCreated,
+ });
+ if (
+ this.state.gatewayCreated &&
+ !this.state.offlineCreated &&
+ !this.state.offlineSaving
+ ) {
+ // Save in a half a second, unless more input changes are made
+ clearTimeout(this.saveSoon);
+ this.saveSoon = setTimeout(this.handleSave, 500);
}
}
@@ -379,6 +382,13 @@ class Gateway extends Component {
this.setState({ uploadedImages });
}
+ updateSpaceId = (spaceId) => {
+ this.saveInputChange(
+ { ...this.state.form, space_id: spaceId },
+ this.state.display_name,
+ );
+ }
+
renderAddGatewayBtn = () => (
@@ -809,6 +869,22 @@ class Gateway extends Component {
/>
+ Gateway Location
+
+
+
@@ -841,6 +917,7 @@ Gateway.propTypes = {
gateway_id: PropTypes.string,
installer_name: PropTypes.string,
placement_type: PropTypes.number,
+ space_id: PropTypes.number,
notes: PropTypes.string,
}),
address: PropTypes.string,
@@ -883,6 +960,7 @@ Gateway.defaultProps = {
gateway_id: '',
installer_name: '',
placement_type: 1,
+ space_id: null,
notes: '',
},
gateway_id: null,
diff --git a/src/components/SensorInstall/constants.js b/src/components/SensorInstall/constants.js
index d1aa4ad4..c1db48bd 100644
--- a/src/components/SensorInstall/constants.js
+++ b/src/components/SensorInstall/constants.js
@@ -24,3 +24,8 @@ export const TEMPERATURE_PROBE_LOCATION_REVERSE = { // eslint-disable-line
3: 'dhw_supply',
4: 'dhw_return',
};
+export const SENSOR_TYPES = { // eslint-disable-line
+ senseware: 1,
+ awair: 2,
+ other: 3,
+};
diff --git a/src/containers/BuildingArea/BuildingAreaTable.js b/src/containers/BuildingArea/BuildingAreaTable.js
index 77a7a79a..327da6b9 100644
--- a/src/containers/BuildingArea/BuildingAreaTable.js
+++ b/src/containers/BuildingArea/BuildingAreaTable.js
@@ -29,7 +29,7 @@ class BuildingAreaTable extends Component {
renderSpaces = (spaces) => {
return spaces.map(spc => (
- {spc.description} node on floor {spc.floor}
+ {spc.description} {spc.floor ? ` - device on floor ${spc.floor}` : ''}
));
}
@@ -64,6 +64,9 @@ class BuildingAreaTable extends Component {
}
render() {
+ if (!this.props.render) {
+ return null;
+ }
return (
@@ -89,12 +92,17 @@ class BuildingAreaTable extends Component {
BuildingAreaTable.propTypes = {
buildingId: PropTypes.string,
+ render: PropTypes.bool,
loadApartments: PropTypes.func,
loadCommonAreas: PropTypes.func,
loadServiceAreas: PropTypes.func,
buildingArea: PropTypes.object, // eslint-disable-line
};
+BuildingAreaTable.defaultProps = {
+ render: true,
+};
+
const mapStateToProps = state => ({
buildingArea: state.buildingArea,
});
diff --git a/src/containers/Sensors/SensorGraph.js b/src/containers/Sensors/SensorGraph.js
index 26ba4b39..5c27db01 100644
--- a/src/containers/Sensors/SensorGraph.js
+++ b/src/containers/Sensors/SensorGraph.js
@@ -90,7 +90,7 @@ export class SensorGraph extends Component {
}, [])];
});
- const weatherPoints = this.props.weather.weatherData.map(val => (
+ const weatherPoints = props.weather.weatherData.map(val => (
[new Date(val.time), val.fields.value]
));
if (weatherPoints.length > 0) {
diff --git a/src/containers/Sensors/SensorGraphIAQ.js b/src/containers/Sensors/SensorGraphIAQ.js
new file mode 100644
index 00000000..f149f2ac
--- /dev/null
+++ b/src/containers/Sensors/SensorGraphIAQ.js
@@ -0,0 +1,679 @@
+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 {
+ 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(),
+ unit_id: 1, // Temperature
+ });
+ 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
+ )).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(),
+ unit_id: 1, // Temperature
+ });
+ }, 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;
+ console.log(latestReadings);
+ 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 Readngs
+
+
+
+
+
+
+ | 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() {
+ 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 3927a9eb..cfd0bd48 100644
--- a/src/containers/Sensors/Sensors.js
+++ b/src/containers/Sensors/Sensors.js
@@ -18,12 +18,20 @@ 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={[
+ {
+ 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 381b8699..ca9a6d86 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 (
+
--
GitLab
From bad21850f88453a0ea458b1b2d347348727e4e07 Mon Sep 17 00:00:00 2001
From: Conrad
Date: Wed, 11 Apr 2018 17:30:35 -0400
Subject: [PATCH 2/4] Fix typo
---
src/containers/Sensors/SensorGraphIAQ.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/containers/Sensors/SensorGraphIAQ.js b/src/containers/Sensors/SensorGraphIAQ.js
index f149f2ac..cd0a1a91 100644
--- a/src/containers/Sensors/SensorGraphIAQ.js
+++ b/src/containers/Sensors/SensorGraphIAQ.js
@@ -349,7 +349,7 @@ class SensorGraphIAQ extends Component {
const latestReadingDevice = latestReadings[latestReadingDeviceId];
return (
-
Latest Readngs
+
Latest Readings