create irradiance composite graph
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<rt-require dependency="./graph/graph.component" as="EnergyGraph"/>
|
||||
<rt-require dependency="./table/table.component" as="EnergyTable"/>
|
||||
<div id="energy_view">
|
||||
<div class="alert alert-warning" rt-if="this.loading_energy_data">
|
||||
Retrieving energy data...
|
||||
</div>
|
||||
<div rt-if="this.props.view === 'graph'">
|
||||
<h4>Select Data</h4>
|
||||
<div class="btn-group" role="group">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import c3 from 'c3';
|
||||
|
||||
import Templates from 'config/templates';
|
||||
import CalendarGridChart from './../../../d3/grid/calendar_grid';
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<th></th>
|
||||
<th>Day</th>
|
||||
<th>Consumption (kWh)</th>
|
||||
<th>Daily Mean Irradiance (W/m<sup>2</sup>)</th>
|
||||
<th>Production (kWh)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -11,8 +12,9 @@
|
||||
<tr rt-repeat="energy_datum in this.house.energy_data" key="{energy_datum.scoped_id}">
|
||||
<td></td>
|
||||
<td>{energy_datum.day_to_s}</td>
|
||||
<td>{energy_datum.consumption_to_s}</td>
|
||||
<td>{energy_datum.production_to_s}</td>
|
||||
<td>{energy_datum.consumption}</td>
|
||||
<td>{energy_datum.irradiance}</td>
|
||||
<td>{energy_datum.production}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
152
client/dashboard/irradiance/graph/graph.component.js
Normal file
152
client/dashboard/irradiance/graph/graph.component.js
Normal file
@@ -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;
|
||||
|
||||
1
client/dashboard/irradiance/graph/graph.rt
Normal file
1
client/dashboard/irradiance/graph/graph.rt
Normal file
@@ -0,0 +1 @@
|
||||
<div id="irradiance_graph"></div>
|
||||
18
client/dashboard/irradiance/irradiance.component.js
Normal file
18
client/dashboard/irradiance/irradiance.component.js
Normal file
@@ -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;
|
||||
13
client/dashboard/irradiance/irradiance.rt
Normal file
13
client/dashboard/irradiance/irradiance.rt
Normal file
@@ -0,0 +1,13 @@
|
||||
<rt-require dependency="./graph/graph.component" as="IrradianceGraph"/>
|
||||
<rt-require dependency="./table/table.component" as="IrradianceTable"/>
|
||||
<div id="irradiance_view">
|
||||
<h4>Irradiance</h4>
|
||||
<IrradianceGraph
|
||||
rt-if="this.props.view === 'graph'"
|
||||
state_manager="{this.state_manager}"
|
||||
date_interval="{this.props.date_interval}" />
|
||||
<IrradianceTable
|
||||
rt-if="this.props.view === 'table'"
|
||||
state_manager="{this.state_manager}"
|
||||
date_interval="{this.props.date_interval}" />
|
||||
</div>
|
||||
3
client/dashboard/irradiance/irradiance.scss
Normal file
3
client/dashboard/irradiance/irradiance.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
#irradiance_component {
|
||||
|
||||
}
|
||||
18
client/dashboard/irradiance/table/table.component.js
Normal file
18
client/dashboard/irradiance/table/table.component.js
Normal file
@@ -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;
|
||||
18
client/dashboard/irradiance/table/table.rt
Normal file
18
client/dashboard/irradiance/table/table.rt
Normal file
@@ -0,0 +1,18 @@
|
||||
<table id="irradiance_table" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Day</th>
|
||||
<th>House</th>
|
||||
<th>Daily Mean Irradiance (W/m<sup>2</sup>)</th>
|
||||
<th>Production (kWh)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody rt-repeat="day in Object.keys(this.state_manager.irradiance_data)" key="irradiance-date-{day}">
|
||||
<tr rt-repeat="energy_datum in this.state_manager.irradiance_data[day]" key="{energy_datum.scoped_id}">
|
||||
<td>{energy_datum.day_to_s}</td>
|
||||
<td>{energy_datum.house.data.name}</td>
|
||||
<td>{energy_datum.irradiance}</td>
|
||||
<td>{energy_datum.production}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<rt-require dependency="./../energy/energy.component" as="Energy"/>
|
||||
<rt-require dependency="./../irradiance/irradiance.component" as="Irradiance"/>
|
||||
<rt-require dependency="./../power/power.component" as="Power"/>
|
||||
<div id="layout">
|
||||
<div rt-if="!this.house" id="about">
|
||||
@@ -9,7 +10,7 @@
|
||||
<ul>
|
||||
<li>React</li>
|
||||
<li>React Templates</li>
|
||||
<li>React Router</li>
|
||||
<li>ReactJs History</li>
|
||||
<li>LokiJs - persisting API calls to indexedDb</li>
|
||||
<li>Webpack - hot mode developing and app bundling</li>
|
||||
<li>Babel - ES6 transpiler</li>
|
||||
@@ -22,15 +23,23 @@
|
||||
|
||||
<div class="alert alert-warning" rt-if="this.state.loading_houses">Retrieving houses...</div>
|
||||
|
||||
<h4>Select household:</h4>
|
||||
<select id="houses_select" rt-if="this.state.houses && this.state_manager" class="form-control" onChange="{this.setHouse.bind(this)}" value="{this.house_id}">
|
||||
<option rt-repeat="house in this.state.houses" value="{house.data.id}" key="{house.scoped_id}">{house.data.name}</option>
|
||||
</select>
|
||||
<div rt-if="this.dataset !== 'irradiance'">
|
||||
<h4>Select household:</h4>
|
||||
<select id="houses_select" rt-if="this.state.houses && this.state_manager" class="form-control" onChange="{this.setHouse.bind(this)}" value="{this.house_id}">
|
||||
<option rt-repeat="house in this.state.houses" value="{house.data.id}" key="{house.scoped_id}">{house.data.name}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button rt-if="this.house" onClick="{this.refreshData.bind(this)}" class="btn btn-xs btn-default">Refresh House Data</button>
|
||||
|
||||
<div rt-if="this.house">
|
||||
<div>
|
||||
<h4>Select dataset:</h4>
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
data-param="dataset"
|
||||
data-value="power"
|
||||
rt-class="{active: this.state.dataset === 'power'}"
|
||||
onClick="{this.setParam.bind(this)}"
|
||||
type="button" class="btn btn-primary">15-minute Power Statistics</button>
|
||||
<button
|
||||
data-param="dataset"
|
||||
data-value="energy"
|
||||
@@ -39,10 +48,10 @@
|
||||
type="button" class="btn btn-primary">Daily Energy Statistics</button>
|
||||
<button
|
||||
data-param="dataset"
|
||||
data-value="power"
|
||||
rt-class="{active: this.state.dataset === 'power'}"
|
||||
data-value="irradiance"
|
||||
rt-class="{active: this.state.dataset === 'irradiance'}"
|
||||
onClick="{this.setParam.bind(this)}"
|
||||
type="button" class="btn btn-primary">15-minute Power Statistics</button>
|
||||
type="button" class="btn btn-primary">Daily Mean Irradiance</button>
|
||||
</div>
|
||||
|
||||
<h4>View as:</h4>
|
||||
@@ -72,26 +81,44 @@
|
||||
class="btn-info btn btn-sm"
|
||||
rt-class="{active: year == this.state.year}"
|
||||
onClick="{this.setParam.bind(this)}">{year}</button>
|
||||
</div><br/>
|
||||
<div class="btn-group" rt-if="this.state.dataset === 'power' || this.state.dataset === 'irradiance'">
|
||||
<button
|
||||
rt-repeat="month in this.house.availableMonths(this.state.year)"
|
||||
data-param="month"
|
||||
data-value="{month}"
|
||||
key="data-month-{month}"
|
||||
class="btn-warning btn btn-sm"
|
||||
rt-class="{active: month === this.state.month}"
|
||||
onClick="{this.setParam.bind(this)}">{month}</button>
|
||||
</div><br/>
|
||||
<div id="date_interval"></div>
|
||||
|
||||
<div class="alert alert-warning" rt-if="this.state.loading_data">
|
||||
Retrieving {this.state.loading_data} data...
|
||||
</div>
|
||||
</div><br/>
|
||||
|
||||
<Energy
|
||||
rt-if="this.should_show_energy_data"
|
||||
house="{this.state.house}"
|
||||
loading_energy_data="{this.state.loading_energy_data}"
|
||||
state_manager="{this.state_manager}"
|
||||
view="{this.state.view}"
|
||||
graph_attr="{this.state.graph_attr}"
|
||||
year="{this.state.year}" />
|
||||
<Irradiance
|
||||
rt-if="this.state.dataset === 'irradiance'"
|
||||
view="{this.state.view}"
|
||||
date_interval="{this.state.date_interval}"
|
||||
state_manager="{this.state_manager}" />
|
||||
<Power
|
||||
rt-if="this.should_show_power_data"
|
||||
house="{this.state.house}"
|
||||
loading_power_data="{this.state.loading_power_data}"
|
||||
state_manager="{this.state_manager}"
|
||||
view="{this.state.view}"
|
||||
month="{this.state.month}"
|
||||
year="{this.state.year}"
|
||||
power_range="{this.state.power_range}" />
|
||||
date_interval="{this.state.date_interval}" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,33 +1,18 @@
|
||||
<rt-require dependency="./graph/graph.component" as="PowerGraph"/>
|
||||
<rt-require dependency="./table/table.component" as="PowerTable"/>
|
||||
<div id="power_view">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
rt-if="this.house"
|
||||
rt-repeat="month in this.house.availableMonths()"
|
||||
data-param="month"
|
||||
data-value="{month}"
|
||||
key="data-month-{month}"
|
||||
class="btn-warning btn btn-sm"
|
||||
rt-class="{active: month === this.house.state.month}"
|
||||
onClick="{this.setParam.bind(this)}">{month}</button>
|
||||
</div>
|
||||
<div class="alert alert-warning" rt-if="this.loading_power_data">
|
||||
Retrieving power data...
|
||||
</div>
|
||||
<div id="power_date_setter"></div>
|
||||
<PowerGraph
|
||||
rt-if="this.props.view === 'graph'"
|
||||
state_manager="{this.props.state_manager}"
|
||||
house="{this.props.house}"
|
||||
month="{this.props.month}"
|
||||
year="{this.props.year}"
|
||||
power_range="{this.props.power_range}" ></PowerGraph>
|
||||
date_interval="{this.props.date_interval}" ></PowerGraph>
|
||||
<PowerTable
|
||||
rt-if="this.props.view === 'table'"
|
||||
state_manager="{this.props.state_manager}"
|
||||
house="{this.props.house}"
|
||||
month="{this.props.month}"
|
||||
year="{this.props.year}"
|
||||
power_range="{this.props.power_range}" ></PowerTable>
|
||||
date_interval="{this.props.date_interval}" ></PowerTable>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -48,6 +48,9 @@ module.exports = function (config) {
|
||||
api: __dirname + '/client/api/development',
|
||||
config: __dirname + '/client/config/development'
|
||||
}
|
||||
},
|
||||
node: {
|
||||
fs: "empty"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]]);
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
|
||||
});
|
||||
65
spec/client/lib/databasable.test.js
Normal file
65
spec/client/lib/databasable.test.js
Normal file
@@ -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();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
76
spec/client/models/house.test.js
Normal file
76
spec/client/models/house.test.js
Normal file
@@ -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]);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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]]);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"spec_dir": "spec",
|
||||
"spec_files": [
|
||||
"**/*.test.js"
|
||||
],
|
||||
"helpers": [
|
||||
"../node_modules/babel-core/register.js",
|
||||
"helpers/**/*.js"
|
||||
],
|
||||
"stopSpecOnExpectationFailure": false,
|
||||
"random": false
|
||||
}
|
||||
Reference in New Issue
Block a user