From 782f5cbf910acf31064ce15bcc5a69c934afaebe Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Fri, 11 Mar 2016 17:38:03 -0600 Subject: [PATCH] create irradiance composite graph --- client/api/development/energy_data.js | 6 +- client/config/development/templates.js | 6 + client/dashboard/energy/energy.rt | 3 - .../dashboard/energy/graph/graph.component.js | 1 + client/dashboard/energy/table/table.rt | 6 +- .../irradiance/graph/graph.component.js | 152 +++++++++++++ client/dashboard/irradiance/graph/graph.rt | 1 + .../irradiance/irradiance.component.js | 18 ++ client/dashboard/irradiance/irradiance.rt | 13 ++ client/dashboard/irradiance/irradiance.scss | 3 + .../irradiance/table/table.component.js | 18 ++ client/dashboard/irradiance/table/table.rt | 18 ++ client/dashboard/layout/layout.component.js | 75 ++++++- client/dashboard/layout/layout.rt | 51 ++++- .../dashboard/power/graph/graph.component.js | 4 +- client/dashboard/power/power.component.js | 65 ------ client/dashboard/power/power.rt | 19 +- client/dashboard/state_manager.js | 205 +++++++++++++----- client/lib/databasable.js | 24 +- client/models/energy_datum.js | 49 ++++- client/models/house.js | 147 ++++++------- gulpfile.babel.js | 22 +- karma.conf.js | 3 + server/app.express.js | 6 +- server/controllers/energy_controller.js | 11 +- server/models/energy_datum.js | 25 ++- shared/utils/date_range.js | 4 +- spec/client/dashboard/models/house.test.js | 85 -------- spec/client/lib/databasable.test.js | 65 ++++++ spec/client/models/house.test.js | 76 +++++++ spec/shared/utils/date_range.test.js | 10 + spec/support/jasmine.json | 12 - 32 files changed, 824 insertions(+), 379 deletions(-) create mode 100644 client/dashboard/irradiance/graph/graph.component.js create mode 100644 client/dashboard/irradiance/graph/graph.rt create mode 100644 client/dashboard/irradiance/irradiance.component.js create mode 100644 client/dashboard/irradiance/irradiance.rt create mode 100644 client/dashboard/irradiance/irradiance.scss create mode 100644 client/dashboard/irradiance/table/table.component.js create mode 100644 client/dashboard/irradiance/table/table.rt delete mode 100644 spec/client/dashboard/models/house.test.js create mode 100644 spec/client/lib/databasable.test.js create mode 100644 spec/client/models/house.test.js delete mode 100644 spec/support/jasmine.json diff --git a/client/api/development/energy_data.js b/client/api/development/energy_data.js index 2c0706c..a08bd56 100644 --- a/client/api/development/energy_data.js +++ b/client/api/development/energy_data.js @@ -6,8 +6,10 @@ class EnergyDataApi { static index(params){ return jQuery.ajax({ - url: ENDPOINT + '?' + jQuery.param(params), - type: 'GET', + url: ENDPOINT, + data: JSON.stringify(params), + contentType: 'application/json', + type: 'POST', dataType: 'json' }).then((res)=>{ return res.data; diff --git a/client/config/development/templates.js b/client/config/development/templates.js index 4f6ce97..41a3049 100644 --- a/client/config/development/templates.js +++ b/client/config/development/templates.js @@ -7,6 +7,9 @@ import layoutRt from './../../dashboard/layout/layout.rt'; import energyRt from './../../dashboard/energy/energy.rt'; import energyGraphRt from './../../dashboard/energy/graph/graph.rt'; import energyTableRt from './../../dashboard/energy/table/table.rt'; +import irradianceRt from './../../dashboard/irradiance/irradiance.rt'; +import irradianceGraphRt from './../../dashboard/irradiance/graph/graph.rt'; +import irradianceTableRt from './../../dashboard/irradiance/table/table.rt'; import powerRt from './../../dashboard/power/power.rt'; import powerGraphRt from './../../dashboard/power/graph/graph.rt'; import powerTableRt from './../../dashboard/power/table/table.rt'; @@ -16,6 +19,9 @@ const TEMPLATES = { energy: energyRt, energy_graph: energyGraphRt, energy_table: energyTableRt, + irradiance: irradianceRt, + irradiance_graph: irradianceGraphRt, + irradiance_table: irradianceTableRt, power: powerRt, power_graph: powerGraphRt, power_table: powerTableRt diff --git a/client/dashboard/energy/energy.rt b/client/dashboard/energy/energy.rt index cced39f..2bc8148 100644 --- a/client/dashboard/energy/energy.rt +++ b/client/dashboard/energy/energy.rt @@ -1,9 +1,6 @@
-
- Retrieving energy data... -

Select Data

diff --git a/client/dashboard/energy/graph/graph.component.js b/client/dashboard/energy/graph/graph.component.js index 7a76d79..2805230 100644 --- a/client/dashboard/energy/graph/graph.component.js +++ b/client/dashboard/energy/graph/graph.component.js @@ -1,4 +1,5 @@ import React from 'react'; +import c3 from 'c3'; import Templates from 'config/templates'; import CalendarGridChart from './../../../d3/grid/calendar_grid'; diff --git a/client/dashboard/energy/table/table.rt b/client/dashboard/energy/table/table.rt index 78538b1..6cc43d1 100644 --- a/client/dashboard/energy/table/table.rt +++ b/client/dashboard/energy/table/table.rt @@ -4,6 +4,7 @@ Day Consumption (kWh) + Daily Mean Irradiance (W/m2) Production (kWh) @@ -11,8 +12,9 @@ {energy_datum.day_to_s} - {energy_datum.consumption_to_s} - {energy_datum.production_to_s} + {energy_datum.consumption} + {energy_datum.irradiance} + {energy_datum.production} diff --git a/client/dashboard/irradiance/graph/graph.component.js b/client/dashboard/irradiance/graph/graph.component.js new file mode 100644 index 0000000..ce4e43b --- /dev/null +++ b/client/dashboard/irradiance/graph/graph.component.js @@ -0,0 +1,152 @@ +import React from 'react'; +import Templates from 'config/templates'; +import c3 from 'c3'; + +class GraphComponent extends React.Component { + + constructor(props){ + super(props); + } + + get state_manager(){ + return this.props.state_manager; + } + + get houses(){ + return this.state_manager.houses; + } + + get chart_data(){ + var irradiance_graph = this; + return Object.keys(irradiance_graph.state_manager.irradiance_data).map((day)=>{ + var day_data = irradiance_graph.state_manager.irradiance_data[day], + day_datum = {date: day}; + day_data.forEach((energy_datum)=>{ + day_datum['irradiance'+energy_datum.house.data.id] = energy_datum.irradiance; + day_datum['production'+energy_datum.house.data.id] = energy_datum.production; + }); + return day_datum; + }).filter((day_datum)=>{ + // due to timezone offsets, some houses might not have an energy_datum point, + // where others do. Just filter those dates out to avoid UI confusion. + return Object.keys(day_datum).length === irradiance_graph.value_keys.length; + }); + } + + get value_keys(){ + var irradiance_graph = this; + return ['date'].concat(Object.keys(irradiance_graph.names)); + } + + get irradiance_keys(){ + return this.houses.map((house)=>{ + return 'irradiance' + house.data.id; + }); + } + + get production_keys(){ + return this.houses.map((house)=>{ + return 'production' + house.data.id; + }); + } + + get colors(){ + var fnColor = d3.scale.category20(), + irradiance_graph = this; + return Object.keys(irradiance_graph.names).reduce((colors, key)=>{ + colors[key] = fnColor(key); + return colors; + }, {}); + } + + get names(){ + var names = {}; + this.houses.forEach((house)=>{ + names['irradiance' + house.data.id] = house.data.name + ' Irradiance'; + names['production' + house.data.id] = house.data.name + ' Production'; + }); + return names; + } + + get axes(){ + var irradiance_graph = this, + axes = {}; + irradiance_graph.production_keys.forEach((production_key)=>{ + axes[production_key] = 'y'; + }); + irradiance_graph.irradiance_keys.forEach((irradiance_key)=>{ + axes[irradiance_key] = 'y2'; + }); + return axes; + } + + get types(){ + var irradiance_graph = this; + return irradiance_graph.production_keys.reduce((types, production_key)=>{ + types[production_key] = 'bar'; + return types; + }, {}); + } + + componentDidMount(){ + var irradiance_graph = this; + irradiance_graph.updateGraph(); + } + + componentDidUpdate(prev_props, prev_state){ + var irradiance_graph = this; + if (irradiance_graph.props.date_interval[0] != prev_props.date_interval[0] || + irradiance_graph.props.date_interval[1] != prev_props.date_interval[1]){ + irradiance_graph.updateGraph(); + } + } + + updateGraph(){ + var irradiance_graph = this, + data = { + json: irradiance_graph.chart_data, + keys: { + x: 'date', // it's possible to specify 'x' when category axis + value: irradiance_graph.value_keys, + }, + types: irradiance_graph.types, + names: irradiance_graph.names, + groups: [irradiance_graph.production_keys], + axes: irradiance_graph.axes, + colors: irradiance_graph.colors + }; + if (!irradiance_graph.chart){ + irradiance_graph.chart = c3.generate({ + bindto: '#irradiance_graph', + data: data, + axis: { + x: { + type: 'timeseries', + tick: { format: d3.time.format('%d %B %y') } + }, + y: { + label: 'Production' + }, + y2: { + show: true, + label: 'Irradiance' + } + } + }); + } else { + console.log('reloading data') + console.log(data) + data.unload = irradiance_graph.chart.data; + irradiance_graph.chart.load(data); + } + } + + render() { + var irradianceGraphRt = Templates.forComponent('irradiance_graph'); + return irradianceGraphRt.call(this); + } +} +GraphComponent.NAME = 'IrradianceGraph'; + +module.exports = GraphComponent; + diff --git a/client/dashboard/irradiance/graph/graph.rt b/client/dashboard/irradiance/graph/graph.rt new file mode 100644 index 0000000..6bce0b8 --- /dev/null +++ b/client/dashboard/irradiance/graph/graph.rt @@ -0,0 +1 @@ +
diff --git a/client/dashboard/irradiance/irradiance.component.js b/client/dashboard/irradiance/irradiance.component.js new file mode 100644 index 0000000..ad18ce3 --- /dev/null +++ b/client/dashboard/irradiance/irradiance.component.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Templates from 'config/templates'; + +class IrradianceComponent extends React.Component { + + get state_manager(){ + return this.props.state_manager; + } + + render() { + var irradianceRt = Templates.forComponent('irradiance'); + return irradianceRt.call(this); + } + +} +IrradianceComponent.NAME = 'Irradiance'; + +module.exports = IrradianceComponent; diff --git a/client/dashboard/irradiance/irradiance.rt b/client/dashboard/irradiance/irradiance.rt new file mode 100644 index 0000000..cb7e9d1 --- /dev/null +++ b/client/dashboard/irradiance/irradiance.rt @@ -0,0 +1,13 @@ + + +
+

Irradiance

+ + +
diff --git a/client/dashboard/irradiance/irradiance.scss b/client/dashboard/irradiance/irradiance.scss new file mode 100644 index 0000000..d931092 --- /dev/null +++ b/client/dashboard/irradiance/irradiance.scss @@ -0,0 +1,3 @@ +#irradiance_component { + +} diff --git a/client/dashboard/irradiance/table/table.component.js b/client/dashboard/irradiance/table/table.component.js new file mode 100644 index 0000000..e80fe06 --- /dev/null +++ b/client/dashboard/irradiance/table/table.component.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Templates from 'config/templates'; + +class TableComponent extends React.Component { + + get state_manager(){ + return this.props.state_manager; + } + + render() { + var irradianceTableRt = Templates.forComponent('irradiance_table'); + return irradianceTableRt.call(this); + } +} + +TableComponent.NAME = 'IrradianceTable'; + +module.exports = TableComponent; diff --git a/client/dashboard/irradiance/table/table.rt b/client/dashboard/irradiance/table/table.rt new file mode 100644 index 0000000..48dea83 --- /dev/null +++ b/client/dashboard/irradiance/table/table.rt @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + +
DayHouseDaily Mean Irradiance (W/m2)Production (kWh)
{energy_datum.day_to_s}{energy_datum.house.data.name}{energy_datum.irradiance}{energy_datum.production}
diff --git a/client/dashboard/layout/layout.component.js b/client/dashboard/layout/layout.component.js index 7a04d09..5da1f66 100644 --- a/client/dashboard/layout/layout.component.js +++ b/client/dashboard/layout/layout.component.js @@ -6,6 +6,7 @@ import Templates from 'config/templates'; import House from './../../models/house'; import PowerDatum from './../../models/power_datum'; import StateManager from './../state_manager'; +import DateRangeSlider from './../../d3/sliders/date_range'; class LayoutComponent extends React.Component { @@ -18,6 +19,8 @@ class LayoutComponent extends React.Component { house: null, dataset: null, year: null, + month: null, + date_interval: null, view: null } } @@ -50,9 +53,35 @@ class LayoutComponent extends React.Component { }); } + componentDidUpdate(prev_props, prev_state){ + var layout = this; + if (layout.shouldShowDateRange() && !layout.datesMatch(prev_state)){ + layout.updateDateRange(); + } else if (!layout.shouldShowDateRange()){ + layout.destroyDateRange(); + } + } + + datesMatch(prev_state){ + var layout = this; + return layout.state.month == prev_state.month && + layout.state.year == prev_state.year && + !layout.shouldShowDateRange() || + layout.state.date_interval && prev_state.date_interval && + layout.state.date_interval[0] == prev_state.date_interval[0] && + layout.state.date_interval[1] == prev_state.date_interval[1]; + } + + shouldShowDateRange(){ + var layout = this; + return layout.state.house && layout.state.dataset === 'power' || layout.state.dataset === 'irradiance'; + } + syncFromStateManager(fnStateSet){ var layout = this; - layout.setState(layout.state_manager.state, fnStateSet); + layout.setState(layout.state_manager.state, ()=>{ + fnStateSet() + }); } setHouse(event){ @@ -72,6 +101,50 @@ class LayoutComponent extends React.Component { layout.state_manager.setParams(update, layout); } + destroyDateRange(){ + var layout = this, + container = document.getElementById('date_interval'); + if (container) container.innerHTML = ''; + layout.date_interval_slider = undefined; + } + + updateDateRange(){ + var layout = this, + house = layout.house, + state_manager = layout.state_manager; + if (layout.date_interval_slider === undefined){ + layout.date_interval_slider = new DateRangeSlider({ + container: '#date_interval', + outer_height: 100, + maxDelta: function(changed_date, other_date){ + if (Math.abs(changed_date.getTime() - other_date.getTime()) > House.MAX_POWER_RANGE * 1000){ + if (changed_date > other_date){ + return new Date(changed_date.getTime() - House.MAX_POWER_RANGE * 1000); + } else { + return new Date(changed_date.getTime() + House.MAX_POWER_RANGE * 1000); + } + } + return false; + } + }); + } + layout.date_interval_slider.onRangeUpdated = (min, max)=>{ + if (layout.date_interval_update) clearTimeout(layout.date_interval_update); + // This will update the URL -> state_manager.state -> component states. + layout.date_interval_update = setTimeout(()=>{ + var date_interval = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)]; + layout.state_manager.setParams({date_interval: date_interval}, layout); + }, 500); + }; + var month_range = state_manager.month_range; + layout.date_interval_slider.drawData({ + abs_min: house.toDate(month_range[0]), + abs_max: house.toDate(month_range[1]), + current_min: house.toDate(state_manager.state.date_interval[0]), + current_max: house.toDate(state_manager.state.date_interval[1]) + }); + } + refreshData(){ var layout = this, houses = layout.state.houses, diff --git a/client/dashboard/layout/layout.rt b/client/dashboard/layout/layout.rt index 46db55a..08a5386 100644 --- a/client/dashboard/layout/layout.rt +++ b/client/dashboard/layout/layout.rt @@ -1,4 +1,5 @@ +
@@ -9,7 +10,7 @@
  • React
  • React Templates
  • -
  • React Router
  • +
  • ReactJs History
  • LokiJs - persisting API calls to indexedDb
  • Webpack - hot mode developing and app bundling
  • Babel - ES6 transpiler
  • @@ -22,15 +23,23 @@
    Retrieving houses...
    -

    Select household:

    - +
    +

    Select household:

    + +
    -
    +

    Select dataset:

    + + type="button" class="btn btn-primary">Daily Mean Irradiance

    View as:

    @@ -72,26 +81,44 @@ class="btn-info btn btn-sm" rt-class="{active: year == this.state.year}" onClick="{this.setParam.bind(this)}">{year} +

    +
    + +

    +
    + +
    + Retrieving {this.state.loading_data} data...

    + + date_interval="{this.state.date_interval}" />
diff --git a/client/dashboard/power/graph/graph.component.js b/client/dashboard/power/graph/graph.component.js index 0da9a0b..f68b705 100644 --- a/client/dashboard/power/graph/graph.component.js +++ b/client/dashboard/power/graph/graph.component.js @@ -1,8 +1,8 @@ import React from 'react'; import Templates from 'config/templates'; +import c3 from 'c3'; import House from './../../../models/house'; -import c3 from 'c3'; class GraphComponent extends React.Component { @@ -21,7 +21,7 @@ class GraphComponent extends React.Component { componentDidUpdate(prev_props, prev_state){ var power_graph = this; - if (prev_props.house != power_graph.props.house || prev_props.power_range != power_graph.props.power_range){ + if (prev_props.house != power_graph.props.house || prev_props.date_interval != power_graph.props.date_interval){ power_graph.updateGraph(); } } diff --git a/client/dashboard/power/power.component.js b/client/dashboard/power/power.component.js index 3b60aaf..35753f9 100644 --- a/client/dashboard/power/power.component.js +++ b/client/dashboard/power/power.component.js @@ -4,7 +4,6 @@ import _ from 'lodash'; import Templates from 'config/templates'; import House from './../../models/house'; -import DateRangeSlider from './../../d3/sliders/date_range'; class PowerComponent extends React.Component { @@ -25,75 +24,11 @@ class PowerComponent extends React.Component { return this.props.state_manager; } - get loading_power_data(){ - return this.props.loading_power_data || this.state.loading_power_data; - } - - componentDidMount(){ - var power = this; - power.initDateRange(); - } - - componentDidUpdate(prev_props, prev_state){ - var power = this, - state_manager = power.state_manager; - if (prev_props.power_range[0] != power.props.power_range[0] || - prev_props.power_range[1] != power.props.power_range[1] || - prev_props.house != power.props.house){ - power.initDateRange(); - state_manager.powerDataRendered(); - } - } - syncFromStateManager(fnStateSet){ var power = this; power.setState(power.state_manager.state, fnStateSet); } - initDateRange(){ - var power = this, - house = power.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)]; - power.state_manager.setParams({power_range: power_range}, power); - }, 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, - param = event.target.dataset.param, - value = event.target.dataset.value, - update = {}, route_helper; - update[param] = value; - if (value == power.state_manager.state[param]) return false; - power.state_manager.setParams(update, power); - } - render() { var powerRt = Templates.forComponent('power'); return powerRt.call(this); diff --git a/client/dashboard/power/power.rt b/client/dashboard/power/power.rt index 281d102..938467b 100644 --- a/client/dashboard/power/power.rt +++ b/client/dashboard/power/power.rt @@ -1,33 +1,18 @@
-
- -
-
- Retrieving power data... -
-
+ date_interval="{this.props.date_interval}" > + date_interval="{this.props.date_interval}" >
diff --git a/client/dashboard/state_manager.js b/client/dashboard/state_manager.js index 4025a77..21464cc 100644 --- a/client/dashboard/state_manager.js +++ b/client/dashboard/state_manager.js @@ -1,5 +1,7 @@ import query_string from 'query-string'; +import moment from 'moment-timezone'; +import EnergyDatum from './../models/energy_datum'; import ObjectUtil from './../../shared/utils/object'; import ArrayUtil from './../../shared/utils/array'; @@ -13,6 +15,9 @@ const ROUTES = [ }, { path: /houses\/(\d+)\/(power)\/([^\/]+)\/(\d+)\/([^\/]+)\/?$/, parameters: { 1: 'house_id', 2: 'dataset', 3: 'month', 4: 'year', 5: 'view' } + }, { + path: /(irradiance)\/([^\/]+)\/(\d+)\/([^\/]+)\/?$/, + parameters: { 1: 'dataset', 2: 'month', 3: 'year', 4: 'view' } } ]; @@ -24,8 +29,7 @@ class StateManager { state_manager.houses = houses; state_manager.state = { - loading_energy_data: false, - loading_power_data: false, + loading_data: false, graph_attr: 'consumption', view: 'graph', dataset: 'power', @@ -33,55 +37,94 @@ class StateManager { house: null, month: null, year: null, - power_range: null }; + date_interval: null }; state_manager.history = createHistory(); state_manager.update_in_progress = false; } + get month_i(){ + return moment.monthsShort().indexOf(this.state.month); + } + get date_params(){ - return ObjectUtil.filterKeys(this.state, ['year', 'month', 'power_range']); + return ObjectUtil.filterKeys(this.state, ['year', 'month', 'date_interval']); + } + + get month_range(){ + var state_manager = this, + house = state_manager.state.house, + start_time = house.parseMoment(`${state_manager.state.year}-${state_manager.month_i + 1}-01`, 'YYYY-M-DD'), + end_time = start_time.clone().endOf('month').unix(); + + start_time = start_time.unix(); + if (start_time < house.data.data_from) start_time = house.data.data_from; + if (end_time > house.data.data_until) end_time = house.data.data_until; + return [start_time, end_time]; + } + + get year_range(){ + var state_manager = this, + house = state_manager.state.house, + start_time = house.parseMoment(`${state_manager.state.year}-01-01`, 'YYYY-MM-DD'), + end_time = start_time.clone().endOf('year').unix(); + + start_time = start_time.unix(); + if (start_time < house.data.data_from) start_time = house.data.data_from; + if (end_time > house.data.data_until) end_time = house.data.data_until; + return [start_time, end_time]; + } + + matchesEnergyState(){ + var state_manager = this, + house = state_manager.state.house, + energy_range = state_manager.state.graph_attr === 'irradiance' ? state_manager.state.date_interval : state_manager.year_range; + if (!house.state.energy_range) return false; + return energy_range[0] === house.state.energy_range[0] && energy_range[1] === house.state.energy_range[1]; + } + + matchesPowerState(){ + var state_manager = this, + house = state_manager.state.house, + month_range = state_manager.month_range; + if (!house.state.power_range) return false; + return month_range[0] === house.state.power_range[0] && month_range[1] === house.state.power_range[1]; } // This will update the house state acccording to passed update parameters. - updateHouseFromState(component, fnResolve){ + updateHouseFromState(component){ var state_manager = this, house = state_manager.state.house, promise; if (!house) { promise = Promise.resolve(); - } else if (state_manager.state.dataset === 'energy' && (!house.energy_data || !house.matchesEnergyState(state_manager.state))){ - house.setMonthState(state_manager.state); + } else if (state_manager.state.dataset === 'energy' && !state_manager.matchesEnergyState()){ promise = state_manager.setHouseEnergyFromState(component); - } else if (state_manager.state.dataset === 'power' && !house.power_data || !house.matchesPowerState(state_manager.state)){ - house.setMonthState(state_manager.state); + } else if (state_manager.state.dataset === 'power' && !state_manager.matchesPowerState()){ promise = state_manager.setHousePowerFromState(component); + } else if (state_manager.state.dataset === 'irradiance'){ + promise = state_manager.setIrradianceData(component); } else { - promise = new Promise((fnResolve, fnReject)=>{ - component.syncFromStateManager(fnResolve); - }); + promise = Promise.resolve(); } - return promise.then(()=>{ state_manager.update_in_progress = false; }) - } - - setHouseEnergyFromState(component){ - var state_manager = this; - state_manager.power_data_updated = true; - return new Promise((fnResolve, fnReject)=>{ - component.setState({ - loading_energy_data: true - }, ()=>{ - state_manager.state.house.setEnergyData() - .then(()=>{ - component.syncFromStateManager(fnResolve); - }); + return promise.then(()=>{ + state_manager.update_in_progress = false; + return new Promise((fnResolve, fnReject)=>{ + component.syncFromStateManager(fnResolve); }); }); } - powerDataRendered(){ + setHouseEnergyFromState(component){ var state_manager = this; - state_manager.power_data_updated = false; + return new Promise((fnResolve, fnReject)=>{ + component.setState({ + loading_data: 'power' + }, ()=>{ + state_manager.state.house.setEnergyData(state_manager.year_range) + .then(fnResolve); + }); + }); } setHousePowerFromState(component){ @@ -89,11 +132,49 @@ class StateManager { house = state_manager.state.house; return new Promise((fnResolve, fnReject)=>{ component.setState({ - loading_power_data: true + loading_data: 'energy' }, ()=>{ - house.setPowerData() - .then(()=>{ - component.syncFromStateManager(fnResolve); + house.setPowerData(state_manager.state.date_interval) + .then(fnResolve); + }); + }); + } + + setIrradianceData(component){ + var state_manager = this, + houses = state_manager.houses, + date_interval = state_manager.state.date_interval; + return new Promise((fnResolve, fnReject)=>{ + component.setState({ + loading_data: 'irradiance' + }, ()=>{ + EnergyDatum.ensureEnergyDataForHouses(houses, date_interval) + .then((res)=>{ + if (res instanceof Promise){ + throw new Error('promise returned promise') + } + var promises = [], + data = {}; + houses.forEach((house)=>{ + var promise = house.setEnergyData(date_interval) + .then(()=>{ + house.energy_data.forEach((energy_datum)=>{ + var date_data = data[energy_datum.day_to_s]; + if (!date_data){ + date_data = []; + data[energy_datum.day_to_s] = date_data; + } + date_data.push(energy_datum); + }); + house.closeDb(); + }); + promises.push(promise); + }); + Promise.all(promises) + .then(()=>{ + state_manager.irradiance_data = data; + fnResolve(); + }); }); }); }); @@ -105,25 +186,29 @@ class StateManager { setParams(params){ var state_manager = this, - url; + url, house, params; if (state_manager.update_in_progress) return false; state_manager.update_in_progress = true; - params = Object.assign({}, state_manager.state, params); - if (!params.house_id){ - url = '/'; - } else { - var house = state_manager.houses.find((h)=>{ return h.data.id == params.house_id; }) - house.verifyMonthState(params); - if (params.dataset === 'energy'){ - url = `/houses/${params.house_id}/energy/${params.year}/${params.graph_attr}/${params.view}`; - } else if (params.dataset === 'power'){ - house.verifyPowerRange(params); - url = `/houses/${params.house_id}/power/${params.month}/${params.year}/${params.view}?${query_string.stringify({dates: params.power_range})}`; - } else { - url = `/houses/${house.house_id}`; - } + params = Object.assign({}, state_manager.state, params); + if (params.house_id){ + house = state_manager.houses.find((h)=>{ return h.data.id == params.house_id; }); + } else { + house = state_manager.state.house || state_manager.houses[0]; + params.house_id = house.data.id; } + + house.verifyMonthState(params); + if (params.dataset === 'irradiance'){ + params.date_interval = house.verifyPowerRange(params.date_interval || [], params); + url = `/irradiance/${params.month}/${params.year}/${params.view}?${query_string.stringify({dates: params.date_interval})}`; + } else if (params.dataset === 'energy'){ + url = `/houses/${params.house_id}/energy/${params.year}/${params.graph_attr}/${params.view}`; + } else { + params.date_interval = house.verifyPowerRange(params.date_interval || [], params); + url = `/houses/${params.house_id}/power/${params.month}/${params.year}/${params.view}?${query_string.stringify({dates: params.date_interval})}`; + } + state_manager.history.push(url); } @@ -132,16 +217,28 @@ class StateManager { */ updateStateFromUrl(location, component){ - var state_manager = this; - var params = state_manager.parseUrl(location.pathname), + var state_manager = this, + params = state_manager.parseUrl(location.pathname), house = null; - if (params.dataset === 'power' && location.query.dates) { - params.power_range = [+location.query.dates[0], +location.query.dates[1]]; - } - if (params.house_id || params.house_id != state_manager.state.house_id){ + if (params.house_id){ house = state_manager.houses.find((h)=>{ return h.data.id == params.house_id; }); + } else if (params.dataset === 'irradiance'){ + // Irradiance needs a house to verify params and + house = state_manager.state.house || state_manager.houses[0]; } - state_manager.state.house = house; + + if (house){ + // params should already be verified if set through StateManager#setParams, but + // verify here again before setting state in case URL manually loaded. + house.verifyMonthState(params); + if (params.dataset === 'power' || params.dataset === 'irradiance') { + var date_interval = location.query.dates || []; + params.date_interval = house.verifyPowerRange([+date_interval[0], +date_interval[1]], params); + } + state_manager.state.house = house; + state_manager.state.house_id = house.data.id; + } + Object.assign(state_manager.state, params); if (state_manager.state.house_id) { state_manager.updateHouseFromState(component); diff --git a/client/lib/databasable.js b/client/lib/databasable.js index abe6b67..06d5fda 100644 --- a/client/lib/databasable.js +++ b/client/lib/databasable.js @@ -7,19 +7,21 @@ const DEFAULTS = { var databasable = { - accessDb: function(db_name, opts){ - var databasable = this; - opts = Object.assign(Object.assign({ - adapter: new LokiIndexedAdapter(db_name) - }, DEFAULTS), opts || {}); + accessDb: function(db_name){ + var databasable = this, + opts = Object.assign({ + adapter: new LokiIndexedAdapter(db_name) + }, DEFAULTS, databasable.lokijs_options || {}), + has_adapter = !!opts.adapter; return new Promise((fnResolve, fnReject)=>{ if (!databasable.db) { databasable.db = new Loki(db_name, opts); - databasable.db.loadDatabase({}, ()=>{ - fnResolve(databasable.db); - }); + if (has_adapter){ + databasable.db.loadDatabase({}, ()=>{ + fnResolve(databasable.db); + }); + } else { fnResolve(databasable.db); } } else { fnResolve(databasable.db); } - }); }, @@ -34,8 +36,12 @@ var databasable = { collection: function(db_name, collection_name, options){ var databasable = this; + options = options || {}; return databasable.accessDb(db_name) .then((db)=>{ + if (!db || db !== databasable.db){ + throw new Error('Databasable does not have db set.') + } var collection = db.getCollection(collection_name) if (!collection){ collection = db.addCollection(collection_name, options); diff --git a/client/models/energy_datum.js b/client/models/energy_datum.js index 5fc32a8..603b3e2 100644 --- a/client/models/energy_datum.js +++ b/client/models/energy_datum.js @@ -1,6 +1,9 @@ import extend from 'extend'; import moment from 'moment-timezone'; +import DateRange from './../../shared/utils/date_range'; +import EnergyDataApi from 'api/energy_data'; + const NAME = 'EnergyDatum'; const COLLECTION_DEFAULTS = { indices: ['day'] @@ -29,16 +32,58 @@ class EnergyDatum { return moment.tz(energy_datum.data.day * 1000, energy_datum.house.data.timezone).format('YYYY-MM-DD'); } - get consumption_to_s(){ + get irradiance(){ + var energy_datum = this; + return Math.round(energy_datum.data.irradiance); + } + + get consumption(){ var energy_datum = this; return Math.round(energy_datum.data.consumption); } - get production_to_s(){ + get production(){ var energy_datum = this; return Math.round(energy_datum.data.production); } + // This method will ensure the energy data for the passed houses while: + // 1. Making only 1 API. + // 2. Only opening 1 house LokiJs DB at a time. + static ensureEnergyDataForHouses(houses, date_range){ + var new_ranges = {}, params = []; + houses.forEach((house)=>{ + var query_ranges = DateRange.addRange(date_range, house.data.energy_datum_ranges || []); + if (query_ranges.gaps_filled.length > 0) { + params.push({dates: query_ranges.gaps_filled, house_id: house.data.id}); + new_ranges[house.data.id] = query_ranges.new_ranges; + } + }); + + // already have all the data we need. + if (params.length === 0) return Promise.resolve(); + + // get all data needed for all houses in one call. + return new Promise((fnResolve, fnReject)=>{ + EnergyDataApi.index({houses: params}) + .then((energy_data)=>{ + energy_data = energy_data.reduce((grouped, energy_datum)=>{ + grouped[energy_datum.house_id] = grouped[energy_datum.house_id] || []; + grouped[energy_datum.house_id].push(energy_datum); + return grouped; + }, {}); + houses.reduce((promise, house)=>{ + return promise.then(()=>{ + if (!energy_data[house.data.id]) return Promise.resolve(); + return house.saveEnergyData(energy_data[house.data.id], new_ranges[house.data.id]) + }).then(()=>{ + house.closeDb(); + }); + }, Promise.resolve()).then(fnResolve); + }); + }) + } + } EnergyDatum.NAME = NAME; diff --git a/client/models/house.js b/client/models/house.js index b059f15..83cce86 100644 --- a/client/models/house.js +++ b/client/models/house.js @@ -13,6 +13,7 @@ import DateRange from './../../shared/utils/date_range'; import Databasable from './../lib/databasable'; const NAME = 'House'; +const MAX_POWER_RANGE = 3600 * 24 * 4; // 4 days class House { @@ -28,10 +29,6 @@ class House { for (var year=house.data_from_moment.year(); year<=house.data_until_moment.year(); year+=1){ house.years.push(year); } - house.setMonthState({ - month: house.data_until_moment.format('MMM'), - year: house.data_until_moment.year() - }); } get data_from_moment(){ @@ -53,10 +50,14 @@ class House { else return {}; } + parseMoment(s, format){ + var house = this; + return moment.tz(s, format, house.data.timezone); + } + availableMonths(year){ var house = this, all_months = moment.monthsShort(); - year = 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')){ @@ -66,95 +67,56 @@ class House { } } - // this will mutate params and set house.state. - setMonthState(params){ - var house = this; - house.verifyMonthState(params); - house.state.month = params.month; - house.state.year = params.year; - - var month_i = moment.monthsShort().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; - var end_of_month = new_month_moment.clone().endOf('month') - house.state.end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month - } - - house.verifyPowerRange(params); - house.state.power_range = params.power_range; - var energy_max = Math.min(house.state.end_of_current_data_moment.clone().endOf('year').unix(), house.data.data_until); - house.state.energy_range = [house.state.end_of_current_data_moment.clone().startOf('year').unix(), energy_max]; - } - // This will mutate params. verifyMonthState(params){ var house = this; - params.month = params.month || house.state.month; - params.year = params.year || house.state.year; - 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()){ - if (house.state.year) params.year = house.state.year; - else params.year = house.years[house.years.length - 1]; - } + params.month = params.month || house.data_until_moment.format('MMM'); + params.year = params.year || house.data_until_moment.year(); + var new_year = +params.year; + if (new_year < house.data_from_moment.year()) params.year = house.data_from_moment.year(); + else if (new_year > house.data_until_moment.year()) params.year = house.data_until_moment.year(); - var available_months = house.availableMonths(params.year); - if (available_months.indexOf(params.month) < 0){ - if (house.state.month) params.month = house.state.month; - else params.month = available_months[available_months.length - 1]; - } + var available_months = house.availableMonths(params.year); + if (available_months.indexOf(params.month) < 0){ + params.month = available_months[available_months.length - 1]; } } // This will mutate params - verifyPowerRange(params){ + verifyPowerRange(power_range, params){ var house = this, month_i = moment.monthsShort().indexOf(params.month), month_moment = moment.tz({year: params.year, month: month_i, day: 1}, house.data.timezone).startOf('month'), end_of_month = month_moment.clone().endOf('month'), - end_of_current_data_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month; + start_moment = month_moment < house.data_from_moment ? house.data_from_moment : month_moment, + end_moment = end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month; - params.power_range = params.power_range || []; - - var current_data_range = [month_moment.unix(), end_of_current_data_moment.unix()], - power_min = params.power_range[0], - power_max = params.power_range[1]; - if (params.power_range.length > 0){ - if (DateRange.inRange(params.power_range[1], current_data_range)){ - power_max = params.power_range[1]; - } - if (DateRange.inRange(params.power_range[0], current_data_range) && params.power_range[0] < power_max){ - power_min = params.power_range[0]; - } + var current_data_range = [start_moment.unix(), end_moment.unix()], + state_power_range = house.state.power_range || [], + power_min = state_power_range[0], + power_max = state_power_range[1]; + if (power_range[1] && DateRange.inRange(power_range[1], current_data_range)){ + power_max = power_range[1]; + } + if (power_range[0] && DateRange.inRange(power_range[0], current_data_range) && power_range[0] < power_max){ + power_min = power_range[0]; } if (!power_max || !DateRange.inRange(power_max, current_data_range)){ - power_max = end_of_current_data_moment.unix(); + power_max = end_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; + power_max - power_min > MAX_POWER_RANGE){ + power_min = power_max - MAX_POWER_RANGE; } - params.power_range = [power_min, power_max]; - } - - matchesEnergyState(params){ - var house = this; - return params.year == house.state.year; - } - - matchesPowerState(params){ - var house = this; - return params.month === house.state.month && params.year == house.state.year && - house.state.power_range[0] == params.power_range[0] && house.state.power_range[1] == params.power_range[1]; + return [power_min, power_max]; } offset_diff(unix){ var house = this, tz = moment.tz.zone(house.data.timezone); - return (new Date().getTimezoneOffset() - tz.offset(unix * 1000)) * 60; + return (new Date(unix * 1000).getTimezoneOffset() - tz.offset(unix * 1000)) * 60; } toDate(unix){ @@ -176,8 +138,9 @@ class House { }); } - setPowerData(){ + setPowerData(power_range){ var house = this; + house.state.power_range = power_range; return house.collection(house.scoped_id, PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS) .then((power_collection)=>{ return house.ensurePowerData() @@ -197,7 +160,6 @@ class House { ensurePowerData(){ var house = this, query_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}; @@ -222,12 +184,13 @@ class House { }) } - setEnergyData(){ + setEnergyData(energy_range){ var house = this; + house.state.energy_range = energy_range; return house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) .then((energy_collection)=>{ return house.ensureEnergyData() - .then(()=>{ + .then((res)=>{ var params = house.rangeToLokiParams('day', house.state.energy_range); house.energy_data = energy_collection.find(params) .sort((pd1, pd2)=>{ @@ -244,25 +207,36 @@ class House { var house = this, 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(()=>{ - house.data.energy_datum_ranges = query_ranges.new_ranges; - house.save(); - }); + return new Promise((fnResolve, fnReject)=>{ + house.getEnergyData({dates: query_ranges.gaps_filled}) + .then((energy_data)=>{ + house.saveEnergyData(energy_data, query_ranges.new_ranges) + .then(fnResolve); + }); + }); } else { return Promise.resolve(); } } getEnergyData(params){ var house = this; params.house_id = house.data.id; - return house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) - .then((energy_collection)=>{ - return EnergyDataApi.index(params) - .then((energy_data)=>{ - energy_collection.insert(energy_data); - house.db.save(); - }); - }) + return EnergyDataApi.index(params); + } + + // save new energy data to LokiJs Db, as well as + // the new energy data query ranges (ie house metadata). + saveEnergyData(energy_data, new_ranges){ + var house = this; + return new Promise((fnResolve, fnReject)=>{ + house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) + .then((energy_collection)=>{ + energy_collection.insert(energy_data); + house.db.save(); + house.data.energy_datum_ranges = new_ranges; + house.save(); + fnResolve(); + }); + }); } // removes all energy and power data from LokiJs (memory and persisted) database. @@ -317,6 +291,7 @@ class House { } House.NAME = NAME; +House.MAX_POWER_RANGE = MAX_POWER_RANGE; Object.assign(House, Databasable); export default House; diff --git a/gulpfile.babel.js b/gulpfile.babel.js index d1100e3..843b203 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -18,19 +18,6 @@ gulp.task('generate_power_csv', function(done){ }); }); -gulp.task('generate_design_data', function(){ - var exec = require('child_process').exec, - house_ids = yargs.argv.house_ids.split(','), - start_date = parseInt(yargs.argv.start_date), - end_date = parseInt(yargs.argv.end_date), - data_generator = new DesignDataGenerator(house_ids, [start_date, end_date]); - return DB.sync() - .then(()=>{ - return data_generator.exec(); - }); -}); - - gulp.task('save_power_csv', function(done){ DB.sync().then(()=>{ PowerDataSeed.saveCsv(yargs.argv, done); @@ -43,6 +30,15 @@ gulp.task('save_house_csv', function(done){ }); }); +gulp.task('generate_design_data', function(){ + var house_ids = yargs.argv.house_ids.split(','), + start_date = parseInt(yargs.argv.start_date), + end_date = parseInt(yargs.argv.end_date), + data_generator = new DesignDataGenerator(house_ids, [start_date, end_date]); + return DB.sync() + .then(()=>{ return data_generator.exec(); }); +}); + // right now, build only available for design. gulp.task('build', function(done) { var config, env; diff --git a/karma.conf.js b/karma.conf.js index 69ab3fb..3dfb51f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -48,6 +48,9 @@ module.exports = function (config) { api: __dirname + '/client/api/development', config: __dirname + '/client/config/development' } + }, + node: { + fs: "empty" } }, }); diff --git a/server/app.express.js b/server/app.express.js index 910d64c..5132a3f 100644 --- a/server/app.express.js +++ b/server/app.express.js @@ -24,10 +24,10 @@ var api = express(); DB.sync().then(()=>{ - routes(api); - api.use(bodyParser.json()); - api.use(bodyParser.urlencoded({ extended: false })); + api.use(bodyParser.urlencoded({ extended: true })); + + routes(api); api.listen(API_PORT, () => { console.log(`API is now running on http://localhost:${API_PORT}`); diff --git a/server/controllers/energy_controller.js b/server/controllers/energy_controller.js index 9646461..9de9164 100644 --- a/server/controllers/energy_controller.js +++ b/server/controllers/energy_controller.js @@ -5,9 +5,14 @@ const NAME = 'EnergyController'; class EnergyController{ static index(req, res){ - DB.EnergyDatum.exposeForHouseAtDates(req.query.house_id, req.query.dates).then((energy_data)=>{ - res.json({data: energy_data}); - }); + console.log ('EnergyController.index'); + console.log(JSON.stringify(req.body)) + console.log(JSON.stringify(req.params)) + console.log(JSON.stringify(req.query)) + DB.EnergyDatum.exposeForHouseAtDates(req.body) + .then((energy_data)=>{ + res.json({data: energy_data}); + }); } } diff --git a/server/models/energy_datum.js b/server/models/energy_datum.js index fc62db7..cbbf3a2 100644 --- a/server/models/energy_datum.js +++ b/server/models/energy_datum.js @@ -32,16 +32,27 @@ var EnergyDatum = DB.sequelize.define(NAME, { associate: ()=>{ EnergyDatum.belongsTo(DB.House); }, - exposeForHouseAtDates: (house_id, dates)=>{ - var params = {house_id: house_id}; - extend(params, ApiHelper.datesParamToSequelize(dates, 'day')); + exposeForHouseAtDates: (query)=>{ + var attributes = ['id', 'production', 'irradiance', 'consumption', 'day'], + params = {}; + if (query.houses){ + attributes.push('house_id'); + params['$or'] = [] + query.houses.forEach((house_query)=>{ + var house_params = {house_id: house_query.house_id}; + extend(house_params, ApiHelper.datesParamToSequelize(house_query.dates, 'day')); + params['$or'].push(house_params); + }); + } else { + params.house_id = query.house_id; + extend(params, ApiHelper.datesParamToSequelize(query.dates, 'day')); + } + return EnergyDatum.findAll({ where: params, - attributes: ['id', 'production', 'consumption', 'day'] + attributes: attributes }).then((energy_data)=>{ - return energy_data.map((energy_datum)=>{ - return energy_datum.dataValues; - }); + return energy_data.map(energy_datum => energy_datum.dataValues); }); } } diff --git a/shared/utils/date_range.js b/shared/utils/date_range.js index b56b7ed..1636bb8 100644 --- a/shared/utils/date_range.js +++ b/shared/utils/date_range.js @@ -17,7 +17,9 @@ class DateRange { if (end && !DateRange.eq(end, range[0]) && DateRange.lte(end, range[0])){ new_ranges.push([last_start, end]); new_ranges.push(range); - gaps_filled.push([last_end, end]); + if (!DateRange.eq(end, last_end)){ + gaps_filled.push([last_end, end]); + } covered = true; } else if (end && !DateRange.gte(end, range[1])) { new_ranges.push([last_start, range[1]]); diff --git a/spec/client/dashboard/models/house.test.js b/spec/client/dashboard/models/house.test.js deleted file mode 100644 index b44366b..0000000 --- a/spec/client/dashboard/models/house.test.js +++ /dev/null @@ -1,85 +0,0 @@ -"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_range: [ 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]); - }); - -}); diff --git a/spec/client/lib/databasable.test.js b/spec/client/lib/databasable.test.js new file mode 100644 index 0000000..12f093e --- /dev/null +++ b/spec/client/lib/databasable.test.js @@ -0,0 +1,65 @@ +import Loki from 'lokijs/src/lokijs'; + +import Databasable from './../../../client/lib/databasable'; + +class DbClass { + constructor(){ + Object.assign(this, Databasable); + } + + get lokijs_options(){ + return { + adapter: null + }; + } + + doSomethingWithCollection(){ + var db_class = this; + return db_class.collection('yadadb', 'yada_collection') + .then((collection)=>{ + db_class.collection = collection; + }) + .then(()=>{ + db_class.worked = db_class.collection instanceof Loki.Collection; + }); + } + +} + +var db_class; + +describe('Databasable', ()=>{ + beforeEach(()=>{ + db_class = new DbClass(); + }); + + describe('Databasable#accessDb', ()=>{ + it('should initiate a new database', (done)=>{ + db_class.accessDb('yadadb') + .then(()=>{ + expect(db_class.db instanceof Loki).toEqual(true); + done(); + }); + }); + }); + + describe('Databasable#collection', ()=>{ + it('should initiate a new database & collection', (done)=>{ + db_class.collection('yadadb', 'yada_collection') + .then((collection)=>{ + expect(db_class.db instanceof Loki).toEqual(true); + expect(collection instanceof Loki.Collection).toEqual(true); + done(); + }); + }); + it('works asynchronously', (done)=>{ + db_class.doSomethingWithCollection() + .then(()=>{ + expect(db_class.collection instanceof Loki.Collection).toEqual(true); + expect(db_class.worked).toEqual(true); + done(); + }) + }); + }); + +}); diff --git a/spec/client/models/house.test.js b/spec/client/models/house.test.js new file mode 100644 index 0000000..f8ce51a --- /dev/null +++ b/spec/client/models/house.test.js @@ -0,0 +1,76 @@ +"use strict"; + +import moment from 'moment-timezone'; +import House from './../../../client/models/house.js'; + +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, // 3 years before + data_until: data_until, + timezone: 'America/New_York' + }); + +describe('House#state', ()=>{ + + it('has no state after init', ()=>{ + expect(house.state).toEqual({}); + }); + +}); + +describe('house#verifyMonthState', ()=>{ + + it('verifies to data_until month and year by default', ()=>{ + var params = {}; + house.verifyMonthState(params); + expect(params.month).toEqual('Feb'); + expect(params.year).toEqual(2016); + }); + + it('verifies properly when passed valid params', ()=>{ + var params = { + month: 'Mar', + year: 2015 + }; + house.verifyMonthState(params); + expect(params.month).toEqual('Mar'); + expect(params.year).toEqual(2015); + }); + + it('corrects for params outside of data range', ()=>{ + var params = { + month: 'Mar', + year: 2006 + }; + house.verifyMonthState(params); + + expect(params.month).toEqual('Mar'); + expect(params.year).toEqual(2013); + }); + +}); +describe('House#verifyPowerRange', ()=>{ + + it('defaults to last four days of data', ()=>{ + var power_max = house.data.data_until, + power_min = power_max - House.MAX_POWER_RANGE, + power_range = house.verifyPowerRange([], {month: 'Feb', year: 2016}); + + expect(power_range).toEqual([power_min, power_max]); + }); + + it('otherwise verifies power range to max 4 day range', ()=>{ + var power_max = moment.tz({year: 2014, month: 9, day: 1}, 'America/New_York').endOf('month').unix(), + invalid_power_min = power_max - House.MAX_POWER_RANGE - 10, + valid_power_min = power_max - House.MAX_POWER_RANGE, + power_range = house.verifyPowerRange([invalid_power_min, power_max], { + month: 'Oct', + year: 2014 + }); + + expect(power_range).toEqual([valid_power_min, power_max]); + }); + +}); diff --git a/spec/shared/utils/date_range.test.js b/spec/shared/utils/date_range.test.js index cbe3dea..8d71552 100644 --- a/spec/shared/utils/date_range.test.js +++ b/spec/shared/utils/date_range.test.js @@ -421,4 +421,14 @@ describe('DateRange.addRange', ()=>{ }); }); + describe('overlapping in the middle', ()=>{ + it('should not return any new ranges', ()=>{ + var new_range = [date1, date2], + ranges = [[date1, date2], [date3, date4], [date5, date6]], + result = DateRange.addRange(new_range, ranges); + expect(result.gaps_filled).toEqual([]); + expect(result.new_ranges).toEqual([[date1, date2], [date3, date4], [date5, date6]]); + }); + }); + }); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json deleted file mode 100644 index bcaf67b..0000000 --- a/spec/support/jasmine.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "spec_dir": "spec", - "spec_files": [ - "**/*.test.js" - ], - "helpers": [ - "../node_modules/babel-core/register.js", - "helpers/**/*.js" - ], - "stopSpecOnExpectationFailure": false, - "random": false -}