From df3152443a97ee90acb9afc4e03bf314d1043fa3 Mon Sep 17 00:00:00 2001 From: Eric Hulburd Date: Mon, 22 Feb 2016 13:45:43 -0600 Subject: [PATCH] polish energy/power data view interactions --- client/api/energy_data.js | 12 +- client/d3/grid/calendar_grid.js | 13 +- client/d3/line/spline_stack.js | 10 +- client/dashboard/energy/energy.js | 31 +++- client/dashboard/layout/layout.js | 21 ++- client/dashboard/layout/layout.rt | 22 ++- client/dashboard/layout/layout.rt.js | 20 ++- client/dashboard/power/power.js | 60 ++++++-- client/dashboard/power/power.rt | 13 +- client/dashboard/power/power.rt.js | 20 ++- client/lib/databasable.js | 22 +-- client/models/energy_datum.js | 9 +- client/models/house.js | 221 ++++++++++++++++++--------- client/models/power_datum.js | 10 +- server/helpers/api_helper.js | 8 +- server/models/energy_datum.js | 2 + 16 files changed, 338 insertions(+), 156 deletions(-) diff --git a/client/api/energy_data.js b/client/api/energy_data.js index 766808c..2c0706c 100644 --- a/client/api/energy_data.js +++ b/client/api/energy_data.js @@ -2,19 +2,9 @@ import extend from 'extend'; const ENDPOINT = '/data/v1/energy'; -// send all date parameters as unix timestamps; class EnergyDataApi { - static index(house, params){ - params = extend({ - }, params); - if (params.dates){ - params.dates = params.dates.map((date_range)=>{ - if (date_range[0]) date_range[0] = date_range[0].unix(); - if (date_range[1]) date_range[1] = date_range[1].unix(); - return [date_range[0], date_range[1]]; - }) - } + static index(params){ return jQuery.ajax({ url: ENDPOINT + '?' + jQuery.param(params), type: 'GET', diff --git a/client/d3/grid/calendar_grid.js b/client/d3/grid/calendar_grid.js index 0a4d4eb..2e9170c 100644 --- a/client/d3/grid/calendar_grid.js +++ b/client/d3/grid/calendar_grid.js @@ -6,7 +6,7 @@ class CalendarGridChart extends Chart{ get chart_options(){ var chart = this; - return extend(Chart.DEFAULTS, { + return extend(Object.assign({}, Chart.DEFAULTS), { margin: {top: 30, left: 150, bottom: 0, right: 0}, grid_padding: 0.05, parse_date_format: '%Y-%m-%d', @@ -103,6 +103,7 @@ class CalendarGridChart extends Chart{ drawData(data){ var grid_chart = this; + grid_chart.i = grid_chart.i || 1; data = grid_chart.serializeData(data); // calibrate axes @@ -118,9 +119,9 @@ class CalendarGridChart extends Chart{ var grid_units = grid_chart.svg.selectAll(".d3-chart-grid-unit") .data(data.values); - grid_chart.applyData(data, grid_units.enter().append("rect")); - grid_chart.applyData(data, grid_units.transition()); grid_units.exit().remove(); + grid_chart.applyData(data, grid_units.enter().append("rect")); + grid_chart.applyData(data, grid_units); } // helper method for drawData. @@ -128,7 +129,7 @@ class CalendarGridChart extends Chart{ var grid_chart = this, series_class = "d3-chart-grid-unit " + data.css_class; elements - .attr("class", function(d){ return series_class; }) + .attr("class", series_class) .attr("y", function(d) { var bottom = grid_chart.y_scale(grid_chart.toMonthString(d)), middle = grid_chart.y_scale.rangeBand() / 2 - grid_chart.grid_unit_size / 2; @@ -140,10 +141,10 @@ class CalendarGridChart extends Chart{ }) .attr("width", function(d) { return grid_chart.grid_unit_size; }) .attr('fill', grid_chart.color) - .attr("opacity", function(d) { return grid_chart.applyOpacity(grid_chart.rangeValue(d), data.range); }); + .attr("opacity", function(d) { return grid_chart.calculateOpacity(75, data.range); }); } - applyOpacity(value, range){ + calculateOpacity(value, range){ return Math.max(0, Math.min(1, 1 - (range.max - (value - range.min)) / range.diff)); }; diff --git a/client/d3/line/spline_stack.js b/client/d3/line/spline_stack.js index f76bbf1..390c7c7 100644 --- a/client/d3/line/spline_stack.js +++ b/client/d3/line/spline_stack.js @@ -6,7 +6,7 @@ const INTERPOLATION = 'cardinal'; class SplineStackChart extends LineChart { get chart_options(){ - return Object.assign(LineChart.DEFAULTS, { + return Object.assign(Object.assign({}, LineChart.DEFAULTS), { interpolation: INTERPOLATION }); } @@ -63,7 +63,7 @@ class SplineStackChart extends LineChart { var stack = spline_stack.svg.selectAll(".d3-chart-spline-stack") .data(data.series); - [stack.enter().append("path"), stack.transition()].forEach((paths)=>{ + [stack.enter().append("path"), stack].forEach((paths)=>{ spline_stack.applyData(paths); }); stack.exit().remove(); @@ -72,7 +72,7 @@ class SplineStackChart extends LineChart { data.series.forEach((series)=>{ var dots = spline_stack.svg.selectAll(".d3-chart-spline-dot." + series.css_class) .data(series.values); - [dots.enter().append("circle"), dots.transition()].forEach((circles)=>{ + [dots.enter().append("circle"), dots].forEach((circles)=>{ spline_stack.applyDots(series, circles); }); dots.exit().remove(); @@ -83,7 +83,7 @@ class SplineStackChart extends LineChart { applyData(paths){ var spline_stack = this; paths - .attr("class", function(series){"d3-chart-spline-stack " + series.css_class;}) + .attr("class", function(series){ return "d3-chart-spline-stack " + series.css_class;}) .attr("d", function(series){ return spline_stack.fnArea(series.values); }) .style("fill", function(series){ return spline_stack.fnColor(series.title); }) .attr('opacity', 1); @@ -92,7 +92,7 @@ class SplineStackChart extends LineChart { applyDots(series, circles){ var spline_stack = this; circles - .attr('class', 'd3-chart-spline-dot' + series.css_class) + .attr('class', 'd3-chart-spline-dot ' + series.css_class) .attr("r", 2) .attr("cx", function(d, i){ return spline_stack.x_scale(d.x); }) .attr("cy", function(d, i){ return spline_stack.y_scale(d.y + d.y0); }) diff --git a/client/dashboard/energy/energy.js b/client/dashboard/energy/energy.js index d06df2d..6d9de98 100644 --- a/client/dashboard/energy/energy.js +++ b/client/dashboard/energy/energy.js @@ -28,9 +28,14 @@ var Energy = React.createClass({ }); }, + componentWillUnmount: function(){ + var energy = this; + energy.destroyGraph(); + }, + componentWillReceiveProps: function(new_props){ var energy = this; - if (new_props.house !== energy.state.house){ + if (new_props.house !== energy.props.house){ energy.setState({loading_data: true}); new_props.house.setEnergyData().then(()=>{ energy.setState({loading_data: false}); @@ -44,6 +49,9 @@ var Energy = React.createClass({ var energy = this, house = energy.props.house; if (prev_props.view !== 'graph' && energy.props.view === 'graph') energy.initGraph(); + if (prev_props.year !== energy.props.year){ + energy.updateCurrentMonth(); + } }, setGraphAttr: function(event){ @@ -65,17 +73,17 @@ var Energy = React.createClass({ energy.graph = new CalendarGridChart({ container: '#energy_graph', outer_width: 800, - outer_height: 200, + outer_height: 300, date_attr: 'day', color: '#0404B4', - toDate: (energy_datum)=>{ return energy_datum.data.day.toDate(); } + toDate: (energy_datum)=>{ return energy_datum.day_to_date; } }); jQuery('#energy_graph').tooltip({ selector: '.d3-chart-grid-unit', container: 'body', title: function(){ var energy_datum = this.__data__, - date_s = d3.time.format('%a %b %d, %Y')(energy_datum.data.day.toDate()), + date_s = d3.time.format('%a %b %d, %Y')(energy_datum.day_to_date), range_value = `${Math.round(energy_datum.data[energy.state.graph_attr])} kWh`; return `${date_s}: ${range_value}`; } @@ -103,6 +111,21 @@ var Energy = React.createClass({ energy.graph = undefined; }, + updateCurrentMonth: function(){ + var energy = this, + house = energy.props.house; + house.setEnergyData() + .then(()=>{ + if (energy.props.view === 'graph'){ + // no update necessary since year already updated in layout.rt. + energy.updateGraph(); + } else { + // force update to render correct data in table. + energy.forceUpdate(); + } + }); + }, + render: function() { return energyRt.call(this); } diff --git a/client/dashboard/layout/layout.js b/client/dashboard/layout/layout.js index aeaca3e..24c3de9 100644 --- a/client/dashboard/layout/layout.js +++ b/client/dashboard/layout/layout.js @@ -24,16 +24,23 @@ var Layout = React.createClass({ var layout = this; // window.addEventListener('resize', this.handleResize); House.ensureHouses().then((houses)=>{ - layout.setState({houses: houses, house: houses[0], requesting_data: false}); + layout.setState({ + houses: houses, + house: houses[0], + requesting_data: false, + month: houses[0].current_month, + year: houses[0].current_year + }); }); }, setHouse: function(event){ var layout = this, house_id = event.target.value, + old_house = layout.state.house, house = layout.state.houses.find((house)=>{ return house.data.id == house_id }); layout.setState({house: house}, ()=>{ - house.closeDb(); + old_house.closeDb(); }); }, @@ -50,6 +57,16 @@ var Layout = React.createClass({ layout.setState({dataset: dataset}); }, + setYear: function(event){ + var layout = this, + year = event.target.dataset.value, + house = layout.state.house; + if (year != house.current_year){ + house.setYear(year); + layout.setState({year: year}); + } + }, + refreshData: function(){ var layout = this, houses = layout.state.houses, diff --git a/client/dashboard/layout/layout.rt b/client/dashboard/layout/layout.rt index 229cfc7..94c7255 100644 --- a/client/dashboard/layout/layout.rt +++ b/client/dashboard/layout/layout.rt @@ -37,6 +37,24 @@ type="button" class="btn btn-primary">Table - - +

Select dates:

+
+ +

+ + + diff --git a/client/dashboard/layout/layout.rt.js b/client/dashboard/layout/layout.rt.js index 6230013..18ed882 100644 --- a/client/dashboard/layout/layout.rt.js +++ b/client/dashboard/layout/layout.rt.js @@ -8,6 +8,14 @@ function repeatHouse1(house, houseIndex) { 'key': house.scoped_id }, house.data.name); } +function repeatYear2(year, yearIndex) { + return React.createElement('button', { + 'data-value': year, + 'key': 'data-year-' + year, + 'className': 'btn-info btn btn-sm' + ' ' + _.keys(_.pick({ active: year == this.state.house.current_year }, _.identity)).join(' '), + 'onClick': this.setYear + }, year); +} export default function () { return React.createElement('div', { 'id': 'layout' }, this.state.requesting_data ? React.createElement('div', { 'className': 'alert alert-warning' }, 'Retrieving houses...') : null, React.createElement('h4', {}, 'Select household:'), this.state.houses ? React.createElement.apply(this, [ 'select', @@ -45,11 +53,17 @@ export default function () { 'className': _.keys(_.pick({ active: this.state.view === 'table' }, _.identity)).join(' ') + ' ' + 'btn btn-primary', 'onClick': this.setView, 'type': 'button' - }, 'Table')), this.state.house && this.state.dataset === 'energy' ? React.createElement(Energy, { + }, 'Table')), React.createElement('h4', {}, 'Select dates:'), React.createElement.apply(this, [ + 'div', + { 'className': 'btn-group' }, + this.state.house ? _.map(this.state.house.years, repeatYear2.bind(this)) : null + ]), React.createElement('br', {}), this.state.house && this.state.dataset === 'energy' ? React.createElement(Energy, { 'house': this.state.house, - 'view': this.state.view + 'view': this.state.view, + 'year': this.state.year }) : null, this.state.house && this.state.dataset === 'power' ? React.createElement(Power, { 'house': this.state.house, - 'view': this.state.view + 'view': this.state.view, + 'year': this.state.year }) : null); }; \ No newline at end of file diff --git a/client/dashboard/power/power.js b/client/dashboard/power/power.js index ed49eba..10141a9 100644 --- a/client/dashboard/power/power.js +++ b/client/dashboard/power/power.js @@ -34,15 +34,19 @@ var Power = React.createClass({ }); }, + componentWillUnmount: function(){ + var power = this; + power.destroyGraph(); + }, + componentWillReceiveProps: function(new_props){ var power = this; - if (new_props.house !== power.state.house){ + if (new_props.house !== power.props.house){ // house will change. power.setState({loading_data: true}); new_props.house.setPowerData().then(()=>{ power.setState({loading_data: false}); if (power.props.view === 'graph'){ - power.initDateRange(); power.initGraph(); } }); @@ -56,9 +60,13 @@ var Power = React.createClass({ house = power.props.house; // view has changed from graph to table. if (prev_props.view !== 'graph' && power.props.view === 'graph'){ - power.initDateRange(); power.initGraph(); } + if (prev_props.house !== house) power.initDateRange(); + var need_update = false; + if (prev_props.year !== power.props.year){ + power.updateCurrentMonth(); + } }, initGraph: function(){ @@ -75,7 +83,7 @@ var Power = React.createClass({ range_attr: 'y', include_dots: true, titleizeDatum: (series, d)=>{ - return series.title + '
' + Math.round(d.y) + ' W
' + moment.tz(d.x.getTime(), house.timezone).format('MMM D [at] HH:mm') + return series.title + '
' + Math.round(d.y) + ' W
' + house.formatDate(d.power_datum.data.time, 'MMM D [at] HH:mm'); } }); jQuery('#power_graph').tooltip({ @@ -97,7 +105,8 @@ var Power = React.createClass({ title: 'Net Power Consumption', values: house.power_data.map((power_datum)=>{ return { - x: house.toDate(power_datum.data.time), + power_datum: power_datum, + x: power_datum.time_to_date, y: Math.max(0, power_datum.data.consumption - power_datum.data.production) } }) }, @@ -105,7 +114,8 @@ var Power = React.createClass({ title: 'Power Production', values: house.power_data.map((power_datum)=>{ return { - x: house.toDate(power_datum.data.time), + power_datum: power_datum, + x: power_datum.time_to_date, y: power_datum.data.production } }) }; @@ -124,7 +134,7 @@ var Power = React.createClass({ container: '#power_date_setter', outer_height: 100, maxDelta: function(changed_date, other_date){ - if (Math.abs(changed_date.getTime() - other_date.getTime()) > 3600 * 24 * 7 * 1000){ + 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 { @@ -141,28 +151,46 @@ var Power = React.createClass({ house.power_date_range = [Math.round(min.getTime() / 1000), Math.round(max.getTime() / 1000)] house.setPowerData() .then(()=>{ - power.updateGraph(); + if (power.props.view === 'graph') power.updateGraph(); + else power.forceUpdate(); }); }, 500); }; - var four_week_start = house.data.data_until - 3600 * 24 * 28, - data_from = Math.max(four_week_start, house.data.data_from); power.date_range_slider.drawData({ - abs_min: house.toDate(four_week_start), - abs_max: house.toDate(house.data.data_until), - current_min: house.toDate(house.default_power_start), - current_max: house.toDate(house.default_power_end) + abs_min: house.current_month_moment.toDate(), + abs_max: house.end_of_current_data_moment.toDate(), + current_min: house.toDate(house.power_date_range[0]), + current_max: house.toDate(house.power_date_range[1]) }); }, destroyGraph: function(){ var power = this; - document.getElementById('power_date_setter').innerHTML = ''; document.getElementById('power_graph').innerHTML = ''; - power.date_range_slider = undefined; power.graph = undefined; }, + setMonth: function(event){ + var power = this, + house = power.props.house, + month = event.target.dataset.value; + if (month !== house.current_month){ + var need_update = house.setMonth(month); + if (need_update) power.updateCurrentMonth(); + } + }, + + updateCurrentMonth: function(){ + var power = this, + house = power.props.house; + power.initDateRange(); + house.setPowerData() + .then(()=>{ + power.forceUpdate(); + if (power.props.view === 'graph') power.updateGraph(); + }); + }, + render: function() { return powerRt.call(this); } diff --git a/client/dashboard/power/power.rt b/client/dashboard/power/power.rt index 9d27f81..f075273 100644 --- a/client/dashboard/power/power.rt +++ b/client/dashboard/power/power.rt @@ -1,8 +1,17 @@
+
+ +
Retrieving power data for the {this.props.house.name} household...
-

Select dates:

@@ -15,7 +24,7 @@ - + diff --git a/client/dashboard/power/power.rt.js b/client/dashboard/power/power.rt.js index d349a29..7927c9a 100644 --- a/client/dashboard/power/power.rt.js +++ b/client/dashboard/power/power.rt.js @@ -1,12 +1,24 @@ import React from 'react'; import _ from 'lodash'; -function repeatPower_datum1(power_datum, power_datumIndex) { - return React.createElement('tr', { 'key': power_datum.scoped_id }, React.createElement('td', {}), React.createElement('td', {}, power_datum.time_to_s), React.createElement('td', {}, power_datum.consumption_to_s), React.createElement('td', {}, power_datum.production_to_s)); +function repeatMonth1(month, monthIndex) { + return React.createElement('button', { + 'data-value': month, + 'key': 'data-month-' + month, + 'className': 'btn-warning btn btn-sm' + ' ' + _.keys(_.pick({ active: month === this.props.house.current_month }, _.identity)).join(' '), + 'onClick': this.setMonth + }, month); +} +function repeatPower_datum2(power_datum, power_datumIndex) { + return React.createElement('tr', { 'key': power_datum.scoped_id }, React.createElement('td', {}, power_datum.data.id), React.createElement('td', {}, power_datum.time_to_s), React.createElement('td', {}, power_datum.consumption_to_s), React.createElement('td', {}, power_datum.production_to_s)); } export default function () { - return React.createElement('div', { 'id': 'power_view' }, this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data for the ', this.props.house.name, ' household...\n ') : null, React.createElement('h4', {}, 'Select dates:'), React.createElement('div', { 'id': 'power_date_setter' }), this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Time'), React.createElement('th', {}, 'Consumption (W)'), React.createElement('th', {}, 'Production (W)'))), React.createElement.apply(this, [ + return React.createElement('div', { 'id': 'power_view' }, React.createElement.apply(this, [ + 'div', + { 'className': 'btn-group' }, + this.props.house ? _.map(this.props.house.availableMonths(), repeatMonth1.bind(this)) : null + ]), this.state.loading_data ? React.createElement('div', { 'className': 'alert alert-warning' }, '\n Retrieving power data for the ', this.props.house.name, ' household...\n ') : null, React.createElement('div', { 'id': 'power_date_setter' }), this.props.view === 'table' ? React.createElement('table', { 'className': 'table' }, React.createElement('thead', {}, React.createElement('tr', {}, React.createElement('th', {}), React.createElement('th', {}, 'Time'), React.createElement('th', {}, 'Consumption (W)'), React.createElement('th', {}, 'Production (W)'))), React.createElement.apply(this, [ 'tbody', {}, - _.map(this.props.house.power_data, repeatPower_datum1.bind(this)) + _.map(this.props.house.power_data, repeatPower_datum2.bind(this)) ])) : null, this.props.view === 'graph' ? React.createElement('div', { 'id': 'power_graph' }) : null); }; \ No newline at end of file diff --git a/client/lib/databasable.js b/client/lib/databasable.js index 9d50e32..abe6b67 100644 --- a/client/lib/databasable.js +++ b/client/lib/databasable.js @@ -32,18 +32,18 @@ var databasable = { } }, - collection: function(collection_name, options){ + collection: function(db_name, collection_name, options){ var databasable = this; - return databasable.accessDb() + return databasable.accessDb(db_name) .then((db)=>{ var collection = db.getCollection(collection_name) if (!collection){ collection = db.addCollection(collection_name, options); - } - if (options && options.unique_indices){ - options.unique_indices.forEach((field)=>{ - collection.ensureUniqueIndex(field); - }); + if (options && options.unique_indices){ + options.unique_indices.forEach((field)=>{ + collection.ensureUniqueIndex(field); + }); + } } return collection; }); @@ -55,12 +55,12 @@ var databasable = { var start_condition = {}, end_condition = {}; date_params['$and'] = [start_condition, end_condition]; - start_condition[attr] = {'$gt': range[0]}; - end_condition[attr] = {'$lt': range[1]}; + start_condition[attr] = {'$gte': range[0]}; + end_condition[attr] = {'$lte': range[1]}; } else if (range[0] !== undefined) { - date_params[attr] = {'$gt': range[0]} + date_params[attr] = {'$gte': range[0]} } else if (range[1] !== undefined) { - date_params[attr] = {'$lt': range[1]} + date_params[attr] = {'$lte': range[1]} } return date_params; } diff --git a/client/models/energy_datum.js b/client/models/energy_datum.js index d92f062..afc5df4 100644 --- a/client/models/energy_datum.js +++ b/client/models/energy_datum.js @@ -10,7 +10,6 @@ class EnergyDatum { constructor(data, house){ var energy_datum = this; energy_datum.house = house; - data.day = moment.tz(data.day, house.data.timezone); energy_datum.data = data; } @@ -18,16 +17,16 @@ class EnergyDatum { return `energy-datum-${this.data.id}`; } + // returns a datestamp that has the client timezone, but actually is house local time. get day_to_date(){ var energy_datum = this, - moment_tz = moment.tz(energy_datum.data.day, energy_datum.house.data.timezone); - // will have to do some additional math here to account for local offset. - return moment(moment_tz.toArray()).toDate(); + house = energy_datum.house; + return house.toDate(energy_datum.data.day); } get day_to_s(){ var energy_datum = this; - return energy_datum.data.day.format('YYYY-MM-DD'); + return moment.tz(energy_datum.data.day * 1000, energy_datum.house.data.timezone).format('YYYY-MM-DD'); } get consumption_to_s(){ diff --git a/client/models/house.js b/client/models/house.js index 2b27dc2..abfa12a 100644 --- a/client/models/house.js +++ b/client/models/house.js @@ -12,6 +12,8 @@ import MathUtil from './../../shared/utils/math'; import DateRange from './../../shared/utils/date_range'; import Databasable from './../lib/databasable'; +const NAME = 'House'; + class House { // must be initiated with a dataset already in Loki database (not directly JSON). @@ -19,69 +21,123 @@ class House { var house = this; house.data = data; Object.assign(house, Databasable); - house.power_date_range = [house.default_power_start, house.default_power_end]; + + var n_years = house.data_until_moment.year() - house.data_from_moment.year() + 1; + house.years = []; + for (var year=house.data_from_moment.year(); year<=house.data_until_moment.year(); year+=1){ + house.years.push(year); + } + house.current_month = house.data_until_moment.format('MMM'); + house.current_year = house.data_until_moment.year(); + house.setCurrentMonthMoment(); + } + + get data_from_moment(){ + var house = this; + return moment.tz(house.data.data_from * 1000, house.data.timezone); + } + + get data_until_moment(){ + var house = this; + return moment.tz(house.data.data_until * 1000, house.data.timezone); + } + + get end_of_current_data_moment(){ + var house = this, + end_of_month = house.current_month_moment.clone().endOf('month'); + return end_of_month > house.data_until_moment ? house.data_until_moment : end_of_month; } get scoped_id(){ return `house-${this.data.id}`; } - get default_power_start(){ - var house = this; - // 3600 * 24 seconds * 4 = 4 days. - return house.data.data_until - 3600 * 24 * 4; + availableMonths(){ + var house = this, + all_months = moment.monthsShort(), + year = house.current_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')){ + return all_months.slice(0, house.data_until_moment.month() + 1); + } else { + return all_months; + } } - get default_power_end(){ + setYear(year){ var house = this; - return house.data.data_until; + house.current_year = year; + return house.setCurrentMonthMoment(); + } + + setMonth(month){ + var house = this; + house.current_month = month; + return house.setCurrentMonthMoment(); + } + + setCurrentMonthMoment(){ + var house = this, + month_i = moment.monthsShort().indexOf(house.current_month), + new_month_moment = moment.tz({year: house.current_year, month: month_i, day: 1}, house.data.timezone).startOf('month'); + if (!house.current_month_moment || new_month_moment.unix() !== house.current_month_moment.unix()){ + house.current_month_moment = new_month_moment; + house.power_date_range = [house.end_of_current_data_moment.clone().subtract(4, 'days').unix(), house.end_of_current_data_moment.unix()]; + house.energy_date_range = [house.end_of_current_data_moment.clone().startOf('year').unix(), house.end_of_current_data_moment.clone().endOf('year').unix()] + return true; + } + return false; + } + + offset_diff(unix){ + var house = this, + tz = moment.tz.zone(house.data.timezone); + return (new Date().getTimezoneOffset() - tz.offset(unix * 1000)) * 60; } toDate(unix){ var house = this; - return moment.tz(unix * 1000, house.data.timezone).toDate(); + return new Date((unix + house.offset_diff(unix)) * 1000); + } + + formatDate(unix, format){ + var house = this; + return moment.tz(unix * 1000, house.data.timezone).format(format) } save(){ var house = this; - return House.collection(House.NAME) + return House.collection(House.NAME, House.NAME) .then((house_collection)=>{ house_collection.update(house.data); return House.db.save(); }); } - setPowerData(opts){ + setPowerData(){ var house = this; - opts = Object.assign({ - dates: house.power_date_range - }, opts || {}); - return house.collection(PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS) + return house.collection(house.scoped_id, PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS) .then((power_collection)=>{ - return house.ensurePowerData(opts) + return house.ensurePowerData() .then(()=>{ - var params = house.rangeToLokiParams('time', opts.dates); + var params = house.rangeToLokiParams('time', house.power_date_range); house.power_data = power_collection.find(params) .sort((pd1, pd2)=>{ if (pd1.time === pd2.time) return 0; if (pd1.time > pd2.time) return 1; if (pd1.time < pd2.time) return -1; }) - .map((data)=>{ return new PowerDatum(data, house); }) + .map((data)=>{ return new PowerDatum(data, house); }); }); }); } - ensurePowerData(opts){ - opts = extend({ - start_date: undefined, - end_date: undefined - }, opts || {}); + ensurePowerData(){ var house = this, - existing_ranges = house.data.power_datum_ranges || [], query_ranges; - query_ranges = DateRange.addRange(opts.dates, existing_ranges); + query_ranges = DateRange.addRange(house.power_date_range, house.data.power_datum_ranges || []); if (query_ranges.gaps_filled.length > 0){ var params = {dates: query_ranges.gaps_filled}; return house.getPowerData(params) @@ -95,60 +151,42 @@ class House { getPowerData(params){ var house = this; params.house_id = house.data.id; - return PowerDataApi.index(params) - .then((power_data)=>{ - return house.collection(PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS) - .then((power_collection)=>{ - power_collection.insert(power_data); - house.db.save(); - }); - }) - } - - clearData(){ - var house = this; - return new Promise((fnResolve, fnReject)=>{ - house.collection(PowerDatum.NAME) - .then((power_collection)=>{ - power_collection.removeWhere({}); - house.db.save(()=>{ - House.collection(House.NAME) - .then((house_collection)=>{ - house_collection.remove(house.data); - House.db.save(()=>{ - fnResolve(); - }) - }); - }); - }); - }); - } - - setEnergyData(opts){ - var house = this; - return house.ensureEnergyData(opts) - .then(()=>{ - return house.collection(EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) - .then((energy_collection)=>{ - var params = house.rangeToLokiParams('day', [opts.start_date, opts.end_date]); - house.energy_data = energy_collection.find(params).map((data)=>{ return new EnergyDatum(data, house); }) + return house.collection(house.scoped_id, PowerDatum.NAME, PowerDatum.COLLECTION_OPTIONS) + .then((power_collection)=>{ + return PowerDataApi.index(params) + .then((power_data)=>{ + power_collection.insert(power_data); + house.db.save(); }); }) } - ensureEnergyData(opts){ - opts = extend({ - start_date: undefined, - end_date: undefined - }, opts || {}); + setEnergyData(){ + var house = this; + return house.collection(house.scoped_id, EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) + .then((energy_collection)=>{ + return house.ensureEnergyData() + .then(()=>{ + var params = house.rangeToLokiParams('day', house.energy_date_range); + house.energy_data = energy_collection.find(params) + .sort((pd1, pd2)=>{ + if (pd1.day === pd2.day) return 0; + if (pd1.day > pd2.day) return 1; + if (pd1.day < pd2.day) return -1; + }) + .map((data)=>{ return new EnergyDatum(data, house); }); + }); + }); + } + + ensureEnergyData(){ var house = this, - query_ranges = DateRange.addRange([opts.start_date, opts.end_date], house.data.energy_datum_ranges); - + query_ranges = DateRange.addRange(house.energy_date_range, house.data.energy_datum_ranges || []); if (query_ranges.gaps_filled.length > 0){ - house.getEnergyData({dates: query_ranges.gaps_filled}) + return house.getEnergyData({dates: query_ranges.gaps_filled}) .then(()=>{ house.data.energy_datum_ranges = query_ranges.new_ranges; - return house.save(); + house.save(); }); } else { return Promise.resolve(); } } @@ -156,18 +194,47 @@ class House { getEnergyData(params){ var house = this; params.house_id = house.data.id; - return EnergyDataApi.index(params) - .then((energy_data)=>{ - return house.collection(EnergyDatum.NAME, EnergyDatum.COLLECTION_OPTIONS) - .then((energy_collection)=>{ + 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); - return house.db.save(); + house.db.save(); }); }) } + // removes all energy and power data from LokiJs (memory and persisted) database. + clearData(){ + var house = this, + all = [ + new Promise((fnResolve, fnReject)=>{ + house.collection(house.scoped_id, PowerDatum.NAME) + .then((power_collection)=>{ + power_collection.removeWhere({}); + house.db.save(fnResolve); + }); + }), + new Promise((fnResolve, fnReject)=>{ + house.collection(house.scoped_id, EnergyDatum.NAME) + .then((energy_collection)=>{ + energy_collection.removeWhere({}); + house.db.save(fnResolve); + }); + }), + new Promise((fnResolve, fnReject)=>{ + House.collection(House.NAME, House.NAME) + .then((house_collection)=>{ + house_collection.remove(house.data); + House.db.save(fnResolve); + }); + }) + ] + return Promise.all(all) + } + static ensureHouses(ids){ - return House.collection(House.NAME) + return House.collection(House.NAME, House.NAME) .then((house_collection)=>{ var houses_data = ids ? house_collection.find({id: {$in: ids}}) : house_collection.find(); if (!ids && houses_data.length === 0 || ids && houses_data.length !== ids.length){ @@ -188,5 +255,7 @@ class House { } +House.NAME = NAME; + Object.assign(House, Databasable); export default House; diff --git a/client/models/power_datum.js b/client/models/power_datum.js index 0b90ed9..c53a7ac 100644 --- a/client/models/power_datum.js +++ b/client/models/power_datum.js @@ -20,14 +20,14 @@ class PowerDatum { get time_to_date(){ var power_datum = this, - moment_tz = moment.tz(power_datum.data.time, power_datum.house.data.timezone); - // will have to do some additional math here to account for local offset. - return moment(moment_tz.toArray()).toDate(); + house = power_datum.house; + return house.toDate(power_datum.data.time); } get time_to_s(){ - var power_datum = this; - return power_datum.data.time.format('YYYY-MM-DD HH:mm'); + var power_datum = this, + moment_tz = moment.tz(power_datum.data.time * 1000, power_datum.house.data.timezone); + return moment_tz.format('YYYY-MM-DD HH:mm'); } get consumption_to_s(){ var power_datum = this; diff --git a/server/helpers/api_helper.js b/server/helpers/api_helper.js index c17f5b6..964188e 100644 --- a/server/helpers/api_helper.js +++ b/server/helpers/api_helper.js @@ -10,15 +10,15 @@ class ApiHelper { dates.forEach((min_max)=>{ var condition_n = {}; condition_n[field_name] = {}; - if (min_max[0]) condition_n[field_name]['$gt'] = min_max[0]; - if (min_max[1]) condition_n[field_name]['$lt'] = min_max[1]; + if (min_max[0]) condition_n[field_name]['$gte'] = min_max[0]; + if (min_max[1]) condition_n[field_name]['$lte'] = min_max[1]; if (Object.keys(condition_n).length) params['$or'].push(condition_n); }); } else { var min_max = dates[0], condition = {} - if (min_max[0]) condition['$gt'] = min_max[0]; - if (min_max[1]) condition['$lt'] = min_max[1]; + if (min_max[0]) condition['$gte'] = min_max[0]; + if (min_max[1]) condition['$lte'] = min_max[1]; if (Object.keys(condition).length) params[field_name] = condition; } return params; diff --git a/server/models/energy_datum.js b/server/models/energy_datum.js index 524da39..1521c1a 100644 --- a/server/models/energy_datum.js +++ b/server/models/energy_datum.js @@ -34,6 +34,8 @@ var EnergyDatum = DB.sequelize.define(NAME, { exposeForHouseAtDates: (house_id, dates)=>{ var params = {house_id: house_id}; extend(params, ApiHelper.datesParamToSequelize(dates, 'day')); + console.log('EnergyDatum#exposeForHouseAtDates') + console.log(params, dates) return EnergyDatum.findAll({ where: params, attributes: ['id', 'production', 'consumption', 'day']
{power_datum.data.id} {power_datum.time_to_s} {power_datum.consumption_to_s} {power_datum.production_to_s}