diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index 2962f58aae..49f17e40f0 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -43,6 +43,7 @@ import SignupSuccess from '../routes/signup/5-success'; import Dashboard from '../routes/dashboard'; import NewDashboard from '../routes/dashboard/new-dashboard'; import EditDashboard from '../routes/dashboard/edit-dashboard'; +import ExpandedDashboard from '../routes/dashboard/expanded-dashboard'; import IntegrationPage from '../routes/integration'; import ChatPage from '../routes/chat'; @@ -230,6 +231,7 @@ const AppRouter = connect( + @@ -367,7 +369,7 @@ class MainApp extends Component { this.props.checkSession(); } - render({ user }, {}) { + render({ user }, { }) { const translationDefinition = get(translations, user.language, { default: translations.en }); return ( diff --git a/front/src/components/boxs/chart/ApexChartBarOptions.js b/front/src/components/boxs/chart/ApexChartBarOptions.js index f26de7d78d..6bb5b83a34 100644 --- a/front/src/components/boxs/chart/ApexChartBarOptions.js +++ b/front/src/components/boxs/chart/ApexChartBarOptions.js @@ -1,11 +1,11 @@ -const getApexChartBarOptions = ({ displayAxes, series, colors, locales, defaultLocale }) => { +const getApexChartBarOptions = ({ displayAxes, height, series, colors, locales, defaultLocale }) => { const options = { chart: { locales, defaultLocale, type: 'bar', fontFamily: 'inherit', - height: displayAxes ? 200 : 100, + height, parentHeightOffset: 0, toolbar: { show: false diff --git a/front/src/components/boxs/chart/ApexChartComponent.jsx b/front/src/components/boxs/chart/ApexChartComponent.jsx index 6282b73481..ee97848279 100644 --- a/front/src/components/boxs/chart/ApexChartComponent.jsx +++ b/front/src/components/boxs/chart/ApexChartComponent.jsx @@ -103,7 +103,14 @@ class ApexChartComponent extends Component { }; } getBarChartOptions = () => { + let height; + if (this.props.size === 'big' && !this.props.display_axes) { + height = 100 + this.props.heightAdditional; + } else { + height = 200 + this.props.heightAdditional; + } const options = getApexChartBarOptions({ + height, displayAxes: this.props.display_axes, series: this.props.series, colors: mergeArray(this.props.colors, DEFAULT_COLORS), @@ -116,11 +123,11 @@ class ApexChartComponent extends Component { getAreaChartOptions = () => { let height; if (this.props.size === 'small' && !this.props.display_axes) { - height = 40; + height = 40 + this.props.heightAdditional; } else if (this.props.size === 'big' && !this.props.display_axes) { - height = 80; + height = 80 + this.props.heightAdditional; } else { - height = 200; + height = 200 + this.props.heightAdditional; } const options = getApexChartAreaOptions({ height, @@ -137,11 +144,11 @@ class ApexChartComponent extends Component { getLineChartOptions = () => { let height; if (this.props.size === 'small' && !this.props.display_axes) { - height = 40; + height = 40 + this.props.heightAdditional; } else if (this.props.size === 'big' && !this.props.display_axes) { - height = 80; + height = 80 + this.props.heightAdditional; } else { - height = 200; + height = 200 + this.props.heightAdditional; } const options = getApexChartLineOptions({ height, @@ -157,11 +164,11 @@ class ApexChartComponent extends Component { getStepLineChartOptions = () => { let height; if (this.props.size === 'small' && !this.props.display_axes) { - height = 40; + height = 40 + this.props.heightAdditional; } else if (this.props.size === 'big' && !this.props.display_axes) { - height = 80; + height = 80 + this.props.heightAdditional; } else { - height = 200; + height = 200 + this.props.heightAdditional; } const options = getApexChartStepLineOptions({ height, diff --git a/front/src/components/boxs/chart/Chart.jsx b/front/src/components/boxs/chart/Chart.jsx index 704f2eb1dd..e96a663797 100644 --- a/front/src/components/boxs/chart/Chart.jsx +++ b/front/src/components/boxs/chart/Chart.jsx @@ -1,8 +1,8 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; import cx from 'classnames'; - -import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; +import { Text, Localizer } from 'preact-i18n'; import style from './style.css'; import { WEBSOCKET_MESSAGE_TYPES, DEVICE_FEATURE_UNITS } from '../../../../../server/utils/constants'; import get from 'get-value'; @@ -26,6 +26,13 @@ const intervalByName = { 'last-year': ONE_YEAR_IN_MINUTES }; +const getTypeByInterval = interval => { + if (interval >= ONE_DAY_IN_MINUTES) return 'hourly'; + if (interval >= THIRTY_DAYS_IN_MINUTES) return 'daily'; + if (interval >= ONE_YEAR_IN_MINUTES) return 'monthly'; + return 'live'; +}; + const UNITS_WHEN_DOWN_IS_POSITIVE = [DEVICE_FEATURE_UNITS.WATT_HOUR]; const notNullNotUndefined = value => { @@ -114,6 +121,7 @@ class Chartbox extends Component { }); this.getData(); }; + getData = async () => { let deviceFeatures = this.props.box.device_features; let deviceFeatureNames = this.props.box.device_feature_names; @@ -136,12 +144,42 @@ class Chartbox extends Component { return; } await this.setState({ loading: true }); + + let type; + if (this.props.showHistoryExpanded) { + let intervalDate; + if (this.state.startDate && this.state.endDate) { + intervalDate = (this.state.endDate - this.state.startDate) / 60000; + } else { + intervalDate = this.state.interval; + } + if (intervalDate <= ONE_DAY_IN_MINUTES) { + type = 'live'; + } else { + type = getTypeByInterval(intervalDate, this.props.box.chart_type); + } + } else { + type = getTypeByInterval(this.state.interval, this.props.box.chart_type); + } try { - const data = await this.props.httpClient.get(`/api/v1/device_feature/aggregated_states`, { - interval: this.state.interval, - max_states: 100, - device_features: deviceFeatures.join(',') - }); + let data; + if (type === 'live') { + data = await this.props.httpClient.get(`/api/v1/device_feature/aggregated_states`, { + interval: this.state.interval, + max_states: this.state.maxStatesLive, + device_features: deviceFeatures.join(','), + start_date: this.state.startDate ? this.state.startDate.toISOString() : null, + end_date: this.state.endDate ? this.state.endDate.toISOString() : null + }); + } else { + data = await this.props.httpClient.get(`/api/v1/device_feature/aggregated_states`, { + interval: this.state.interval, + max_states: this.state.maxStatesNoLive, + device_features: deviceFeatures.join(','), + start_date: this.state.startDate ? this.state.startDate.toISOString() : undefined, + end_date: this.state.endDate ? this.state.endDate.toISOString() : undefined + }); + } let emptySeries = true; @@ -290,15 +328,39 @@ class Chartbox extends Component { interval: intervalByName[this.props.box.interval] }); }; + + handleZoom = async (min, max) => { + if (min === null || max === null) { + await this.setState({ + startDate: null, + endDate: null + }); + this.getData(); + } else { + await this.setState({ + startDate: new Date(min), + endDate: new Date(max) + }); + this.getData(); + } + }; + constructor(props) { super(props); this.props = props; + console.log('props constructor Chart', props); this.state = { interval: this.props.box.interval ? intervalByName[this.props.box.interval] : ONE_HOUR_IN_MINUTES, loading: true, initialized: false, height: 'small', - nbFeaturesDisplayed: 0 + nbFeaturesDisplayed: 0, + startDate: null, + endDate: null, + dropdownOpen: false, + selectedCriteria: 'before', + maxStatesLive: 10000, + maxStatesNoLive: 1000 }; } componentDidMount() { @@ -327,6 +389,7 @@ class Chartbox extends Component { this.updateDeviceStateWebsocket ); } + render( props, { @@ -340,58 +403,160 @@ class Chartbox extends Component { interval, emptySeries, unit, - nbFeaturesDisplayed + nbFeaturesDisplayed, + startDate, + endDate } ) { - const { box } = this.props; + const { box, displayPreview, showHistoryExpanded } = this.props; const displayVariation = box.display_variation; let heightAdditional = 0; - if (props.box.chart_type === 'timeline') { + + const showAggregatedDataWarning = this.state.series && this.state.series.some(serie => serie.data.length === this.state.maxStatesNoLive); + if (showHistoryExpanded) { + if (props.box.chart_type === 'timeline') { + heightAdditional = 55 * (nbFeaturesDisplayed - 1) + 300; + } else { + heightAdditional = 300; + } + } else if (props.box.chart_type === 'timeline') { heightAdditional = 55 * nbFeaturesDisplayed; } return (
-
+
{props.box.title}
-
+ {showHistoryExpanded && !showAggregatedDataWarning && ( +
+ +
+ )} + {showHistoryExpanded && showAggregatedDataWarning && ( +
{"(ATTENTION: Données aggrégées sur l'intervalle, vous pouvez zoomer sur un intervalle plus petit pour voir les données réelles)"}
+ )} +
{props.box.chart_type && ( - + + {displayVariation && props.box.chart_type !== 'timeline' && emptySeries === false && ( +
+ {notNullNotUndefined(lastValueRounded) && !Number.isNaN(lastValueRounded) && ( +
+ {lastValueRounded} + {unit !== undefined && } +
+ )} +
0 && !variationDownIsPositive) || (variation < 0 && variationDownIsPositive), + [style.textYellow]: variation === 0, + [style.textRed]: + (variation > 0 && variationDownIsPositive) || (variation < 0 && !variationDownIsPositive) + })} + > + {variation !== undefined && ( + + {roundWith2DecimalIfNeeded(variation)} + + {variation > 0 && ( + @@ -430,104 +595,19 @@ class Chartbox extends Component {
)}
-
- - {props.box.chart_type && ( -
- {displayVariation && props.box.chart_type !== 'timeline' && emptySeries === false && ( -
- {notNullNotUndefined(lastValueRounded) && !Number.isNaN(lastValueRounded) && ( -
- {lastValueRounded} - {unit !== undefined && } -
- )} -
0 && !variationDownIsPositive) || (variation < 0 && variationDownIsPositive), - [style.textYellow]: variation === 0, - [style.textRed]: - (variation > 0 && variationDownIsPositive) || (variation < 0 && !variationDownIsPositive) - })} - > - {variation !== undefined && ( - - {roundWith2DecimalIfNeeded(variation)} - - {variation > 0 && ( - - - - - - )} - {variation === 0 && ( - - - - - )} - {variation < 0 && ( - - - - - - )} - - )} -
-
- )} - {emptySeries === false && props.box.display_axes && ( -
- -
- )} + )} + {emptySeries === false && props.box.display_axes && ( +
+
)}
@@ -555,34 +635,17 @@ class Chartbox extends Component {
)} - {props.box.chart_type && ( -
- {emptySeries === true && ( -
-
-
- - -
-
- -
-
- )} - {emptySeries === false && !props.box.display_axes && ( - - )} -
+ {emptySeries === false && !props.box.display_axes && ( + )}
diff --git a/front/src/components/boxs/chart/EditChart.jsx b/front/src/components/boxs/chart/EditChart.jsx index d40c59f3cd..88f51fc871 100644 --- a/front/src/components/boxs/chart/EditChart.jsx +++ b/front/src/components/boxs/chart/EditChart.jsx @@ -573,7 +573,7 @@ class EditChart extends Component { - {displayPreview && } + {displayPreview && } {!displayPreview && (
))} diff --git a/front/src/routes/dashboard/expanded-dashboard/index.js b/front/src/routes/dashboard/expanded-dashboard/index.js new file mode 100644 index 0000000000..602457cddb --- /dev/null +++ b/front/src/routes/dashboard/expanded-dashboard/index.js @@ -0,0 +1,515 @@ +import { Component } from 'preact'; +import { Text, Localizer } from 'preact-i18n'; +import { connect } from 'unistore/preact'; +import { route } from 'preact-router'; +import { Link } from 'preact-router/match'; +import cx from 'classnames'; +import style from './style.css'; +import Chart from '../../../components/boxs/chart/Chart'; + +import DatePicker from 'react-datepicker'; +import 'react-datepicker/dist/react-datepicker.css'; + +import { useState } from 'preact/hooks'; + +const criteriaDateExpendOptions = [ + { value: 'all', label: }, + { value: 'previous', label: }, + { value: 'current', label: }, + { value: 'next', label: }, + { value: 'before', label: }, + { value: 'after', label: }, + { value: 'between', label: }, + { value: 'custom', label: } +]; +const criteriaAggregateExpendOptions = [ + { value: 'minute', label: }, + { value: 'hour', label: }, + { value: 'day', label: }, + { value: 'week', label: }, + { value: 'month', label: }, + { value: 'year', label: }, + { value: 'quarter', label: }, + { value: 'notAggregate', label: } +]; + +const intervalUnit = [ + { value: 'days', label: }, + { value: 'weeks', label: }, + { value: 'months', label: }, + { value: 'quarter', label: }, + { value: 'years', label: } +]; +const intervalUnitCurrent = [ + { value: 'days', label: }, + { value: 'weeks', label: }, + { value: 'months', label: }, + { value: 'quarter', label: }, + { value: 'years', label: } +]; + +const ChartBoxExpanded = ({ children, ...props }) => ( +
+ {console.log('props ChartBoxExpanded', props)} + {console.log('children ChartBoxExpanded', children)} + {console.log('this ChartBoxExpanded', this)} +
+
+
+
+
+
+
+
+
+
+ + + +

+ {props.name} - {props.box.title} +

+
+ {/* Future description ? */} + {/*

+ +

*/} + {props.dashboardAlreadyExistError && ( +
+ +
+ )} + {props.unknownError && ( +
+ +
+ )} + {/* Futures options */} + {/*
+ + + } + value={props.name} + onInput={props.updateName} + /> + +
*/} + + {/* Future options / Boutons */} + {/* */} +
+
+
+
+ +
+ {console.log('props.dropdownOpenGlobal', props.dropdownOpenGlobal)} +
+ + + + {props.dropdownOpenGlobal && ( +
+
+ +
+ {criteriaDateExpendOptions.map(option => ( + props.handleCriteriaDateChange(option.value)}> + {option.label} + + ))} +
+ {(props.selectedCriteriaDate === 'previous' || props.selectedCriteriaDate === 'next') && +
+ + +
+ } + {(props.selectedCriteriaDate === 'current') && +
+ +
+ } + {console.log('props.boxOptions.startDate', props.boxOptions)} + {(props.selectedCriteriaDate === 'before') && + <> +
+
+ + +
+ +
+ + +
+
+ + } + {(props.selectedCriteriaDate === 'between') && + <> +
+ + +
+
+
+ + + + +
+
+ + + + +
+
+ + } + +
+
+ +
+
+ )} + + + + +
+ +
+ +
+
+ +
+ {criteriaAggregateExpendOptions.map(option => ( + props.handleCriteriaAggregateChange(option.value)}> + {option.label} + + ))} +
+
+
+
+ + +
+
+ +
+
+
+
+
+
+); + +class ExpandedDashboardPage extends Component { + toggleDropdownGlobal = () => { + this.setState(prevState => ({ + dropdownOpenGlobal: !prevState.dropdownOpenGlobal + })); + }; + applyAndClose = () => { + console.log('applyAndClose'); + this.setState({ dropdownOpenGlobal: false }); + // Logique supplémentaire si besoin, comme l'appel à une fonction pour appliquer les critères sélectionnés. + }; + toggleDropdownCriteriaDate = () => { + this.setState(prevState => ({ + dropdownOpenCriteriaDate: !prevState.dropdownOpenCriteriaDate + })); + }; + handleIntervalUnitChange = (event) => { + console.log('handleIntervalUnitChange', event.target.value); + this.setState({ boxOptions: { ...this.state.boxOptions, intervalUnit: event.target.value } }); + }; + + handleCriteriaDateChange = (criteria) => { + this.setState({ selectedCriteriaDate: criteria, dropdownOpenCriteriaDate: false }); + }; + toggleDropdownCriteriaAggregate = () => { + this.setState(prevState => ({ + dropdownOpenCriteriaAggregate: !prevState.dropdownOpenCriteriaAggregate + })); + }; + handleCriteriaAggregateChange = (criteria) => { + this.setState({ selectedCriteriaAggregate: criteria, dropdownOpenCriteriaAggregate: false }); + }; + + handleStartDateChange = (e) => { + let date; + if (e.target) { + date = e.target.valueAsDate; + } else { + date = e; + } + console.log('handleStartDateChange', date); + this.setState(prevState => { + if (prevState.boxOptions.endDate) { + const newEndDate = prevState.boxOptions.endDate || new Date(date.getTime() + prevState.boxOptions.interval * 60000); + return { + startDate: date, + endDate: newEndDate < date ? date : newEndDate + }; + } + return { + startDate: date, + endDate: new Date() + }; + }); + }; + + handleEndDateChange = (e) => { + let date; + if (e.target) { + date = e.target.valueAsDate; + } else { + date = e; + } + console.log('handleEndDateChange', date); + this.setState(prevState => { + const boxOptions = prevState.boxOptions; + console.log('handleEndDateChange prevState', prevState); + boxOptions.endDate = new Date(date); + return boxOptions; + if (boxOptions.startDate) { + const newStartDate = boxOptions.startDate || new Date(date.getTime() - boxOptions.interval * 60000); + + return { + endDate: date, + startDate: newStartDate > date ? date : newStartDate + }; + } + return { + endDate: date + }; + }); + }; + calculateNewDates = (startDate, endDate, interval, direction) => { + let newStartDate, newEndDate; + const multiplier = direction === 'previous' ? -1 : 1; + + if (startDate && !endDate) { + newStartDate = new Date(startDate.getTime() + multiplier * interval * 60000); + newEndDate = new Date(startDate.getTime()); + } else if (!startDate && endDate) { + newStartDate = new Date(endDate.getTime() + multiplier * interval * 60000); + newEndDate = new Date(endDate.getTime()); + } else if (!startDate && !endDate) { + newStartDate = new Date(Date.now() + multiplier * interval * 60000 * 2); + newEndDate = new Date(Date.now() + multiplier * interval * 60000); + } else { + newStartDate = new Date(startDate.getTime() + multiplier * (endDate.getTime() - startDate.getTime())); + newEndDate = new Date(endDate.getTime() + multiplier * (endDate.getTime() - startDate.getTime())); + } + + return { newStartDate, newEndDate }; + }; + + handlePreviousDate = () => { + const { startDate, endDate, interval } = this.state; + const { newStartDate, newEndDate } = this.calculateNewDates(startDate, endDate, interval, 'previous'); + this.setState( + { + startDate: newStartDate, + endDate: newEndDate + }, + this.getData + ); + }; + handleNextDate = () => { + const { startDate, endDate, interval } = this.state; + const { newStartDate, newEndDate } = this.calculateNewDates(startDate, endDate, interval, 'next'); + this.setState( + { + startDate: newStartDate, + endDate: newEndDate + }, + this.getData + ); + }; + constructor(props) { + console.log('constructor props', props); + super(props); + this.props = props; + this.state = { + name: '', + box: null, + loading: true, + x: props.x, + y: props.y, + boxOptions: { + startDate: null, + endDate: null, + interval: 30, + intervalUnit: 'days' + }, + dropdownOpenCriteriaDate: false, + dropdownOpenCriteriaAggregate: false, + selectedGlobalOption: false, + selectedCriteriaDate: 'before', + selectedCriteriaAggregate: 'notAggregate', + }; + console.log('this.state', this.state); + } + + async componentDidMount() { + const { x, y, dashboardSelector } = this.props; + const currentDashboard = await this.props.httpClient.get( + `/api/v1/dashboard/${dashboardSelector}` + ); + const box = currentDashboard.boxes[x][y]; + const name = currentDashboard.name; + this.setState({ box, loading: false, name }); + } + render(props, { loading, + boxOptions, + dropdownOpenGlobal, + dropdownOpenCriteriaDate, + dropdownOpenCriteriaAggregate, + selectedCriteriaDate, + selectedCriteriaAggregate + }) { + if (!loading) { + return ( + + + ); + } + } +} + +export default connect('user,httpClient', {})(ExpandedDashboardPage); diff --git a/front/src/routes/dashboard/expanded-dashboard/style.css b/front/src/routes/dashboard/expanded-dashboard/style.css new file mode 100644 index 0000000000..f846518c48 --- /dev/null +++ b/front/src/routes/dashboard/expanded-dashboard/style.css @@ -0,0 +1,70 @@ +.containerWithMargin { + margin-top: 1rem; +} + +.dropdownMenuUp { + top: auto; + bottom: 100%; + transform: translateY(-1%); + z-index: 300; +} +.dropdownMenuDown { + width: auto; + min-width: 150px; +} +.dropdownItem { + cursor: pointer; +} + +.flexContainer { + display: flex; + align-items: center; + justify-content: center; +} + +.cardFooter { + margin-bottom: 1rem; + margin-top: 1rem; + display: flex; + align-items: center; + justify-content: space-between; +} +.datePicker { + display: flex; + align-items: center; + margin-right: 0.5rem; +} + +.datePickerChart { + display: flex; + align-items: center; + padding: 0.4rem 0; +} + +.datePickerLabel { + margin: 0 1rem; + font-size: 0.75rem; +} + +.datePickerInput { + height: calc(1.5em + 0.75rem + 2px); + padding: 0.375rem 0.75rem; + font-size: 0.625rem; +} + +.displacementRaftersChart { + margin-left: auto; + display: flex; + align-items: center; +} + +/* .backButtonDiv { + margin-bottom: 1rem; + /* max-width: 24rem; */ +/*} +@media (max-width: 768px) { + .backButtonDiv { + /* margin-bottom: 1rem; */ +/* max-width: 24rem; */ +/* } +} */ \ No newline at end of file diff --git a/server/api/controllers/device.controller.js b/server/api/controllers/device.controller.js index b04c096e11..23568fda5a 100644 --- a/server/api/controllers/device.controller.js +++ b/server/api/controllers/device.controller.js @@ -102,6 +102,8 @@ module.exports = function DeviceController(gladys) { req.query.device_features.split(','), req.query.interval, req.query.max_states, + req.query.start_date, + req.query.end_date, ); res.json(states); } diff --git a/server/lib/device/device.getDeviceFeaturesAggregates.js b/server/lib/device/device.getDeviceFeaturesAggregates.js index fc6d6e0ac7..47c5fee3a8 100644 --- a/server/lib/device/device.getDeviceFeaturesAggregates.js +++ b/server/lib/device/device.getDeviceFeaturesAggregates.js @@ -6,11 +6,19 @@ const { NotFoundError } = require('../../utils/coreErrors'); * @param {string} selector - Device selector. * @param {number} intervalInMinutes - Interval. * @param {number} maxStates - Number of elements to return max. + * @param {Date} startDate - Start date. + * @param {Date} endDate - End date. * @returns {Promise} - Resolve with an array of data. * @example - * device.getDeviceFeaturesAggregates('test-devivce'); + * device.getDeviceFeaturesAggregates('test-device'); */ -async function getDeviceFeaturesAggregates(selector, intervalInMinutes, maxStates = 100) { +async function getDeviceFeaturesAggregates( + selector, + intervalInMinutes, + maxStates = 100, + startDate = null, + endDate = null, +) { const deviceFeature = this.stateManager.get('deviceFeature', selector); if (deviceFeature === null) { throw new NotFoundError('DeviceFeature not found'); @@ -18,33 +26,55 @@ async function getDeviceFeaturesAggregates(selector, intervalInMinutes, maxState const device = this.stateManager.get('deviceById', deviceFeature.device_id); const now = new Date(); - const intervalDate = new Date(now.getTime() - intervalInMinutes * 60 * 1000); + // const intervalDate = new Date(now.getTime() - intervalInMinutes * 60 * 1000); + let intervalDate; + let newStartDate = startDate; + let newEndDate = endDate; + if (startDate === null && endDate === null) { + intervalDate = new Date(now.getTime() - intervalInMinutes * 60 * 1000); + } else if (startDate !== null && endDate === null) { + intervalDate = new Date(new Date(startDate).getTime() + intervalInMinutes * 60 * 1000); + newEndDate = intervalDate; + intervalDate = new Date(startDate); + } else if (startDate === null && endDate !== null) { + intervalDate = new Date(new Date(endDate).getTime() - intervalInMinutes * 60 * 1000); + newStartDate = intervalDate; + intervalDate = new Date(endDate); + } else { + intervalDate = new Date(startDate); + } const values = await db.duckDbReadConnectionAllAsync( ` - WITH intervals AS ( - SELECT - created_at, - value, - NTILE(?) OVER (ORDER BY created_at) AS interval - FROM - t_device_feature_state - WHERE device_feature_id = ? - AND created_at > ? - ) - SELECT - MIN(created_at) AS created_at, - AVG(value) AS value - FROM - intervals - GROUP BY - interval - ORDER BY - created_at; + WITH intervals AS ( + SELECT + created_at, + value, + NTILE(?) OVER (ORDER BY created_at) AS interval + FROM + t_device_feature_state + WHERE device_feature_id = ? + AND created_at > ? + ${startDate ? 'AND created_at >= ?' : ''} + ${endDate ? 'AND created_at <= ?' : ''} + ) + SELECT + MIN(created_at) AS created_at, + AVG(value) AS value + FROM + intervals + GROUP BY + interval + ORDER BY + created_at; `, - maxStates, - deviceFeature.id, - intervalDate, + ...[ + maxStates, + deviceFeature.id, + intervalDate, + ...(startDate ? [new Date(startDate)] : []), + ...(endDate ? [new Date(endDate)] : []) + ] ); return { diff --git a/server/lib/device/device.getDeviceFeaturesAggregatesMulti.js b/server/lib/device/device.getDeviceFeaturesAggregatesMulti.js index 68016a9bb4..5b127c6210 100644 --- a/server/lib/device/device.getDeviceFeaturesAggregatesMulti.js +++ b/server/lib/device/device.getDeviceFeaturesAggregatesMulti.js @@ -4,15 +4,23 @@ const Promise = require('bluebird'); * @param {Array} selectors - Array of device feature selectors. * @param {number} intervalInMinutes - Interval. * @param {number} maxStates - Number of elements to return max. + * @param {Date} startDate - Start date. + * @param {Date} endDate - End date. * @returns {Promise} - Resolve with an array of array of data. * @example * device.getDeviceFeaturesAggregates('test-devivce'); */ -async function getDeviceFeaturesAggregatesMulti(selectors, intervalInMinutes, maxStates = 100) { +async function getDeviceFeaturesAggregatesMulti( + selectors, + intervalInMinutes, + maxStates = 100, + startDate = null, + endDate = null, +) { return Promise.map( selectors, async (selector) => { - return this.getDeviceFeaturesAggregates(selector, intervalInMinutes, maxStates); + return this.getDeviceFeaturesAggregates(selector, intervalInMinutes, maxStates, startDate, endDate); }, { concurrency: 4 }, );