diff --git a/client/app.js b/client/app.js index c2dc237..9d6c715 100644 --- a/client/app.js +++ b/client/app.js @@ -1,13 +1,14 @@ import 'babel-polyfill'; import 'bootstrap/dist/js/bootstrap.min'; - import React from 'react'; import ReactDOM from 'react-dom'; -import Layout from './dashboard/layout/layout'; +import {Router} from 'react-router'; -export default function(){ +import {ROUTES} from './dashboard/routes'; + +export default function(history){ ReactDOM.render( - React.createElement(Layout), + React.createElement(Router, {routes: ROUTES, history: history}), document.getElementById('root') ); }; diff --git a/client/config/design/app.js b/client/config/design/app.js index df46f77..07a045d 100644 --- a/client/config/design/app.js +++ b/client/config/design/app.js @@ -1,5 +1,6 @@ import Styles from 'config/styles'; import Templates from 'config/templates'; +import {hashHistory} from 'react-router'; import app from './../../app'; Promise.all([ @@ -7,5 +8,5 @@ Promise.all([ Styles.sync() ]).then(()=>{ jQuery('#compiling_layouts').remove(); - app(); + app(hashHistory); }); diff --git a/client/config/design/templates.js b/client/config/design/templates.js index 745b232..c798681 100644 --- a/client/config/design/templates.js +++ b/client/config/design/templates.js @@ -11,11 +11,6 @@ const TEMPLATE_ROUTES = Object.freeze({ power: 'dashboard/power/power.rt' }); -const COMPONENTS = { - Power: Power, - Energy: Energy -}; - var TEMPLATES = {}; class Templates { @@ -40,16 +35,14 @@ class Templates { url: TEMPLATE_ROUTES[view] }).done((template)=>{ var code = rt.convertTemplateToReact(template, {modules: 'none', name: view}), - context = {}; - code = code.replace('var '+view+' = ', 'context.'+view+' = '); + eval_context = {}; + code = code.replace('var ' + view + ' = ', 'eval_context.' + view + ' = '); new Function('with(this){ ' + code + ' } ').call({ - Energy: Energy, - Power: Power, - context: context, + eval_context: eval_context, '_': _, 'React': React }); - TEMPLATES[view] = context[view]; + TEMPLATES[view] = eval_context[view]; fnResolve(); }); } diff --git a/client/config/development/app.js b/client/config/development/app.js index 1a330ac..e86c9ec 100644 --- a/client/config/development/app.js +++ b/client/config/development/app.js @@ -1,3 +1,4 @@ +import {browserHistory} from 'react-router'; import app from './../../app'; -app(); +app(browserHistory); diff --git a/client/config/development/templates.js b/client/config/development/templates.js index be27504..5068f77 100644 --- a/client/config/development/templates.js +++ b/client/config/development/templates.js @@ -1,15 +1,29 @@ // All react templates should be pre-compiled for development. // run 'gulp compile_react_templates' +import fs from 'fs'; + +import aboutRt from './../../dashboard/about/about.rt.js'; +import houseRt from './../../dashboard/house/house.rt.js'; import layoutRt from './../../dashboard/layout/layout.rt.js'; import energyRt from './../../dashboard/energy/energy.rt.js'; +import energyGraphRt from './../../dashboard/energy/graph/graph.rt.js'; +import energyTableRt from './../../dashboard/energy/table/table.rt.js'; import powerRt from './../../dashboard/power/power.rt.js'; +import powerGraphRt from './../../dashboard/power/graph/graph.rt.js'; +import powerTableRt from './../../dashboard/power/table/table.rt.js'; -const TEMPLATES = Object.freeze({ +const TEMPLATES = { + about: aboutRt, + house: houseRt, layout: layoutRt, energy: energyRt, + energy_graph: energyGraphRt, + energy_table: energyTableRt, power: powerRt, -}); + power_graph: powerGraphRt, + power_table: powerTableRt +}; class Templates { diff --git a/client/dashboard/about/about.component.js b/client/dashboard/about/about.component.js new file mode 100644 index 0000000..3802dfe --- /dev/null +++ b/client/dashboard/about/about.component.js @@ -0,0 +1,13 @@ +import React from 'react'; +import Templates from 'config/templates'; + +class AboutComponent extends React.Component { + + render() { + var aboutRt = Templates.forComponent('about'); + return aboutRt.call(this); + } + +} + +export default AboutComponent; diff --git a/client/dashboard/about/about.rt b/client/dashboard/about/about.rt new file mode 100644 index 0000000..fe4a2cb --- /dev/null +++ b/client/dashboard/about/about.rt @@ -0,0 +1,18 @@ +
+
+
About
+
+

This is a Spike bundle prototype using the following lirbaries:

+
    +
  • React
  • +
  • React Templates
  • +
  • React Router
  • +
  • LokiJs - persisting API calls to indexedDb
  • +
  • Webpack - hot mode developing and app bundling
  • +
  • Babel - ES6 transpiler
  • +
+

The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.

+

Select a house below to get started.

+
+
+
diff --git a/client/dashboard/about/about.rt.js b/client/dashboard/about/about.rt.js new file mode 100644 index 0000000..0c32ab8 --- /dev/null +++ b/client/dashboard/about/about.rt.js @@ -0,0 +1,5 @@ +import React from 'react'; +import _ from 'lodash'; +export default function () { + return React.createElement('div', { 'id': 'about' }, React.createElement('div', { 'className': 'panel panel-default' }, React.createElement('div', { 'className': 'panel-heading' }, 'About'), React.createElement('div', { 'className': 'panel-body' }, React.createElement('p', {}, 'This is a Spike bundle prototype using the following lirbaries:'), React.createElement('ul', {}, React.createElement('li', {}, 'React'), React.createElement('li', {}, 'React Templates'), React.createElement('li', {}, 'React Router'), React.createElement('li', {}, 'LokiJs - persisting API calls to indexedDb'), React.createElement('li', {}, 'Webpack - hot mode developing and app bundling'), React.createElement('li', {}, 'Babel - ES6 transpiler')), React.createElement('p', {}, 'The demo app consists of a dataset of 10 houses and 10 years of randomly generated power consumption and production at 15 minute intervals. You can toggle between different houses and time periods to compare and contrast the dataset.'), React.createElement('p', {}, 'Select a house below to get started.')))); +}; \ No newline at end of file diff --git a/client/dashboard/energy/energy.component.js b/client/dashboard/energy/energy.component.js new file mode 100644 index 0000000..264c930 --- /dev/null +++ b/client/dashboard/energy/energy.component.js @@ -0,0 +1,77 @@ +import React from 'react'; +import Templates from 'config/templates'; +import House from './../../models/house'; +import {RouteHelper} from './../routes'; + +class EnergyComponent extends React.Component { + + constructor(props){ + super(props); + var energy = this; + energy.state = { + loading_energy_data: true + }; + } + + componentDidMount(){ + var energy = this, + house = energy.context.house; + if (!house || energy.context.loading_energy_data) return false; + house.setEnergyData() + .then(()=>{ + energy.setState({loading_energy_data: false}); + }); + } + + componentDidUpdate(prev_props, prev_state, prev_context){ + var energy = this, + house = energy.context.house; + if (!house) return false; + if (!prev_context.house || + prev_context.house.data.id != energy.context.house.data.id || + !house.matchesYearState(prev_props.params)) { + energy.setState({loading_energy_data: true}); + house.setEnergyData() + .then(()=>{ + energy.setState({loading_energy_data: false}); // will update graph or table. + }); + } + } + + setParam(event){ + var energy = this, + param = event.target.dataset.param, + value = event.target.dataset.value, + update = {}, route_helper; + override[param] = value; + route_helper = new RouteHelper(energy.context.house, energy.props); + if (route_helper.routeUpdated()){ + route_helper.updateHouseState(); + energy.context.router.push(makeRoute(house, energy.props, override)); + } + } + + getChildContext(){ + var layout = this; + return { + loading_energy_data: layout.state.loading_energy_data + }; + } + + render() { + var energyRt = Templates.forComponent('energy'); + return energyRt.call(this); + } +} + + +EnergyComponent.childContextTypes = { + loading_energy_data: React.PropTypes.bool.isRequired +}; + +EnergyComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + router: React.PropTypes.object.isRequired +}; + +export default EnergyComponent; diff --git a/client/dashboard/energy/energy.js b/client/dashboard/energy/energy.js deleted file mode 100644 index ebf9015..0000000 --- a/client/dashboard/energy/energy.js +++ /dev/null @@ -1,130 +0,0 @@ -import React from 'react'; -import Templates from 'config/templates'; -import House from './../../models/house'; -import CalendarGridChart from './../../d3/grid/calendar_grid'; - -var Energy = React.createClass({ - - getInitialState: function(){ - var energy = this; - return { - graph_attr: 'production', - loading_data: true - }; - }, - - handleResize: function(e) { - this.setState({windowWidth: window.innerWidth}); - }, - - componentDidMount: function() { - // window.addEventListener('resize', this.handleResize); - var energy = this, - house = energy.props.house; - energy.graph_title = 'Daily Consumption'; - house.setEnergyData().then(()=>{ - energy.setState({loading_data: false}); - if (energy.props.view === 'graph') energy.initGraph(); - }); - }, - - componentWillReceiveProps: function(new_props){ - var energy = this; - if (new_props.house !== energy.props.house){ - energy.setState({loading_data: true}); - new_props.house.setEnergyData().then(()=>{ - energy.setState({loading_data: false}); - if (energy.props.view === 'graph') energy.initGraph(); - }); - } - if (new_props.view !== 'graph' && energy.props.view === 'graph') energy.destroyGraph(); - }, - - componentDidUpdate: function(prev_props, _prev_state){ - var energy = this, - house = energy.props.house; - if (prev_props.view !== 'graph' && energy.props.view === 'graph') energy.initGraph(); - if (prev_props.year !== energy.props.year){ - energy.updateCurrentMonth(); - } - }, - - setGraphAttr: function(event){ - var energy = this, - graph_attr = event.target.dataset.value; - if (graph_attr !== energy.state.graph_attr){ - energy.graph_title = 'Daily ' + event.target.innerText; - energy.setState({ - graph_attr: graph_attr - }, function(){ - if (energy.props.view === 'graph') energy.updateGraph(); - }) - } - }, - - initGraph: function(){ - var energy = this; - if (energy.graph === undefined){ - energy.graph = new CalendarGridChart({ - container: '#energy_graph', - outer_width: 800, - outer_height: 300, - date_attr: 'day', - color: '#0404B4', - toDate: (energy_datum)=>{ return energy_datum.day_to_date; } - }); - jQuery('#energy_graph').tooltip({ - selector: '.d3-chart-grid-unit', - container: 'body', - title: function(){ - var energy_datum = this.__data__, - date_s = d3.time.format('%a %b %d, %Y')(energy_datum.day_to_date), - range_value = `${Math.round(energy_datum.data[energy.state.graph_attr])} kWh`; - return `${date_s}: ${range_value}`; - } - }); - } - energy.updateGraph(); - }, - - updateGraph: function(){ - var energy = this, - house = energy.props.house; - energy.graph.rangeValue = (datum)=>{ return datum.data[energy.state.graph_attr]; } - energy.graph.drawData({ - title: energy.graph_title, - css_class: '', - min_range: 0, - max_range: 150, - values: house.energy_data - }); - }, - - destroyGraph: function(){ - var energy = this; - document.getElementById('energy_graph').innerHTML = ''; - energy.graph = undefined; - }, - - updateCurrentMonth: function(){ - var energy = this, - house = energy.props.house; - house.setEnergyData() - .then(()=>{ - if (energy.props.view === 'graph'){ - // no update necessary since year already updated in layout.rt. - energy.updateGraph(); - } else { - // force update to render correct data in table. - energy.forceUpdate(); - } - }); - }, - - render: function() { - var energyRt = Templates.forComponent('energy'); - return energyRt.call(this); - } -}); - -export default Energy; diff --git a/client/dashboard/energy/energy.rt b/client/dashboard/energy/energy.rt index 4c94242..89b1a94 100644 --- a/client/dashboard/energy/energy.rt +++ b/client/dashboard/energy/energy.rt @@ -1,39 +1,23 @@
-
- Retrieving energy data for the {this.props.house.name} household... +
+ Retrieving energy data...

Select Data

- - - - - - - - - - - - - - - - - -
DayConsumption (kWh)Production (kWh)
{energy_datum.day_to_s}{energy_datum.consumption_to_s}{energy_datum.production_to_s}
-
+ {this.props.children}
diff --git a/client/dashboard/energy/energy.rt.js b/client/dashboard/energy/energy.rt.js index a17cbd5..056e626 100644 --- a/client/dashboard/energy/energy.rt.js +++ b/client/dashboard/energy/energy.rt.js @@ -1,25 +1,20 @@ import React from 'react'; import _ from 'lodash'; -function repeatEnergy_datum1(energy_datum, energy_datumIndex) { - return React.createElement('tr', { 'key': energy_datum.scoped_id }, React.createElement('td', {}), React.createElement('td', {}, energy_datum.day_to_s), React.createElement('td', {}, energy_datum.consumption_to_s), React.createElement('td', {}, energy_datum.production_to_s)); -} export default function () { - return React.createElement('div', { 'id': 'energy_view' }, this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving energy data for the ', this.props.house.name, ' household...\n ') : null, this.props.view === 'graph' ? React.createElement('div', {}, React.createElement('h4', {}, 'Select Data'), React.createElement('div', { + return React.createElement('div', { 'id': 'energy_view' }, this.state.loading_energy_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving energy data...\n ') : null, this.props.view === 'graph' ? React.createElement('div', {}, React.createElement('h4', {}, 'Select Data'), React.createElement('div', { 'className': 'btn-group', 'role': 'group' }, React.createElement('button', { + 'data-param': 'graph_attr', 'data-value': 'consumption', 'className': _.keys(_.pick({ active: this.state.graph_attr === 'consumption' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', - 'onClick': this.setGraphAttr, + 'onClick': this.setAttr, 'type': 'button' }, 'Consumption'), React.createElement('button', { + 'data-param': 'graph_attr', 'data-value': 'production', 'className': _.keys(_.pick({ active: this.state.graph_attr === 'production' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', 'onClick': this.setGraphAttr, 'type': 'button' - }, 'Production'))) : null, this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Day'), React.createElement('th', {}, 'Consumption (kWh)'), React.createElement('th', {}, 'Production (kWh)'))), React.createElement.apply(this, [ - 'tbody', - {}, - _.map(this.props.house.energy_data, repeatEnergy_datum1.bind(this)) - ])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'energy_graph' }) : null); + }, 'Production'))) : null, '\n ', this.props.children, '\n'); }; \ No newline at end of file diff --git a/client/dashboard/energy/graph/graph.component.js b/client/dashboard/energy/graph/graph.component.js new file mode 100644 index 0000000..8683695 --- /dev/null +++ b/client/dashboard/energy/graph/graph.component.js @@ -0,0 +1,76 @@ +import React from 'react'; + +import Templates from 'config/templates'; +import CalendarGridChart from './../../../d3/grid/calendar_grid'; +import House from './../../../models/house'; + +class GraphComponent extends React.Component { + + componentDidMount(){ + var energy_graph = this, + house = energy_graph.context.house; + if (!energy_graph.context.loading_energy_data) energy_graph.updateGraph(); + } + + componentDidUpdate(prev_props, prev_state, prev_context){ + var energy_graph = this, + house = energy_graph.context.house; + if (energy_graph.context.loading_energy_data) {return false;} + if (!prev_context.house || + prev_context.loading_energy_data || + prev_context.house.id != energy_graph.context.house.id) { + energy_graph.updateGraph(); + } + } + + + updateGraph(){ + var energy_graph = this, + house = energy_graph.context.house, + graph_attr = energy_graph.props.params.graph_attr; + + if (energy_graph.graph === undefined){ + energy_graph.graph = new CalendarGridChart({ + container: '#energy_graph', + outer_width: 800, + outer_height: 300, + date_attr: 'day', + color: '#0404B4', + toDate: (energy_datum)=>{ return energy_datum.day_to_date; } + }); + jQuery('#energy_graph').tooltip({ + selector: '.d3-chart-grid-unit', + container: 'body', + title: function(){ + var energy_datum = this.__data__, + date_s = d3.time.format('%a %b %d, %Y')(energy_datum.day_to_date), + range_value = `${Math.round(energy_datum.data[graph_attr])} kWh`; + return `${date_s}: ${range_value}`; + } + }); + } + + energy_graph.graph.rangeValue = (datum)=>{ return datum.data[graph_attr]; } + energy_graph.graph.drawData({ + title: energy_graph.graph_title, + css_class: '', + min_range: 0, + max_range: 150, + values: house.energy_data + }); + } + + render(){ + var energyGraphRt = Templates.forComponent('energy_graph'); + return energyGraphRt.call(this); + } + +} + +GraphComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + loading_energy_data: React.PropTypes.bool.isRequired, + router: React.PropTypes.object.isRequired +}; + +export default GraphComponent; diff --git a/client/dashboard/energy/graph/graph.rt b/client/dashboard/energy/graph/graph.rt new file mode 100644 index 0000000..ef989f7 --- /dev/null +++ b/client/dashboard/energy/graph/graph.rt @@ -0,0 +1 @@ +
diff --git a/client/dashboard/energy/graph/graph.rt.js b/client/dashboard/energy/graph/graph.rt.js new file mode 100644 index 0000000..c02eac8 --- /dev/null +++ b/client/dashboard/energy/graph/graph.rt.js @@ -0,0 +1,5 @@ +import React from 'react'; +import _ from 'lodash'; +export default function () { + return React.createElement('div', { 'id': 'energy_graph' }); +}; \ No newline at end of file diff --git a/client/dashboard/energy/table/table.component.js b/client/dashboard/energy/table/table.component.js new file mode 100644 index 0000000..9a7d596 --- /dev/null +++ b/client/dashboard/energy/table/table.component.js @@ -0,0 +1,20 @@ +import React from 'react'; +import Templates from 'config/templates'; + +import House from './../../../models/house'; + +class TableComponent extends React.Component { + + render() { + var tableRt = Templates.forComponent('energy_table'); + return tableRt.call(this); + } + +} + +TableComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + router: React.PropTypes.object.isRequired +}; + +export default TableComponent; diff --git a/client/dashboard/energy/table/table.rt b/client/dashboard/energy/table/table.rt new file mode 100644 index 0000000..6b54504 --- /dev/null +++ b/client/dashboard/energy/table/table.rt @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +
DayConsumption (kWh)Production (kWh)
{energy_datum.day_to_s}{energy_datum.consumption_to_s}{energy_datum.production_to_s}
diff --git a/client/dashboard/energy/table/table.rt.js b/client/dashboard/energy/table/table.rt.js new file mode 100644 index 0000000..721cbb8 --- /dev/null +++ b/client/dashboard/energy/table/table.rt.js @@ -0,0 +1,12 @@ +import React from 'react'; +import _ from 'lodash'; +function repeatEnergy_datum1(energy_datum, energy_datumIndex) { + return React.createElement('tr', { 'key': energy_datum.scoped_id }, React.createElement('td', {}), React.createElement('td', {}, energy_datum.day_to_s), React.createElement('td', {}, energy_datum.consumption_to_s), React.createElement('td', {}, energy_datum.production_to_s)); +} +export default function () { + return this.context.house ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Day'), React.createElement('th', {}, 'Consumption (kWh)'), React.createElement('th', {}, 'Production (kWh)'))), React.createElement.apply(this, [ + 'tbody', + {}, + _.map(this.context.house.energy_data, repeatEnergy_datum1.bind(this)) + ])) : null; +}; \ No newline at end of file diff --git a/client/dashboard/house/house.component.js b/client/dashboard/house/house.component.js new file mode 100644 index 0000000..c75e99f --- /dev/null +++ b/client/dashboard/house/house.component.js @@ -0,0 +1,64 @@ +import React from 'react'; +import Templates from 'config/templates'; +import House from './../../models/house'; +import {RouteHelper} from './../routes'; +import EnergyComponent from './../energy/energy.component'; +import PowerComponent from './../power/power.component'; + +class HouseComponent extends React.Component { + + constructor(props){ + super(props); + this.renders = 0; + } + + setParam(event){ + var house_component = this, + house = house_component.context.house, + param = event.target.dataset.param, + value = event.target.dataset.value, + update = {}, route_helper; + update[param] = value; + route_helper = new RouteHelper(house, house_component.props, update); + if (route_helper.routeUpdated()){ + route_helper.updateHouseState(); + if (house_component.renders < 10){ + house_component.context.router.push(route_helper.newRoute()); + house_component.renders += 1; + } + } + } + + graphSelected(){ + var house_component = this; + return RouteHelper.graphSelected(house_component.props.routes); + } + + tableSelected(){ + var house_component = this; + return RouteHelper.tableSelected(house_component.props.routes); + } + + energySelected(){ + var house_component = this; + return RouteHelper.energySelected(house_component.props.routes); + } + + powerSelected(){ + var house_component = this; + return RouteHelper.powerSelected(house_component.props.routes); + } + + render() { + var houseRt = Templates.forComponent('house'); + return houseRt.call(this); + } + +}; + +HouseComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + router: React.PropTypes.object.isRequired +}; + +export default HouseComponent; diff --git a/client/dashboard/house/house.rt b/client/dashboard/house/house.rt new file mode 100644 index 0000000..e2bb0e5 --- /dev/null +++ b/client/dashboard/house/house.rt @@ -0,0 +1,49 @@ +
+

Select dataset:

+
+ + +
+ +

View as:

+
+ + +
+ +
+

Select dates:

+
+ +
+

+ + {this.props.children} +
diff --git a/client/dashboard/house/house.rt.js b/client/dashboard/house/house.rt.js new file mode 100644 index 0000000..7fd4f39 --- /dev/null +++ b/client/dashboard/house/house.rt.js @@ -0,0 +1,48 @@ +import React from 'react'; +import _ from 'lodash'; +function repeatYear1(year, yearIndex) { + return React.createElement('button', { + 'data-param': 'year', + 'data-value': year, + 'key': 'data-year-' + year, + 'className': 'btn-info btn btn-sm' + ' ' + _.keys(_.pick({ active: year == this.context.house.state.year }, _.identity)).join(' '), + 'onClick': this.setParam.bind(this) + }, year); +} +export default function () { + return React.createElement('div', { 'id': 'house' }, React.createElement('h4', {}, 'Select dataset:'), React.createElement('div', { + 'className': 'btn-group', + 'role': 'group' + }, React.createElement('button', { + 'data-param': 'dataset', + 'data-value': 'energy', + 'className': _.keys(_.pick({ active: this.energySelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary', + 'onClick': this.setParam.bind(this), + 'type': 'button' + }, 'Daily Energy Statistics'), React.createElement('button', { + 'data-param': 'dataset', + 'data-value': 'power', + 'className': _.keys(_.pick({ active: this.powerSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary', + 'onClick': this.setParam.bind(this), + 'type': 'button' + }, '15-minute Power Statistics')), React.createElement('h4', {}, 'View as:'), React.createElement('div', { + 'className': 'btn-group', + 'role': 'group' + }, React.createElement('button', { + 'data-param': 'view', + 'data-value': 'graph', + 'className': _.keys(_.pick({ active: this.graphSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary', + 'onClick': this.setParam.bind(this), + 'type': 'button' + }, 'Graph'), React.createElement('button', { + 'data-param': 'view', + 'data-value': 'table', + 'className': _.keys(_.pick({ active: this.tableSelected() }, _.identity)).join(' ') + ' ' + 'btn btn-primary', + 'onClick': this.setParam.bind(this), + 'type': 'button' + }, 'Table')), this.context.house ? React.createElement('div', {}, React.createElement('h4', {}, 'Select dates:'), React.createElement.apply(this, [ + 'div', + { 'className': 'btn-group' }, + _.map(this.context.house.years, repeatYear1.bind(this)) + ])) : null, React.createElement('br', {}), '\n\n ', this.props.children, '\n'); +}; \ No newline at end of file diff --git a/client/dashboard/layout/layout.component.js b/client/dashboard/layout/layout.component.js new file mode 100644 index 0000000..502afb5 --- /dev/null +++ b/client/dashboard/layout/layout.component.js @@ -0,0 +1,84 @@ +import React from 'react'; +import Templates from 'config/templates'; +import House from './../../models/house'; +import PowerDatum from './../../models/power_datum'; +import {RouteHelper} from './../routes'; + +class LayoutComponent extends React.Component { + + constructor(props, context){ + super(props, context); + this.renders = 0; + this.state = { + houses: null, + house: null, + requesting_data: true + }; + } + + componentDidMount() { + var layout = this; + House.ensureHouses().then((houses)=>{ + var house = null; + if (layout.props.params.house_id != undefined){ + house = houses.find((h)=>{ return h.data.id == layout.props.params.house_id; }); + var route_helper = new RouteHelper(house, layout.props); + if (route_helper.paramsHaveDateState()) route_helper.updateHouseToParams(); + } + layout.setState({ + houses: houses, + requesting_data: false, + house: house }); + }); + } + + setHouse(event){ + var layout = this, + house_id = event.target.value, + old_house = layout.state.house, + house = layout.state.houses.find((house)=>{ return house.data.id == house_id }); + if (!old_house || old_house.id != house_id){ + var route_helper = new RouteHelper(house, layout.props); + route_helper.updateHouseToParams(); + layout.setState({house: house}, ()=>{ + if (layout.renders < 10){ + layout.context.router.push(route_helper.newRoute()); + layout.renders += 1 + } + if (old_house) old_house.closeDb(); + }); + } + } + + refreshData(){ + var layout = this, + houses = layout.state.houses, + all = []; + houses.forEach((house)=>{ + all.push(house.clearData()); + }); + Promise.all(all) + .then(()=>{ + window.location.reload(); + }); + } + + getChildContext(){ + var layout = this; + return {house: layout.state.house}; + } + + render() { + var layoutRt = Templates.forComponent('layout'); + return layoutRt.call(this); + } +} + +LayoutComponent.contextTypes = { + router: React.PropTypes.object.isRequired +}; + +LayoutComponent.childContextTypes = { + house: React.PropTypes.instanceOf(House) +}; +export default LayoutComponent; diff --git a/client/dashboard/layout/layout.js b/client/dashboard/layout/layout.js deleted file mode 100644 index ea8d8c1..0000000 --- a/client/dashboard/layout/layout.js +++ /dev/null @@ -1,89 +0,0 @@ -import React from 'react'; -import Templates from 'config/templates'; -import House from './../../models/house'; -import PowerDatum from './../../models/power_datum'; - -var Layout = React.createClass({ - - getInitialState: function(){ - var layout = this; - return { - houses: null, - house: null, - view: 'graph', - dataset: 'power', - requesting_data: true - }; - }, - - handleResize: function(e) { - this.setState({windowWidth: window.innerWidth}); - }, - - componentDidMount: function() { - var layout = this; - // window.addEventListener('resize', this.handleResize); - House.ensureHouses().then((houses)=>{ - layout.setState({ - houses: houses, - house: houses[0], - requesting_data: false, - month: houses[0].current_month, - year: houses[0].current_year - }); - }); - }, - - setHouse: function(event){ - var layout = this, - house_id = event.target.value, - old_house = layout.state.house, - house = layout.state.houses.find((house)=>{ return house.data.id == house_id }); - layout.setState({house: house}, ()=>{ - old_house.closeDb(); - }); - }, - - setView: function(event){ - var layout = this, - view = event.target.dataset.value; - layout.view_name = event.target.innerText; - layout.setState({view: view}); - }, - - setDataset: function(event){ - var layout = this, - dataset = event.target.dataset.value; - layout.setState({dataset: dataset}); - }, - - setYear: function(event){ - var layout = this, - year = event.target.dataset.value, - house = layout.state.house; - if (year != house.current_year){ - house.setYear(year); - layout.setState({year: year}); - } - }, - - refreshData: function(){ - var layout = this, - houses = layout.state.houses, - all = []; - houses.forEach((house)=>{ - all.push(house.clearData()); - }); - Promise.all(all) - .then(()=>{ - window.location.reload(); - }); - }, - - render: function() { - var layoutRt = Templates.forComponent('layout'); - return layoutRt.call(this); - } -}); - -export default Layout; diff --git a/client/dashboard/layout/layout.rt b/client/dashboard/layout/layout.rt index 94c7255..68578d2 100644 --- a/client/dashboard/layout/layout.rt +++ b/client/dashboard/layout/layout.rt @@ -1,60 +1,11 @@ - -
Retrieving houses...

Select household:

- - + -

Select dataset:

-
- - -
- -

View as:

-
- - -
- -

Select dates:

-
- -

- - - + {this.props.children}
diff --git a/client/dashboard/layout/layout.rt.js b/client/dashboard/layout/layout.rt.js index 18ed882..f3d8a7b 100644 --- a/client/dashboard/layout/layout.rt.js +++ b/client/dashboard/layout/layout.rt.js @@ -1,69 +1,23 @@ import React from 'react'; import _ from 'lodash'; -import Energy from './../energy/energy'; -import Power from './../power/power'; function repeatHouse1(house, houseIndex) { return React.createElement('option', { 'value': house.data.id, 'key': house.scoped_id }, house.data.name); } -function repeatYear2(year, yearIndex) { - return React.createElement('button', { - 'data-value': year, - 'key': 'data-year-' + year, - 'className': 'btn-info btn btn-sm' + ' ' + _.keys(_.pick({ active: year == this.state.house.current_year }, _.identity)).join(' '), - 'onClick': this.setYear - }, year); -} export default function () { return React.createElement('div', { 'id': 'layout' }, this.state.requesting_data ? React.createElement('div', { 'className': 'alert alert-warning' }, 'Retrieving houses...') : null, React.createElement('h4', {}, 'Select household:'), this.state.houses ? React.createElement.apply(this, [ 'select', { + 'id': 'houses_select', 'className': 'form-control', - 'onChange': this.setHouse + 'onChange': this.setHouse.bind(this), + 'value': this.props.params.house_id }, _.map(this.state.houses, repeatHouse1.bind(this)) ]) : null, this.state.house ? React.createElement('button', { - 'onClick': this.refreshData, + 'onClick': this.refreshData.bind(this), 'className': 'btn btn-xs btn-default' - }, 'Refresh House Data') : null, React.createElement('h4', {}, 'Select dataset:'), React.createElement('div', { - 'className': 'btn-group', - 'role': 'group' - }, React.createElement('button', { - 'data-value': 'energy', - 'className': _.keys(_.pick({ active: this.state.dataset === 'energy' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', - 'onClick': this.setDataset, - 'type': 'button' - }, 'Daily Energy Statistics'), React.createElement('button', { - 'data-value': 'power', - 'className': _.keys(_.pick({ active: this.state.dataset === 'power' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', - 'onClick': this.setDataset, - 'type': 'button' - }, '15-minute Power Statistics')), React.createElement('h4', {}, 'View as:'), React.createElement('div', { - 'className': 'btn-group', - 'role': 'group' - }, React.createElement('button', { - 'data-value': 'graph', - 'className': _.keys(_.pick({ active: this.state.view === 'graph' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', - 'onClick': this.setView, - 'type': 'button' - }, 'Graph'), React.createElement('button', { - 'data-value': 'table', - 'className': _.keys(_.pick({ active: this.state.view === 'table' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', - 'onClick': this.setView, - 'type': 'button' - }, 'Table')), React.createElement('h4', {}, 'Select dates:'), React.createElement.apply(this, [ - 'div', - { 'className': 'btn-group' }, - this.state.house ? _.map(this.state.house.years, repeatYear2.bind(this)) : null - ]), React.createElement('br', {}), this.state.house && this.state.dataset === 'energy' ? React.createElement(Energy, { - 'house': this.state.house, - 'view': this.state.view, - 'year': this.state.year - }) : null, this.state.house && this.state.dataset === 'power' ? React.createElement(Power, { - 'house': this.state.house, - 'view': this.state.view, - 'year': this.state.year - }) : null); + }, 'Refresh House Data') : null, '\n\n ', this.props.children, '\n'); }; \ No newline at end of file diff --git a/client/dashboard/power/graph/graph.component.js b/client/dashboard/power/graph/graph.component.js new file mode 100644 index 0000000..3854725 --- /dev/null +++ b/client/dashboard/power/graph/graph.component.js @@ -0,0 +1,91 @@ +import React from 'react'; +import Templates from 'config/templates'; + +import House from './../../../models/house'; +import SplineStackChart from './../../../d3/line/spline_stack'; + +class GraphComponent extends React.Component { + + componentDidMount(){ + var power_graph = this, + house = power_graph.context.house; + power_graph.graph_title = ' '; + if (!power_graph.context.loading_power_data) power_graph.updateGraph(); + } + + componentDidUpdate(prev_props, prev_state, prev_context){ + var power_graph = this, + house = power_graph.context.house; + if (power_graph.context.loading_power_data) {return false;} + if (!prev_context.house || + prev_context.loading_power_data || + prev_context.house.id != power_graph.context.house.id) { + power_graph.updateGraph(); + } + } + + updateGraph(){ + var power_graph = this, + house = power_graph.context.house; + if (power_graph.graph === undefined){ + power_graph.graph = new SplineStackChart({ + container: '#power_graph', + outer_width: 800, + outer_height: 200, + color: '#0404B4', + time_series: true, + domain_attr: 'x', + range_attr: 'y', + include_dots: true, + titleizeDatum: (series, d)=>{ + return series.title + '
' + Math.round(d.y) + ' W
' + house.formatDate(d.power_graph_datum.data.time, 'MMM D [at] HH:mm'); + } + }); + jQuery('#power_graph').tooltip({ + selector: 'circle', + container: 'body', + html: true, + title: function(){ + return this.__data__.title; + } + }); + } + var net_power_graph = { + title: 'Net Power Consumption', + values: house.power_data.map((power_graph_datum)=>{ + return { + power_graph_datum: power_graph_datum, + x: power_graph_datum.time_to_date, + y: Math.max(0, power_graph_datum.data.consumption - power_graph_datum.data.production) } + }) + }, + savings = { + title: 'Power Production', + values: house.power_data.map((power_graph_datum)=>{ + return { + power_graph_datum: power_graph_datum, + x: power_graph_datum.time_to_date, + y: power_graph_datum.data.production } + }) + }; + power_graph.graph.drawData({ + title: power_graph.graph_title, + css_class: '', + series: [net_power_graph, savings] + }); + } + + render() { + var powerGraphRt = Templates.forComponent('power_graph'); + return powerGraphRt.call(this); + } + +} + +GraphComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + loading_power_data: React.PropTypes.bool.isRequired, + router: React.PropTypes.object.isRequired +}; + +export default GraphComponent; diff --git a/client/dashboard/power/graph/graph.rt b/client/dashboard/power/graph/graph.rt new file mode 100644 index 0000000..aef40b8 --- /dev/null +++ b/client/dashboard/power/graph/graph.rt @@ -0,0 +1 @@ +
diff --git a/client/dashboard/power/graph/graph.rt.js b/client/dashboard/power/graph/graph.rt.js new file mode 100644 index 0000000..230bc07 --- /dev/null +++ b/client/dashboard/power/graph/graph.rt.js @@ -0,0 +1,5 @@ +import React from 'react'; +import _ from 'lodash'; +export default function () { + return React.createElement('div', { 'id': 'power_graph' }); +}; \ No newline at end of file diff --git a/client/dashboard/power/power.component.js b/client/dashboard/power/power.component.js new file mode 100644 index 0000000..137d7da --- /dev/null +++ b/client/dashboard/power/power.component.js @@ -0,0 +1,120 @@ +import React from 'react'; +import moment from 'moment-timezone'; +import _ from 'lodash'; + +import Templates from 'config/templates'; +import House from './../../models/house'; +import DateRangeSlider from './../../d3/sliders/date_range'; +import {RouteHelper} from './../routes'; + +class PowerComponent extends React.Component { + + constructor(props){ + super(props); + var power = this; + power.state = { + loading_power_data: true }; + power.updates = 0; + } + + componentDidMount(){ + var power = this, + house = power.context.house; + power.renders = 0; + if (!house) return false; + power.initDateRange(); + house.setPowerData() + .then(()=>{ + power.setState({loading_power_data: false }); }); + } + + componentDidUpdate(prev_props, prev_state, prev_context){ + var power = this, + house = power.context.house; + if (!house) return false; + var route_helper = new RouteHelper(house, power.props); + if ((!prev_context.house || prev_context.house.data.id != power.context.house.data.id) || + !house.matchesPowerRange(prev_props.location.query['dates[]'] || [])) { + power.setState({loading_power_data: true}); + house.setPowerData() + .then(()=>{ + power.initDateRange(); + power.setState({loading_power_data: false}); // will update graph or table. + }); + } else if (!house.matchesMonthState(prev_props.params)) power.initDateRange(); + } + + initDateRange(){ + var power = this, + house = power.context.house; + if (power.date_range_slider === undefined){ + power.date_range_slider = new DateRangeSlider({ + container: '#power_date_setter', + outer_height: 100, + maxDelta: function(changed_date, other_date){ + if (Math.abs(changed_date.getTime() - other_date.getTime()) > 3600 * 24 * 4 * 1000){ + if (changed_date > other_date){ + return new Date(changed_date.getTime() - 3600 * 24 * 4 * 1000); + } else { + return new Date(changed_date.getTime() + 3600 * 24 * 4 * 1000); + } + } + return false; + } + }); + } + power.date_range_slider.onRangeUpdated = (min, max)=>{ + if (power.date_range_update) clearTimeout(power.date_range_update); + power.date_range_update = setTimeout(()=>{ + var power_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)], + route_helper = new RouteHelper(house, power.props, {power_range: power_range}); + + route_helper.updateHouseState(); + power.context.router.push(route_helper.newRoute()); + }, 500); + }; + power.date_range_slider.drawData({ + abs_min: house.state.current_month_moment.toDate(), + abs_max: house.state.end_of_current_data_moment.toDate(), + current_min: house.toDate(house.state.power_range[0]), + current_max: house.toDate(house.state.power_range[1]) + }); + } + + setParam(event){ + var power = this, + house = power.context.house, + param = event.target.dataset.param, + value = event.target.dataset.value, + update = {}, route_helper; + update[param] = value; + route_helper = new RouteHelper(house, power.props, update); + if (route_helper.routeUpdated()){ + route_helper.updateHouseState(); + power.context.router.push(route_helper.newRoute()); + } + } + + getChildContext(){ + var layout = this; + return { + loading_power_data: layout.state.loading_power_data + }; + } + + render() { + var powerRt = Templates.forComponent('power'); + return powerRt.call(this); + } +} + +PowerComponent.childContextTypes = { + loading_power_data: React.PropTypes.bool.isRequired +}; + +PowerComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + router: React.PropTypes.object.isRequired +}; + +export default PowerComponent; diff --git a/client/dashboard/power/power.js b/client/dashboard/power/power.js deleted file mode 100644 index c173f54..0000000 --- a/client/dashboard/power/power.js +++ /dev/null @@ -1,195 +0,0 @@ -import React from 'react'; -import moment from 'moment-timezone'; -import _ from 'lodash'; - -import Templates from 'config/templates'; -import House from './../../models/house'; -import SplineStackChart from './../../d3/line/spline_stack'; -import DateRangeSlider from './../../d3/sliders/date_range'; - -var Power = React.createClass({ - - getInitialState: function(){ - var power = this; - return { - loading_data: true - }; - }, - - handleResize: function(e) { - this.setState({windowWidth: window.innerWidth}); - }, - - componentDidMount: function() { - // window.addEventListener('resize', this.handleResize); - var power = this, - house = power.props.house; - power.graph_title = ''; - power.initDateRange(); - house.setPowerData().then(()=>{ - power.setState({loading_data: false}); - if (power.props.view === 'graph'){ - power.initGraph(); - } - }); - }, - - componentWillReceiveProps: function(new_props){ - var power = this; - if (new_props.house !== power.props.house){ - // house will change. - power.setState({loading_data: true}); - new_props.house.setPowerData().then(()=>{ - power.setState({loading_data: false}); - if (power.props.view === 'graph'){ - power.initGraph(); - } - }); - } - // view will change from graph to table. - if (new_props.view !== 'graph' && power.props.view === 'graph') power.destroyGraph(); - }, - - componentDidUpdate: function(prev_props, _prev_state){ - var power = this, - house = power.props.house; - // view has changed from graph to table. - if (prev_props.view !== 'graph' && power.props.view === 'graph'){ - power.initGraph(); - } - if (prev_props.house !== house) power.initDateRange(); - var need_update = false; - if (prev_props.year !== power.props.year){ - power.updateCurrentMonth(); - } - }, - - initGraph: function(){ - var power = this, - house = power.props.house; - if (power.graph === undefined){ - power.graph = new SplineStackChart({ - container: '#power_graph', - outer_width: 800, - outer_height: 200, - color: '#0404B4', - time_series: true, - domain_attr: 'x', - range_attr: 'y', - include_dots: true, - titleizeDatum: (series, d)=>{ - return series.title + '
' + Math.round(d.y) + ' W
' + house.formatDate(d.power_datum.data.time, 'MMM D [at] HH:mm'); - } - }); - jQuery('#power_graph').tooltip({ - selector: 'circle', - container: 'body', - html: true, - title: function(){ - return this.__data__.title; - } - }); - } - power.updateGraph(); - }, - - updateGraph: function(){ - var power = this, - house = power.props.house, - net_power = { - title: 'Net Power Consumption', - values: house.power_data.map((power_datum)=>{ - return { - power_datum: power_datum, - x: power_datum.time_to_date, - y: Math.max(0, power_datum.data.consumption - power_datum.data.production) } - }) - }, - savings = { - title: 'Power Production', - values: house.power_data.map((power_datum)=>{ - return { - power_datum: power_datum, - x: power_datum.time_to_date, - y: power_datum.data.production } - }) - }; - power.graph.drawData({ - title: power.graph_title, - css_class: '', - series: [net_power, savings] - }); - }, - - initDateRange: function(){ - var power = this, - house = power.props.house; - if (power.date_range_slider === undefined){ - power.date_range_slider = new DateRangeSlider({ - container: '#power_date_setter', - outer_height: 100, - maxDelta: function(changed_date, other_date){ - if (Math.abs(changed_date.getTime() - other_date.getTime()) > 3600 * 24 * 4 * 1000){ - if (changed_date > other_date){ - return new Date(changed_date.getTime() - 3600 * 24 * 4 * 1000); - } else { - return new Date(changed_date.getTime() + 3600 * 24 * 4 * 1000); - } - } - return false; - } - }); - } - power.date_range_slider.onRangeUpdated = (min, max)=>{ - if (power.date_range_update) clearTimeout(power.date_range_update) - power.date_range_update = setTimeout(()=>{ - house.power_date_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)] - house.setPowerData() - .then(()=>{ - if (power.props.view === 'graph') power.updateGraph(); - else power.forceUpdate(); - }); - }, 500); - }; - power.date_range_slider.drawData({ - abs_min: house.current_month_moment.toDate(), - abs_max: house.end_of_current_data_moment.toDate(), - current_min: house.toDate(house.power_date_range[0]), - current_max: house.toDate(house.power_date_range[1]) - }); - }, - - destroyGraph: function(){ - var power = this; - document.getElementById('power_graph').innerHTML = ''; - power.graph = undefined; - }, - - setMonth: function(event){ - var power = this, - house = power.props.house, - month = event.target.dataset.value; - if (month !== house.current_month){ - var need_update = house.setMonth(month); - if (need_update) power.updateCurrentMonth(); - } - }, - - updateCurrentMonth: function(){ - var power = this, - house = power.props.house; - power.initDateRange(); - house.setPowerData() - .then(()=>{ - power.forceUpdate(); - if (power.props.view === 'graph') power.updateGraph(); - }); - }, - - render: function() { - var powerRt = Templates.forComponent('power'); - return powerRt.call(this); - } -}); - -export default Power; diff --git a/client/dashboard/power/power.rt b/client/dashboard/power/power.rt index 34d9efc..d18a796 100644 --- a/client/dashboard/power/power.rt +++ b/client/dashboard/power/power.rt @@ -1,35 +1,18 @@
+ rt-class="{active: month === this.context.house.state.month}" + onClick="{this.setParam.bind(this)}">{month}
-
- Retrieving power data for the {this.props.house.name} household... +
+ Retrieving power data...
- - - - - - - - - - - - - - - - - -
TimeConsumption (W)Production (W)
{power_datum.time_to_s}{power_datum.consumption_to_s}{power_datum.production_to_s}
-
+ {this.props.children}
diff --git a/client/dashboard/power/power.rt.js b/client/dashboard/power/power.rt.js index 1f8d93b..7a49c0c 100644 --- a/client/dashboard/power/power.rt.js +++ b/client/dashboard/power/power.rt.js @@ -2,26 +2,17 @@ import React from 'react'; import _ from 'lodash'; function repeatMonth1(month, monthIndex) { return React.createElement('button', { + 'data-param': 'month', 'data-value': month, 'key': 'data-month-' + month, - 'className': 'btn-warning btn btn-sm' + ' ' + _.keys(_.pick({ active: month === this.props.house.current_month }, _.identity)).join(' '), - 'onClick': this.setMonth + 'className': 'btn-warning btn btn-sm' + ' ' + _.keys(_.pick({ active: month === this.context.house.state.month }, _.identity)).join(' '), + 'onClick': this.setParam.bind(this) }, month); } -function repeatPower_datum2(power_datum, power_datumIndex) { - return React.createElement('tr', { - 'className': 'fuck-you', - 'key': power_datum.scoped_id - }, React.createElement('td', {}, power_datum.data.id), React.createElement('td', {}, power_datum.time_to_s), React.createElement('td', {}, power_datum.consumption_to_s), React.createElement('td', {}, power_datum.production_to_s)); -} export default function () { return React.createElement('div', { 'id': 'power_view' }, React.createElement.apply(this, [ 'div', { 'className': 'btn-group' }, - this.props.house ? _.map(this.props.house.availableMonths(), repeatMonth1.bind(this)) : null - ]), this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data for the ', this.props.house.name, ' household...\n ') : null, React.createElement('div', { 'id': 'power_date_setter' }), this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Time'), React.createElement('th', {}, 'Consumption (W)'), React.createElement('th', {}, 'Production (W)'))), React.createElement.apply(this, [ - 'tbody', - {}, - _.map(this.props.house.power_data, repeatPower_datum2.bind(this)) - ])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'power_graph' }) : null); + this.context.house ? _.map(this.context.house.availableMonths(), repeatMonth1.bind(this)) : null + ]), this.state.loading_power_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data...\n ') : null, React.createElement('div', { 'id': 'power_date_setter' }), '\n ', this.props.children, '\n'); }; \ No newline at end of file diff --git a/client/dashboard/power/table/table.component.js b/client/dashboard/power/table/table.component.js new file mode 100644 index 0000000..51471b2 --- /dev/null +++ b/client/dashboard/power/table/table.component.js @@ -0,0 +1,19 @@ +import React from 'react'; +import Templates from 'config/templates'; + +import House from './../../../models/house'; + +class TableComponent extends React.Component { + + render() { + var powerTableRt = Templates.forComponent('power_table'); + return powerTableRt.call(this); + } +} + +TableComponent.contextTypes = { + house: React.PropTypes.instanceOf(House), + router: React.PropTypes.object.isRequired +}; + +export default TableComponent; diff --git a/client/dashboard/power/table/table.rt b/client/dashboard/power/table/table.rt new file mode 100644 index 0000000..86e1f85 --- /dev/null +++ b/client/dashboard/power/table/table.rt @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +
TimeConsumption (W)Production (W)
{power_datum.time_to_s}{power_datum.consumption_to_s}{power_datum.production_to_s}
diff --git a/client/dashboard/power/table/table.rt.js b/client/dashboard/power/table/table.rt.js new file mode 100644 index 0000000..00d8916 --- /dev/null +++ b/client/dashboard/power/table/table.rt.js @@ -0,0 +1,12 @@ +import React from 'react'; +import _ from 'lodash'; +function repeatPower_datum1(power_datum, power_datumIndex) { + return React.createElement('tr', { 'key': power_datum.scoped_id }, React.createElement('td', {}), React.createElement('td', {}, power_datum.time_to_s), React.createElement('td', {}, power_datum.consumption_to_s), React.createElement('td', {}, power_datum.production_to_s)); +} +export default function () { + return this.context.house ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Time'), React.createElement('th', {}, 'Consumption (W)'), React.createElement('th', {}, 'Production (W)'))), React.createElement.apply(this, [ + 'tbody', + {}, + _.map(this.context.house.power_data, repeatPower_datum1.bind(this)) + ])) : null; +}; \ No newline at end of file diff --git a/client/dashboard/routes.js b/client/dashboard/routes.js new file mode 100644 index 0000000..a44be41 --- /dev/null +++ b/client/dashboard/routes.js @@ -0,0 +1,166 @@ +import House from './house/house.component' +import Power from './power/power.component'; +import PowerGraph from './power/graph/graph.component'; +import PowerTable from './power/table/table.component'; +import Energy from './energy/energy.component'; +import EnergyGraph from './energy/graph/graph.component'; +import EnergyTable from './energy/table/table.component'; +import About from './about/about.component'; +import Layout from './layout/layout.component'; +import ArrayUtil from './../../shared/utils/array'; + +const POWER_ROUTES = { + path: 'power', + component: Power, + childRoutes: [ + {path: ':month/:year', component: PowerGraph}, + {path: ':month/:year/graph', component: PowerGraph}, + {path: ':month/:year/table', component: PowerTable} + ] +}; + +const ENERGY_ROUTES = { + path: 'energy', + component: Energy, + childRoutes: [ + {path: ':year/:graph_attr', component: EnergyGraph}, + {path: ':year/:graph_attr/graph', component: EnergyGraph}, + {path: ':year/:graph_attr/table', component: EnergyTable} + ] +}; + +export const ROUTES = [{ + path: '/', + component: Layout, + indexRoute: { component: About }, + childRoutes: [{ + path: 'houses/:house_id', + component: House, + childRoutes: [ENERGY_ROUTES, POWER_ROUTES] + }] +}]; + +export class RouteHelper { + + constructor(house, props, update){ + var route_helper = this; + route_helper.house = house; + route_helper.props = props; + route_helper.update = update || {}; + } + + get view(){ + var route_helper = this; + return route_helper.update.view || (route_helper.tableSelected() ? 'table' : 'graph'); + } + + get graph_attr(){ + var route_helper = this; + return route_helper.update.graph_attr || route_helper.props.params.graph_attr || 'consumption'; + } + + get dataset(){ + var route_helper = this; + return route_helper.update.dataset || (route_helper.energySelected() ? 'energy' : 'power'); + } + + get power_range(){ + var route_helper = this; + return route_helper.update.power_range || route_helper.props.location.query['dates[]']; + } + + get date_params(){ + var route_helper = this; + return { + month: route_helper.update.month || route_helper.house.month, + year: route_helper.update.year || route_helper.house.year }; + } + + routeUpdated(){ + var route_helper = this, + house = route_helper.house; + return (route_helper.energySelected() && !house.matchesYearState(route_helper.date_params)) || + (route_helper.powerSelected() && !house.matchesMonthState(route_helper.date_params) || !house.matchesPowerRange(route_helper.power_range)); + } + + // This will update the house state acccording to passed update parameters. + updateHouseState(){ + var route_helper = this, + house = route_helper.house; + house.setMonthState(route_helper.date_params, route_helper.update.power_range); + } + + paramsHaveDateState(){ + var route_helper = this; + return !!route_helper.props.params.year; + } + + // This will update the house according to URL parameters. + updateHouseToParams(){ + var route_helper = this, + house = route_helper.house, + power_range; + if (route_helper.props.location.query['dates[]']){ + power_range = []; + power_range[0] = +route_helper.props.location.query['dates[]'][0]; + power_range[1] = +route_helper.props.location.query['dates[]'][1]; + } + house.setMonthState(route_helper.props.params, power_range); + } + + // should be run AFTER updateHouseState is called. + newRoute(){ + var route_helper = this, + house = route_helper.house; + if (route_helper.dataset === 'energy'){ + return `/houses/${house.data.id}/energy/${house.state.year}/${route_helper.graph_attr}/${route_helper.view}`; + } else { + return `/houses/${house.data.id}/power/${house.state.month}/${house.state.year}/${route_helper.view}?${jQuery.param({dates: house.state.power_range})}`; + } + } + + graphSelected(){ + return RouteHelper.graphSelected(this.props.routes); + } + + static graphSelected(routes){ + if (RouteHelper.energySelected(routes)){ + return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyGraph; }); + } else if (RouteHelper.powerSelected(routes)){ + return ArrayUtil.any(routes, (route)=>{ return route.component === PowerGraph; }); + } + return false; + } + + tableSelected(){ + return RouteHelper.tableSelected(this.props.routes); + } + + static tableSelected(routes){ + if (RouteHelper.energySelected(routes)){ + return ArrayUtil.any(routes, (route)=>{ return route.component === EnergyTable; }); + } else if (RouteHelper.powerSelected(routes)){ + return ArrayUtil.any(routes, (route)=>{ return route.component === PowerTable; }); + } + return false; + } + + energySelected(){ + return RouteHelper.energySelected(this.props.routes); + } + + static energySelected(routes){ + return ArrayUtil.any(routes, (route)=>{ return route.component === Energy; }); + } + + powerSelected(){ + return RouteHelper.powerSelected(this.props.routes); + } + + static powerSelected(routes){ + return ArrayUtil.any(routes, (route)=>{ return route.component === Power; }); + } + + +} + diff --git a/client/models/house.js b/client/models/house.js index e65ccfa..ee9b589 100644 --- a/client/models/house.js +++ b/client/models/house.js @@ -20,6 +20,7 @@ class House { constructor(data){ var house = this; house.data = data; + house.state = {}; Object.assign(house, Databasable); var n_years = house.data_until_moment.year() - house.data_from_moment.year() + 1; @@ -27,9 +28,10 @@ class House { for (var year=house.data_from_moment.year(); year<=house.data_until_moment.year(); year+=1){ house.years.push(year); } - house.current_month = house.data_until_moment.format('MMM'); - house.current_year = house.data_until_moment.year(); - house.setCurrentMonthMoment(); + house.setMonthState({ + month: house.data_until_moment.format('MMM'), + year: house.data_until_moment.year() + }); } get data_from_moment(){ @@ -42,20 +44,19 @@ class House { return moment.tz(house.data.data_until * 1000, house.data.timezone); } - get end_of_current_data_moment(){ - var house = this, - end_of_month = house.current_month_moment.clone().endOf('month'); - return end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month; - } - get scoped_id(){ return `house-${this.data.id}`; } + get select_props(){ + if (this.selected) return {selected: true}; + else return {}; + } + availableMonths(){ var house = this, all_months = moment.monthsShort(), - year = house.current_year.toString(); + year = house.state.year.toString(); if ((year) === house.data_from_moment.format('YYYY')){ return all_months.slice(house.data_from_moment.month(), 12); } else if (year === house.data_until_moment.format('YYYY')){ @@ -65,29 +66,76 @@ class House { } } - setYear(year){ - var house = this; - house.current_year = year; - return house.setCurrentMonthMoment(); - } - - setMonth(month){ - var house = this; - house.current_month = month; - return house.setCurrentMonthMoment(); - } - - setCurrentMonthMoment(){ + setMonthState(params, power_ranges){ var house = this, - month_i = moment.monthsShort().indexOf(house.current_month), - new_month_moment = moment.tz({year: house.current_year, month: month_i, day: 1}, house.data.timezone).startOf('month'); - if (!house.current_month_moment || new_month_moment.unix() !== house.current_month_moment.unix()){ - house.current_month_moment = new_month_moment; - house.power_date_range = [house.end_of_current_data_moment.clone().subtract(4, 'days').unix(), house.end_of_current_data_moment.unix()]; - house.energy_date_range = [house.end_of_current_data_moment.clone().startOf('year').unix(), house.end_of_current_data_moment.clone().endOf('year').unix()] - return true; + all_months = moment.monthsShort(); + + if (house.state.month !== params.month || house.state.year != params.year){ + var new_year = +params.year; + if (new_year >= house.data_from_moment.year() && new_year <= house.data_until_moment.year()){ + house.state.year = params.year; + } else if (!house.state.year){ + house.state.year = house.years[house.years.length - 1]; + } + var available_months = house.availableMonths(); + if (available_months.indexOf(params.month) >= 0){ + house.state.month = params.month; + } else if (!house.state.month || available_months.indexOf(house.state.month) < 0){ + house.state.month = available_months[available_months.length - 1]; + } } - return false; + + var month_i = all_months.indexOf(house.state.month), + new_month_moment = moment.tz({year: house.state.year, month: month_i, day: 1}, house.data.timezone).startOf('month'); + if (!house.state.current_month_moment || new_month_moment.unix() !== house.state.current_month_moment.unix()){ + house.state.current_month_moment = new_month_moment; + } + house.setDataRanges(power_ranges); + } + + setDataRanges(power_ranges){ + var house = this, + end_of_month = house.state.current_month_moment.clone().endOf('month'), + end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month, + energy_max = Math.min(end_of_current_data_moment.clone().endOf('year').unix(), house.data.data_until); + house.state.energy_range = [end_of_current_data_moment.clone().startOf('year').unix(), energy_max]; + house.state.power_range = house.state.power_range || []; + house.state.end_of_current_data_moment = end_of_current_data_moment; + + var current_data_range = [house.state.current_month_moment.unix(), end_of_current_data_moment.unix()], + power_min = house.state.power_range[0], + power_max = house.state.power_range[1]; + if (power_ranges){ + if (DateRange.inRange(power_ranges[1], current_data_range)){ + power_max = power_ranges[1]; + } + if (DateRange.inRange(power_ranges[0], current_data_range) && power_ranges[0] < power_max){ + power_min = power_ranges[0]; + } + } + if (!power_max || !DateRange.inRange(power_max, current_data_range)){ + power_max = end_of_current_data_moment.unix(); + } + if (!power_min || !DateRange.inRange(power_min, current_data_range) || + power_max - power_min > 3600 * 24 * 4){ + power_min = power_max - 3600 * 24 * 4; + } + house.state.power_range = [power_min, power_max]; + } + + matchesYearState(params){ + var house = this; + return params.year == house.state.year; + } + + matchesMonthState(params){ + var house = this; + return params.month == house.state.month && params.year == house.state.year; + } + + matchesPowerRange(dates){ + var house = this; + return house.state.power_range[0] == dates[0] && house.state.power_range[1] == dates[1]; } offset_diff(unix){ @@ -121,7 +169,7 @@ class House { .then((power_collection)=>{ return house.ensurePowerData() .then(()=>{ - var params = house.rangeToLokiParams('time', house.power_date_range); + var params = house.rangeToLokiParams('time', house.state.power_range); house.power_data = power_collection.find(params) .sort((pd1, pd2)=>{ if (pd1.time === pd2.time) return 0; @@ -137,7 +185,7 @@ class House { var house = this, query_ranges; - query_ranges = DateRange.addRange(house.power_date_range, house.data.power_datum_ranges || []); + query_ranges = DateRange.addRange(house.state.power_range, house.data.power_datum_ranges || []); if (query_ranges.gaps_filled.length > 0){ var params = {dates: query_ranges.gaps_filled}; return house.getPowerData(params) @@ -167,7 +215,7 @@ class House { .then((energy_collection)=>{ return house.ensureEnergyData() .then(()=>{ - var params = house.rangeToLokiParams('day', house.energy_date_range); + var params = house.rangeToLokiParams('day', house.state.energy_range); house.energy_data = energy_collection.find(params) .sort((pd1, pd2)=>{ if (pd1.day === pd2.day) return 0; @@ -181,7 +229,7 @@ class House { ensureEnergyData(){ var house = this, - query_ranges = DateRange.addRange(house.energy_date_range, house.data.energy_datum_ranges || []); + query_ranges = DateRange.addRange(house.state.energy_range, house.data.energy_datum_ranges || []); if (query_ranges.gaps_filled.length > 0){ return house.getEnergyData({dates: query_ranges.gaps_filled}) .then(()=>{ diff --git a/npm-debug.log b/npm-debug.log deleted file mode 100644 index f818a33..0000000 --- a/npm-debug.log +++ /dev/null @@ -1,45 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/usr/local/bin/node', '/usr/local/bin/npm', 'start' ] -2 info using npm@3.5.3 -3 info using node@v5.4.1 -4 verbose run-script [ 'prestart', 'start', 'poststart' ] -5 info lifecycle spike_proto@0.0.0~prestart: spike_proto@0.0.0 -6 silly lifecycle spike_proto@0.0.0~prestart: no script for prestart, continuing -7 info lifecycle spike_proto@0.0.0~start: spike_proto@0.0.0 -8 verbose lifecycle spike_proto@0.0.0~start: unsafe-perm in lifecycle true -9 verbose lifecycle spike_proto@0.0.0~start: PATH: /usr/local/lib/node_modules/npm/bin/node-gyp-bin:/home/eric/Code/spike2/node_modules/.bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@oroeco_dev/bin:/home/eric/.rvm/gems/ruby-1.9.3-p484@global/bin:/home/eric/.rvm/rubies/ruby-1.9.3-p484/bin:/home/eric/.rvm/bin:/home/eric/bin:/usr/local/heroku/bin:/home/eric/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/opt/lampp/bin -10 verbose lifecycle spike_proto@0.0.0~start: CWD: /home/eric/Code/spike2 -11 silly lifecycle spike_proto@0.0.0~start: Args: [ '-c', 'babel-node ./server/app.express.js' ] -12 silly lifecycle spike_proto@0.0.0~start: Returned: code: 1 signal: null -13 info lifecycle spike_proto@0.0.0~start: Failed to exec start script -14 verbose stack Error: spike_proto@0.0.0 start: `babel-node ./server/app.express.js` -14 verbose stack Exit status 1 -14 verbose stack at EventEmitter. (/usr/local/lib/node_modules/npm/lib/utils/lifecycle.js:232:16) -14 verbose stack at emitTwo (events.js:87:13) -14 verbose stack at EventEmitter.emit (events.js:172:7) -14 verbose stack at ChildProcess. (/usr/local/lib/node_modules/npm/lib/utils/spawn.js:24:14) -14 verbose stack at emitTwo (events.js:87:13) -14 verbose stack at ChildProcess.emit (events.js:172:7) -14 verbose stack at maybeClose (internal/child_process.js:821:16) -14 verbose stack at Process.ChildProcess._handle.onexit (internal/child_process.js:211:5) -15 verbose pkgid spike_proto@0.0.0 -16 verbose cwd /home/eric/Code/spike2 -17 error Linux 3.19.0-49-generic -18 error argv "/usr/local/bin/node" "/usr/local/bin/npm" "start" -19 error node v5.4.1 -20 error npm v3.5.3 -21 error code ELIFECYCLE -22 error spike_proto@0.0.0 start: `babel-node ./server/app.express.js` -22 error Exit status 1 -23 error Failed at the spike_proto@0.0.0 start script 'babel-node ./server/app.express.js'. -23 error Make sure you have the latest version of node.js and npm installed. -23 error If you do, this is most likely a problem with the spike_proto package, -23 error not with npm itself. -23 error Tell the author that this fails on your system: -23 error babel-node ./server/app.express.js -23 error You can get information on how to open an issue for this project with: -23 error npm bugs spike_proto -23 error Or if that isn't available, you can get their info via: -23 error npm owner ls spike_proto -23 error There is likely additional logging output above. -24 verbose exit [ 1, true ] diff --git a/package.json b/package.json index f9c3a44..1eb594a 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "express": "4.13.3", "react": "0.14.3", "react-dom": "0.14.3", + "react-router": "2.0.0", "webpack": "1.12.9", "webpack-dev-server": "1.14.0", "extract-text-webpack-plugin": "1.0.1", diff --git a/readme.md b/readme.md index 8fcb048..cca98b3 100644 --- a/readme.md +++ b/readme.md @@ -55,7 +55,7 @@ npm start When developing, it can be really useful to clear all of the data saved to local client storage. A better alternative to clearing all data in your browser cache, hit the 'Refresh Data' button on the main page below the houses dropdown. -## Building the designer pack +## Building the design pack To build a design pack, you first need to install [sass.js](https://github.com/medialize/sass.js/) in the design build directory so the design build can compile the sass in the browser. @@ -64,7 +64,7 @@ cd client/build/design git clone https://github.com/medialize/sass.js.git ``` -Then to build the app, you'll need to build the app with webpack: +Then build the app with webpack: ```sh gulp build --design @@ -80,7 +80,7 @@ python -m SimpleHTTPServer 8000 python3 -m http.server ``` -Access the app at localhost:8000. The app will run slow because json responses are not as finely paginated and both React templates and sass are compiled in the browser before the app runs. +Access the app at localhost:8000. The app will run slowly because json responses are not as finely paginated and both React templates and sass are compiled in the browser before the app runs. The designer can change React templates and sass files in `/dashboard`. Refresh the page to see the changes reflected. They can then share this directory with us so we can update the files in this repository. diff --git a/server/app.express.js b/server/app.express.js index 421fa53..47f6fe7 100644 --- a/server/app.express.js +++ b/server/app.express.js @@ -29,7 +29,6 @@ DB.sync().then(()=>{ api.use(bodyParser.json()); api.use(bodyParser.urlencoded({ extended: false })); - api.listen(API_PORT, () => { console.log(`API is now running on http://localhost:${API_PORT}`); }); @@ -74,7 +73,7 @@ app.use(assets({ paths: ["./../node_modules"], build: true, buildDir: false, - //compile: false, + // compile: false, compress: true })); // serve public static files. @@ -83,44 +82,10 @@ dev_server.app.use('/', express.static(path.resolve(__dirname, 'public'))); // view engine set up app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'jade'); -app.get("/", (req, res, next)=>{ +app.get("*", (req, res, next)=>{ res.render("index"); }); - -/* - * Handle Errors - */ - -// catch 404 and forward to error handler -app.use(function(req, res, next) { - var err = new Error('Not Found'); - err.status = 404; - next(err); -}); - -// development error handler -// will print stacktrace -if (app.get('env') === 'development') { - app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: err - }); - }); -} - -// production error handler -// no stacktraces leaked to user -app.use(function(err, req, res, next) { - res.status(err.status || 500); - res.render('error', { - message: err.message, - error: {} - }); -}); - dev_server.listen(APP_PORT, () => { console.log(`App is now running on http://localhost:${APP_PORT}`); }); diff --git a/shared/utils/date_range.js b/shared/utils/date_range.js index 04be8e1..b56b7ed 100644 --- a/shared/utils/date_range.js +++ b/shared/utils/date_range.js @@ -64,5 +64,11 @@ class DateRange { return new Date(date.getTime() + s); } + static inRange(n, min_max){ + var min = min_max[0], + max = min_max[1]; + return ((n >= min) && (n <= max)); + } + } export default DateRange; diff --git a/shared/utils/math.js b/shared/utils/math.js index b626ed1..6607ba7 100644 --- a/shared/utils/math.js +++ b/shared/utils/math.js @@ -29,10 +29,4 @@ export default class { return minus; } - static inRange(n, min_max){ - var min = min_max[0], - max = min_max[1]; - return ((n >= min) && (n <= max)); - } - } diff --git a/spec/client/dashboard/models/house.test.js b/spec/client/dashboard/models/house.test.js new file mode 100644 index 0000000..133049a --- /dev/null +++ b/spec/client/dashboard/models/house.test.js @@ -0,0 +1,85 @@ +"use strict"; + +import moment from 'moment-timezone'; +import House from './../../../../client/models/house.js'; + + +describe('house#setMonthState', ()=>{ + + var data_until = 1456589922, // Sat, 27 Feb 2016 16:18:42 +0000 + house = new House({ + id: 1, + name: 'Johnson', + data_from: data_until - 3600 * 24 * 365 * 3, + data_until: data_until, + timezone: 'America/New_York' + }); + + it('is updated properly on init', ()=>{ + var current_month_moment = moment.tz({year: 2016, month: 1, day: 1}, 'America/New_York'), + energy_min = moment.tz({year: 2016, month: 0, day: 1}, 'America/New_York').unix(), + energy_max = data_until, + power_min = data_until - 3600 * 24 * 4, + power_max = data_until; + + expect(house.state.month).toEqual('Feb'); + expect(house.state.year).toEqual(2016); + expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix()); + expect(house.state.energy_range).toEqual([energy_min, energy_max]); + expect(house.state.power_range).toEqual([power_min, power_max]); + }); + + it('is not updated when passed no params', ()=>{ + var current_month_moment = moment.tz({year: 2016, month: 1, day: 1}, 'America/New_York'), + energy_min = moment.tz({year: 2016, month: 0, day: 1}, 'America/New_York').unix(), + energy_max = data_until, + power_min = data_until - 3600 * 24 * 4, + power_max = data_until; + + house.setMonthState({}); + expect(house.state.month).toEqual('Feb'); + expect(house.state.year).toEqual(2016); + expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix()); + expect(house.state.energy_range).toEqual([energy_min, energy_max]); + expect(house.state.power_range).toEqual([power_min, power_max]); + }); + + it('is updated properly when passed power params', ()=>{ + var current_month_moment = moment.tz({year: 2015, month: 2, day: 1}, 'America/New_York'), + energy_min = moment.tz({year: 2015, month: 0, day: 1}, 'America/New_York').unix(), + energy_max = moment.tz({year: 2015, month: 0, day: 1}, 'America/New_York').endOf('year').unix(), + power_max = current_month_moment.clone().endOf('month').subtract(3, 'days').unix(), + power_min = current_month_moment.clone().endOf('month').subtract(6, 'days').unix() + + house.setMonthState({ + month: 'Mar', + year: 2015 + }, [ power_min, power_max ]); + + expect(house.state.month).toEqual('Mar'); + expect(house.state.year).toEqual(2015); + expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix()); + expect(house.state.energy_range).toEqual([energy_min, energy_max]); + expect(house.state.power_range).toEqual([power_min, power_max]); + }); + + it('is updated properly when passed energy params', ()=>{ + var current_month_moment = moment.tz({year: 2014, month: 9, day: 1}, 'America/New_York'), + energy_min = moment.tz({year: 2014, month: 0, day: 1}, 'America/New_York').unix(), + energy_max = moment.tz({year: 2014, month: 0, day: 1}, 'America/New_York').endOf('year').unix(), + power_max = moment.tz({year: 2014, month: 9, day: 1}, 'America/New_York').endOf('month').unix(), + power_min = power_max - 3600 * 24 * 4; + + house.setMonthState({ + month: 'Oct', + year: 2014 + }); + + expect(house.state.month).toEqual('Oct'); + expect(house.state.year).toEqual(2014); + expect(house.state.current_month_moment.unix()).toEqual(current_month_moment.unix()); + expect(house.state.energy_range).toEqual([energy_min, energy_max]); + expect(house.state.power_range).toEqual([power_min, power_max]); + }); + +});