diff --git a/src/containers/BuildingArea/BuildingAreaTable.js b/src/containers/BuildingArea/BuildingAreaTable.js index 327da6b91fff7f23b08a483a2f2bfbc6d89b7871..a5a41fef60dadd2b4fd886ea6c346574f0470d1a 100644 --- a/src/containers/BuildingArea/BuildingAreaTable.js +++ b/src/containers/BuildingArea/BuildingAreaTable.js @@ -2,7 +2,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { Container, Row, Col, ListGroup, ListGroupItem } from 'reactstrap'; +import { Collapse, Card, Container, Row, Col, ListGroup, ListGroupItem } from 'reactstrap'; +import { Icon } from 'react-fa'; import { loadApartments, loadCommonAreas, loadServiceAreas } from './actions'; @@ -67,25 +68,41 @@ class BuildingAreaTable extends Component { if (!this.props.render) { return null; } + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ return ( - - - {this.renderHeading('Apartments')} - - {this.renderApartments()} - - - {this.renderHeading('Common Area')} - - {this.renderArea(this.props.buildingArea.commonAreas)} - - - {this.renderHeading('Service Area')} - - {this.renderArea(this.props.buildingArea.serviceAreas)} - - - +
+ +

( + this.setState({ areasOpen: !this.state.areasOpen }) + )} + style={{ cursor: 'pointer' }} + > + Sensor Locations {' '} + +

+ + + + {this.renderHeading('Apartments')} + + {this.renderApartments()} + + + {this.renderHeading('Common Area')} + + {this.renderArea(this.props.buildingArea.commonAreas)} + + + {this.renderHeading('Service Area')} + + {this.renderArea(this.props.buildingArea.serviceAreas)} + + + + +
+
); } } diff --git a/src/containers/BuildingEvents/BuildingEventsTable.js b/src/containers/BuildingEvents/BuildingEventsTable.js new file mode 100644 index 0000000000000000000000000000000000000000..c87cdac8a91994e53850f24f8cbc44a09acd53b6 --- /dev/null +++ b/src/containers/BuildingEvents/BuildingEventsTable.js @@ -0,0 +1,153 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import { + Card, Table, Row, Col, + ListGroup, ListGroupItem, + Collapse, +} from 'reactstrap'; +import { Icon } from 'react-fa'; +import { loadEvents } from '../Event/actions'; +import Loading from '../../components/Loading'; +import ErrorAlert from '../../components/ErrorAlert'; + + +class BuildingEventsTable extends Component { + state = { eventsOpen: false } + + componentDidMount() { + this.props.loadEvents({ + 'building_id[]': this.props.buildingId, + limit: this.props.limit, + order: 'desc', + }); + } + + renderHeading = (heading) => { + return ( + + +

{heading}

+
+ +
+ ); + } + + renderSpaces = (spaces) => { + return spaces.map(spc => ( + + {spc.description} {spc.floor ? ` - device on floor ${spc.floor}` : ''} + + )); + } + + renderApartments = () => { + return this.props.events.map(apt => { + return ( +
  • +

    + Apartment {apt.number} +

    + + {this.renderSpaces(apt.spaces)} + +
  • + ); + }); + } + + renderArea = (incArea) => { + return incArea.map(area => { + return ( + + {this.renderSpaces(area.spaces)} + + ); + }); + } + + render() { + let mainContent = ( + + + + + + + + + + + {this.props.events.eventsData.map(event => ( + + + + + + + ))} + +
    TimestampEvent NameIndoor TemperatureOutdoor Temperature
    {event.ts}{event.type_description}{event.event_specific.indoor_temperature.toFixed(2)} °F{event.event_specific.outdoor_temperature.toFixed(2)} °F
    + ); + if (this.props.events.eventsLoading) { + mainContent = ; + } else if (this.props.events.eventsError) { + mainContent = ( + + ); + } else if (this.props.events.eventsData.length === 0) { + mainContent = 'Sorry, no events data for this building'; + } + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ + return ( +
    + +

    ( + this.setState({ eventsOpen: !this.state.eventsOpen }) + )} + style={{ cursor: 'pointer' }} + > + Recent Sensor Events {' '} + +

    + + {mainContent} + +
    +
    + ); + } +} + +BuildingEventsTable.propTypes = { + buildingId: PropTypes.string, + loadEvents: PropTypes.func, + events: PropTypes.object, // eslint-disable-line + limit: PropTypes.number, +}; + +BuildingEventsTable.defaultProps = { + limit: 10, +}; + +const mapStateToProps = state => ({ + events: state.events, +}); + +const mapDispatchToProps = dispatch => ( + bindActionCreators({ + loadEvents, + }, dispatch) +); + +export default connect(mapStateToProps, mapDispatchToProps)(BuildingEventsTable); diff --git a/src/containers/Event/actions.js b/src/containers/Event/actions.js new file mode 100644 index 0000000000000000000000000000000000000000..07b2c1ad75ccca7bd5550e017f1ebbea7e5fa72a --- /dev/null +++ b/src/containers/Event/actions.js @@ -0,0 +1,13 @@ +import { + EVENTS_REQUESTED, + EVENTS_SUCCEEDED, + EVENTS_FAILED, +} from './constants'; + +import { makeActionCreator } from '../../utils/reduxHelpers'; + +/* Retrieve Events */ +export const loadEvents = makeActionCreator(EVENTS_REQUESTED, 'filters'); +export const eventsLoaded = makeActionCreator(EVENTS_SUCCEEDED, 'eventsData'); +export const eventsFailed = makeActionCreator(EVENTS_FAILED, 'error'); + diff --git a/src/containers/Event/constants.js b/src/containers/Event/constants.js new file mode 100644 index 0000000000000000000000000000000000000000..cd55049a82f6032117c27b5ce3f09d32f618a9b9 --- /dev/null +++ b/src/containers/Event/constants.js @@ -0,0 +1,4 @@ +/* Retrieve Events */ +export const EVENTS_REQUESTED = 'EVENTS_REQUESTED'; +export const EVENTS_SUCCEEDED = 'EVENTS_SUCCEEDED'; +export const EVENTS_FAILED = 'EVENTS_FAILED'; diff --git a/src/containers/Event/reducer.js b/src/containers/Event/reducer.js new file mode 100644 index 0000000000000000000000000000000000000000..567de9e77a925603cd2f71e155cd6370db66d1fc --- /dev/null +++ b/src/containers/Event/reducer.js @@ -0,0 +1,32 @@ +import { + EVENTS_REQUESTED, + EVENTS_SUCCEEDED, + EVENTS_FAILED, +} from './constants'; + +export const initState = { + eventsLoading: false, + eventsError: false, + eventsData: [], +}; + +export default (state = initState, action) => { + switch (action.type) { + case EVENTS_REQUESTED: + return { ...state, eventsLoading: true, eventsError: false }; + + case EVENTS_SUCCEEDED: + return { + ...state, + eventsLoading: false, + eventsError: false, + eventsData: action.eventsData.data, + }; + + case EVENTS_FAILED: + return { ...state, eventsLoading: false, eventsError: action.error }; + + default: + return state; + } +}; diff --git a/src/containers/Event/sagas.js b/src/containers/Event/sagas.js new file mode 100644 index 0000000000000000000000000000000000000000..bcb5b9a794d017e5c76efc2d3327496fb6633c44 --- /dev/null +++ b/src/containers/Event/sagas.js @@ -0,0 +1,23 @@ +import { takeEvery } from 'redux-saga/effects'; +import SagaRequests from '../../utils/sagaRequests'; +import { eventsURL } from '../../utils/restServices'; + +import { + EVENTS_REQUESTED, +} from './constants'; + +import { + eventsLoaded, + eventsFailed, +} from './actions'; + +function* getEvents(action) { + yield SagaRequests.get(action, eventsURL, eventsLoaded, eventsFailed); +} + + +function* eventsWatcher() { + yield takeEvery(EVENTS_REQUESTED, getEvents); +} + +export default eventsWatcher; diff --git a/src/containers/Sensors/SensorGraph.js b/src/containers/Sensors/SensorGraph.js index 52ac8b350ce6a51dabee7be611733bf4a6d644bc..3f74b8e1c45ed0d18a11214875db5165852ad68e 100644 --- a/src/containers/Sensors/SensorGraph.js +++ b/src/containers/Sensors/SensorGraph.js @@ -6,6 +6,7 @@ import debounce from 'lodash.debounce'; import { Charts, ChartContainer, + Resizable, ChartRow, YAxis, LineChart, @@ -13,6 +14,12 @@ import { styler, Legend, } from 'react-timeseries-charts'; +import DatePicker from 'react-datepicker'; +import moment from 'moment'; +import { + Card, Table, Collapse, Row, Col, + Nav, NavItem, NavLink, +} from 'reactstrap'; import { TimeSeries, TimeRange } from 'pondjs'; import { Icon } from 'react-fa'; import { loadAllNodeData } from './actions'; @@ -22,22 +29,28 @@ import { GRAPH_COLORS } from './colors'; import { TEMPERATURE_PROBE_LOCATION_REVERSE, SENSOR_TYPES } from '../../components/SensorInstall/constants'; import Loading from '../../components/Loading'; - export class SensorGraph extends Component { - NUM_DAYS = 20; // eslint-disable-line + NUM_DAYS = 10; // eslint-disable-line constructor(props) { super(props); const today = new Date(); this.state = { - timeseries: new TimeSeries(), - weatherTimeseries: new TimeSeries(), + temperatureTimeseries: [], + humidityTimeseries: [], + co2Timeseries: [], + chemicalsTimeseries: [], + dustTimeseries: [], + latestReadings: {}, timerange: new TimeRange([subtractDaysFromNow(this.NUM_DAYS - 1), today]), - showChart: false, from: subtractDaysFromNow(this.NUM_DAYS), today, lineStyles: {}, + collapseLatest: false, + activeTab: '', + activeGraphTab: 'Temperature', + firstLoad: true, }; } @@ -47,12 +60,13 @@ export class SensorGraph extends Component { nodes: '', data: '', from: this.state.from.toUTCString(), - unit_id: 3, // Temperature }); this.props.loadWeather({ measurement: 'temperature', interval: 'hourly', location: 'New_York:NY', + date_start: moment(this.state.from).format('YYYY-MM-DD hh:mm:ss'), + date_end: moment(this.state.today).format('YYYY-MM-DD hh:mm:ss'), }); } @@ -61,10 +75,6 @@ export class SensorGraph extends Component { return; } this.generateData(nextProps); - const renderIaq = nextProps.sensors.sensorData.reduce((acc, gateway) => ( - gateway.sensor_type === SENSOR_TYPES.awair || acc - ), false); - this.props.setRenderIaq(renderIaq); } sensorDataLoading = (props) => ( @@ -75,56 +85,93 @@ export class SensorGraph extends Component { if (this.sensorDataLoading(props)) { return; } + this.setState({ firstLoad: false }); const { sensorData } = props.sensors; - let lines = []; - sensorData.forEach(gateway => { - if (gateway.nodes) { - lines = [...lines, ...gateway.nodes.reduce((acc, oneNode) => { - // If it's not repeater node - if (!oneNode.repeater) { - // If it's not a boiler node - if (!oneNode.temperature_probe_1 && !oneNode.temperature_probe_2 && - !oneNode.temperature_probe_3 && !oneNode.temperature_probe_4) { - acc.push(this.generateTimeSeries(oneNode)); + const latestReadings = {}; + const lines = sensorData.filter(gateway => ( + (!gateway.data && gateway.nodes.length > 0) || gateway.data.length > 0 + )).reduce((acc, gateway) => { + // Handle awair differently + if (gateway.sensor_type === SENSOR_TYPES.awair) { + const timeSeriesData = this.generateTimeSeriesFromSensor(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; + // Handle senseware nodes differently + } else if (gateway.sensor_type === SENSOR_TYPES.senseware) { + if (gateway.nodes) { + gateway.nodes.filter(oneNode => ( + oneNode.data.length > 0 + )).map((oneNode) => { + // If it's an apartment node + if (oneNode.node_type === 1) { + const timeSeriesData = this.generateTimeSeriesFromSensor(oneNode, 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; // Handle boiler nodes differently - } else { - acc.push(...this.generateBoilerTimeSeries(oneNode)); + } else if (oneNode.node_type === 2) { + const timeSeriesData = this.generateBoilerTimeSeries(oneNode, props); + Object.keys(timeSeriesData.timeseries).map((key) => { + acc[key].push(timeSeriesData.timeseries[key]); + return key; + }); } - } - return acc; - }, [])]; + return oneNode; + }); + } } + return acc; + }, { + temperature: [], + humidity: [], + co2: [], + chemicals: [], + dust: [], }); + this.setState({ latestReadings }); - const weatherPoints = props.weather.weatherData.map(val => ( - [new Date(val.time), val.fields.value] + const weatherPoints = props.weather.weatherData['New_York:NY'].hourly.map(point => ( + [new Date(point.ts), point.val] )); if (weatherPoints.length > 0) { - lines.push(new TimeSeries({ + lines.temperature.push(new TimeSeries({ name: 'Outdoor Temperature', columns: ['time', 'Outdoor Temperature'], points: weatherPoints, })); } - if (lines.length === 0) { + if (Object.keys(lines).length === 0) { return; } const styles = {}; - lines.forEach((line, index) => { - styles[line.name()] = this.generateLineStyle(line, index); + Object.keys(lines).forEach((type) => { + lines[type].forEach((line, index) => { + styles[line.name()] = this.generateLineStyle(line, index); + }); }); this.setState({ - timeseries: lines, - showChart: true, + temperatureTimeseries: lines.temperature, + humidityTimeseries: lines.humidity, + co2Timeseries: lines.co2, + chemicalsTimeseries: lines.chemicals, + dustTimeseries: lines.dust, lineStyles: styles, - cat: this.generateCategories(lines), }); } - findNodeInArea = (incArea, node) => { + findDeviceInArea = (incArea, device) => { let spaceInArea = null; let spaceArea = null; @@ -132,7 +179,7 @@ export class SensorGraph extends Component { incArea.every(area => { spaceInArea = area.spaces.reduce((acc, spc) => { - if (spc.id === node.space_id) { + if (spc.id === device.space_id) { return spc; } return acc; @@ -149,43 +196,101 @@ export class SensorGraph extends Component { return { spaceArea, spaceInArea }; } - findNodeLocation = (sensorNode) => { - const { buildingArea } = this.props; + findDeviceLocation = (sensorNode, props) => { + const { buildingArea } = props; let spaceOfNode = null; - let nodeName = ''; + let deviceName = ''; if (!buildingArea.apartmentsLoading) { - const temp = this.findNodeInArea(this.props.buildingArea.apartments, sensorNode); + const temp = this.findDeviceInArea(buildingArea.apartments, sensorNode); if (temp.spaceArea) { spaceOfNode = temp; - nodeName = `Apt ${spaceOfNode.spaceArea.number} ${spaceOfNode.spaceInArea.description}`; + deviceName = `Apt ${spaceOfNode.spaceArea.number} ${spaceOfNode.spaceInArea.description}`; } } if (!buildingArea.commonAreasLoading) { - const temp = this.findNodeInArea(this.props.buildingArea.commonAreas, sensorNode); + const temp = this.findDeviceInArea(buildingArea.commonAreas, sensorNode); if (temp.spaceArea) { spaceOfNode = temp; - nodeName = `Common area ${spaceOfNode.spaceInArea.description}`; + deviceName = `Common area ${spaceOfNode.spaceInArea.description}`; } } if (!buildingArea.serviceAreasLoading) { - const temp = this.findNodeInArea(this.props.buildingArea.serviceAreas, sensorNode); + const temp = this.findDeviceInArea(buildingArea.serviceAreas, sensorNode); if (temp.spaceArea) { spaceOfNode = temp; - nodeName = `Service area ${spaceOfNode.spaceInArea.description}`; + deviceName = `Service area ${spaceOfNode.spaceInArea.description}`; } } - return { spaceOfNode, nodeName }; + return { spaceOfNode, deviceName }; } - generateBoilerTimeSeries = (sensorNode) => { + generateTimeSeriesFromSensor = (sensor, props) => { + /* eslint-disable quote-props */ + const unitToMeasurement = { + '°F': 'temperature', + '%': 'humidity', + 'ppm': 'co2', + 'ppb': 'chemicals', + 'mcg3': 'dust', + }; + const measurements = sensor.data.reduce((acc, i) => { + // Ignore values we don't care about like voltage + if (i.unit in unitToMeasurement) { + acc[unitToMeasurement[i.unit]].push(i); + } + return acc; + }, { + temperature: [], + humidity: [], + co2: [], + chemicals: [], + dust: [], + }); + const latestReading = {}; + let name = ''; + const timeseries = Object.keys(measurements).filter(measurement => ( + measurements[measurement].length > 0 + )).reduce((acc, measurement) => { + const device = { + space_id: sensor.space_id, + id: sensor.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: sensor.id, + reading: latestReading, + }, + }; + } + + generateBoilerTimeSeries = (sensorNode, props) => { const boilerChannels = sensorNode.data.reduce((acc, i) => { if (i.channel_id in acc) { acc[i.channel_id].data.push(i); @@ -209,29 +314,32 @@ export class SensorGraph extends Component { data: [], }, }); + // Get the device location + const device = { + space_id: sensorNode.space_id, + id: sensorNode.id, + }; + const spaceOfNode = this.findDeviceLocation(device, props); + return Object.keys(boilerChannels).map((cid) => { const type = TEMPERATURE_PROBE_LOCATION_REVERSE[boilerChannels[cid].type]; - return this.generateTimeSeries({ - space_id: sensorNode.space_id, - id: sensorNode.id, - data: boilerChannels[cid].data, - }, ` - ${type}`); + let name = spaceOfNode.deviceName ? spaceOfNode.deviceName : `node ${sensorNode.id}`; + name = `${name} - ${type}`; + const data = boilerChannels[cid].data; + return this.generateTimeSeries(data, name); }); } - generateTimeSeries = (sensorNode, addToName = '') => { - let nodeName = `node ${sensorNode.id} `; - let spaceOfNode = null; - + generateTimeSeries = (data, deviceName) => { // Get all points let prevPoint = null; - const points = sensorNode.data.reduce((acc, i) => { + 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 10 minutes, add null values - if (diffSeconds > 600) { + // 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()) { @@ -247,13 +355,9 @@ export class SensorGraph extends Component { }, []); - spaceOfNode = this.findNodeLocation(sensorNode); - nodeName = spaceOfNode.nodeName ? spaceOfNode.nodeName : nodeName; - nodeName = `${nodeName}${addToName}`; - return new TimeSeries({ - name: nodeName, - columns: ['time', nodeName], + name: deviceName, + columns: ['time', deviceName], points, }); } @@ -264,51 +368,54 @@ export class SensorGraph extends Component { width: 1, }) - generateCategories = (nodes) => { - const stuff = nodes.map(i => ({ key: i.name(), label: i.name() })); - return stuff; - } - fetchData = debounce((from) => { this.props.loadAllNodeData({ building_id: this.props.buildingId, nodes: '', data: '', from: from.toUTCString(), - unit_id: 3, // Temperature + }); + this.props.loadWeather({ + measurement: 'temperature', + interval: 'hourly', + location: 'New_York:NY', + date_start: moment(from).format('YYYY-MM-DD hh:mm:ss'), + date_end: moment(this.state.today).format('YYYY-MM-DD hh:mm:ss'), }); }, 500); - updateTimeRange = (timerange) => { - if (!this.state.showChart || this.state.timeseries.length === 0) { - return; + updateTimeRangeStart = (momentDate) => { + const date = momentDate.toDate(); + if (date < this.state.from) { + this.fetchData(date); } - 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([date, this.state.timerange.end()]), + from: date, + }); + } + updateTimeRangeEnd = (momentDate) => { + const date = momentDate.toDate(); this.setState({ - timerange: new TimeRange([timerange.begin(), endDate]), - from: timerange.begin(), + timerange: new TimeRange([this.state.timerange.begin(), date]), }); } - renderLineGraph = () => { - if (!this.state.showChart) { + renderLineGraph = (timeseries) => { + if (!timeseries.length) { return ( ); } - - const lineGraph = this.state.timeseries.map(line => ( + timeseries.map((line) => { + return line; + }); + const lineGraph = timeseries.map(line => ( ; - } + renderGraph = (timeseries, min, max, unit, baselines, bottomText) => { return ( -
    +
    -
    - {this.state.showChart && +
    + {timeseries.length > 0 && ({ key: i.name(), label: i.name() }))} + categories={timeseries.map( + i => ({ key: i.name(), label: i.name() }) + )} style={styler(Object.keys(this.state.lineStyles).map(key => ( this.state.lineStyles[key])) )} @@ -341,41 +447,292 @@ export class SensorGraph extends Component { }
    - {this.props.sensors.loading && !this.props.sensors.error && } + {(this.props.weather.loading || (this.props.sensors.loading && !this.props.sensors.error)) && }
    -
    - this.setState({ tracker })} - onTimeRangeChanged={timerange => this.updateTimeRange(timerange)} - enablePanZoom - > - - - - {this.renderLineGraph()} - + + + + - - - + + {this.renderLineGraph(timeseries)} + {baselines} + + + +
    +

    {bottomText}

    +
    + ); + } + + renderLatestReadings = () => { + const { latestReadings } = this.state; + if (Object.keys(latestReadings).length === 0) { + return 'No latest readings'; + } + 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 ( +
    + +
    + + + + + + + + + + + {latestReadingDevice.temperature ? ( + + + + + + ) : null} + {latestReadingDevice.humidity ? ( + + + + + + ) : null} + {latestReadingDevice.co2 ? ( + + + + + + ) : null} + {latestReadingDevice.chemicals ? ( + + + + + + ) : null} + {latestReadingDevice.dust ? ( + + + + + + ) : null} + +
    TimestampMeasurementValue
    {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() { + let renderedGraph = null; + switch (this.state.activeGraphTab) { + case 'Temperature': + renderedGraph = this.renderGraph( + this.state.temperatureTimeseries, + 0, + 100, + '°F', + [ + (), + (), + ], + 'Thresholds determined using the OSHA. 2003 standard', + ); + break; + case 'Humidity': + renderedGraph = this.renderGraph( + this.state.humidityTimeseries, + 0, + 100, + '% Humidity', + [ + (), + (), + ], + 'Thresholds determined using the OSHA. 2003 standard', + ); + break; + case 'CO2': + renderedGraph = this.renderGraph( + this.state.co2Timeseries, + 300, + 1200, + 'PPM CO2', + [ + (), + ], + 'Threshold determined using the ASHRAE 2010 standard', + ); + break; + case 'Chemicals': + renderedGraph = this.renderGraph( + this.state.chemicalsTimeseries, + 0, + 1000, + 'PPB Chemicals', + [ + (), + ], + 'Threshold determined using the EPA 2014 standard', + ); + break; + case 'Dust': + renderedGraph = this.renderGraph( + this.state.dustTimeseries, + 5, + 40, + 'mcg3 Dust', + [ + (), + ], + 'Threshold determined using the EPA 2014 standard', + ); + break; + + default: + break; + } + /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ + return ( +
    + + + +

    Sensor Data

    + + +

    + From + +

    + + + To + + +
    + + {this.state.firstLoad ? : renderedGraph} +
    + +

    ( + this.setState({ latestReadingsOpen: !this.state.latestReadingsOpen }) + )} + style={{ cursor: 'pointer' }} + > + Latest Readings {' '} + +

    + + {this.state.firstLoad ? : this.renderLatestReadings()} + +
    ); } @@ -386,7 +743,6 @@ 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 deleted file mode 100644 index 8c4aa948de9db7e82c0d72441ba218e5386411d2..0000000000000000000000000000000000000000 --- a/src/containers/Sensors/SensorGraphIAQ.js +++ /dev/null @@ -1,777 +0,0 @@ -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, - UncontrolledTooltip, -} 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

    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    TimestampMeasurementValue
    {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 - {' '} - - - - - Threshold determined using the OSHA. 2003 standard - -

    -
    -
    - {this.state.temperatureTimeseries.length > 0 && - ({ key: i.name(), label: i.name() }) - )} - style={styler(Object.keys(this.state.lineStyles).map(key => ( - this.state.lineStyles[key])) - )} - type="line" - /> - } -
    -
    - {this.props.sensors.loading && !this.props.sensors.error && } -
    -
    -
    -
    - - - - - {this.renderLineGraph(this.state.temperatureTimeseries)} - - - - - -
    -
    -
    -
    -
    -

    - Humidity - {' '} - - - - - Threshold determined using the OSHA. 2003 standard - -

    -
    -
    - {this.state.humidityTimeseries.length > 0 && - ({ key: i.name(), label: i.name() }) - )} - style={styler(Object.keys(this.state.lineStyles).map(key => ( - this.state.lineStyles[key])) - )} - type="line" - /> - } -
    -
    - {this.props.sensors.loading && !this.props.sensors.error && } -
    -
    -
    -
    - - - - - {this.renderLineGraph(this.state.humidityTimeseries)} - - - - - -
    -
    -
    -
    -
    -

    - CO2 - {' '} - - - - - Threshold determined using the ASHRAE 2010 standard - -

    -
    -
    - {this.state.co2Timeseries.length > 0 && - ({ key: i.name(), label: i.name() }) - )} - style={styler(Object.keys(this.state.lineStyles).map(key => ( - this.state.lineStyles[key])) - )} - type="line" - /> - } -
    -
    - {this.props.sensors.loading && !this.props.sensors.error && } -
    -
    -
    -
    - - - - - {this.renderLineGraph(this.state.co2Timeseries)} - - - - -
    -
    -
    -
    -
    -

    - Chemicals - {' '} - - - - - Threshold determined using the EPA 2014 standard - -

    -
    -
    - {this.state.chemicalsTimeseries.length > 0 && - ({ key: i.name(), label: i.name() }) - )} - style={styler(Object.keys(this.state.lineStyles).map(key => ( - this.state.lineStyles[key])) - )} - type="line" - /> - } -
    -
    - {this.props.sensors.loading && !this.props.sensors.error && } -
    -
    -
    -
    - - - - - {this.renderLineGraph(this.state.chemicalsTimeseries)} - - - - -
    -
    -
    -
    -
    -

    - Dust - {' '} - - - - - Threshold determined using the EPA 2014 standard - -

    -
    -
    - {this.state.dustTimeseries.length > 0 && - ({ key: i.name(), label: i.name() }) - )} - style={styler(Object.keys(this.state.lineStyles).map(key => ( - this.state.lineStyles[key])) - )} - type="line" - /> - } -
    -
    - {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 b11cec5d5ca59cbd3b14da353128ce8002f66351..fd18a1cddf55f20608fafdaeb8e575c445ca6336 100644 --- a/src/containers/Sensors/Sensors.js +++ b/src/containers/Sensors/Sensors.js @@ -4,14 +4,11 @@ import { connect } from 'react-redux'; import LinkBarDetail from '../../components/LinkBarDetail'; import SensorGraphCon from './SensorGraph'; import BuildingAreaTable from '../BuildingArea/BuildingAreaTable'; +import BuildingEventsTable from '../BuildingEvents/BuildingEventsTable'; class Sensors extends Component { - state = { renderIaq: false } - - setRenderIaq = (renderIaq) => { - this.setState({ renderIaq }); - } + state = { } render() { return ( @@ -23,13 +20,6 @@ class Sensors extends Component { { name: 'Sensors', url: 'null' }, ]} 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`, @@ -41,7 +31,9 @@ class Sensors extends Component { + - - - -
    - ); - } -} - -SensorsIAQ.propTypes = { - buildingId: PropTypes.string, -}; - -export default connect(null, null)(SensorsIAQ); diff --git a/src/containers/Weather/reducer.js b/src/containers/Weather/reducer.js index bda12282ab838e2f7900f24b6dad2f3a8bad722f..1143bfc24b4848893620974a587fbfa7cc7f00d5 100644 --- a/src/containers/Weather/reducer.js +++ b/src/containers/Weather/reducer.js @@ -11,16 +11,34 @@ export const initState = { }; export default (state = initState, action) => { + let weatherData = null; + let location = null; + let interval = null; switch (action.type) { case WEATHER_REQUESTED: return { ...state, weatherLoading: true, weatherError: false }; case WEATHER_SUCCEEDED: + weatherData = action.weatherData.data.reduce((acc, weatherPoint) => { + location = weatherPoint.tags.location; + if (!(location in acc)) { + acc[location] = {}; + } + interval = weatherPoint.tags.interval; + if (!(interval in acc[location])) { + acc[location][interval] = []; + } + acc[location][interval].push({ + ts: weatherPoint.time, + val: weatherPoint.fields.value, + }); + return acc; + }, {}); return { ...state, weatherLoading: false, weatherError: false, - weatherData: action.weatherData.data, + weatherData, }; case WEATHER_FAILED: diff --git a/src/containers/Weather/sagas.js b/src/containers/Weather/sagas.js index c2e7c5c3f411e05236041bd1fcff523d9a7f8885..86b4ffefa8dd36b038828a2a3d4362d8b61f8e2d 100644 --- a/src/containers/Weather/sagas.js +++ b/src/containers/Weather/sagas.js @@ -12,7 +12,8 @@ import { } from './actions'; function* getWeather(action) { - yield SagaRequests.get(action, weatherURL, weatherLoaded, weatherFailed); + const temp = SagaRequests.get(action, weatherURL, weatherLoaded, weatherFailed); + yield temp; } diff --git a/src/reducers.js b/src/reducers.js index 01448c9569ae32d43bef5ea58b1fe81fd87d4d53..a6b710cfccb473f998eb3378f70b6372d23a1d55 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -15,6 +15,7 @@ import user from './containers/User/reducer'; import sensors from './containers/Sensors/reducer'; import buildingArea from './containers/BuildingArea/reducer'; import weather from './containers/Weather/reducer'; +import events from './containers/Event/reducer'; export default combineReducers({ @@ -33,4 +34,5 @@ export default combineReducers({ sensors, buildingArea, weather, + events, }); diff --git a/src/routes.js b/src/routes.js index ca9a6d86b597064597c2cbd31f1183d358cd5cab..381b8699d32f795b53981ae64b23c82a131b7e24 100644 --- a/src/routes.js +++ b/src/routes.js @@ -20,7 +20,6 @@ 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'; @@ -56,7 +55,6 @@ export default ( - diff --git a/src/sagas.js b/src/sagas.js index 7ef7849ac909f44fa203273ad145f1153bb9778f..7f33bf76b92293a53360514ddf6728f329287a39 100644 --- a/src/sagas.js +++ b/src/sagas.js @@ -12,6 +12,7 @@ import userSaga from './containers/User/sagas'; import sensorsSaga from './containers/Sensors/sagas'; import buildingAreaSaga from './containers/BuildingArea/sagas'; import weatherSaga from './containers/Weather/sagas'; +import eventsSaga from './containers/Event/sagas'; export default function* rootSaga() { @@ -30,5 +31,6 @@ export default function* rootSaga() { sensorsSaga(), buildingAreaSaga(), weatherSaga(), + eventsSaga(), ]; } diff --git a/src/utils/restServices.js b/src/utils/restServices.js index 18aad52a20a5f45fa7f8336898e4aa37577159f0..c7158fbf8d8dac5c91cdd97bf28b300c6421d4a7 100644 --- a/src/utils/restServices.js +++ b/src/utils/restServices.js @@ -47,6 +47,7 @@ export const customImpactReportURL = `${reportService}/customimpact/`; export const gatewayURL = `${iotService}/gateway/`; export const sensewareNodeURL = `${iotService}/sensewarenode/`; export const sensorImageURL = `${iotService}/sensorimage/`; +export const eventsURL = `${iotService}/event/`; export const userURL = `${userService}/user/`; export const userGroupsURL = `${userService}/usergroup/`;