Commit e6b74355 authored by Guilhem CARRON's avatar Guilhem CARRON
Browse files

feat(analysis): Add electricity consumption profile in analysis

- new doctype enedis.maxpower
- new doctype enedis.monthly.analysis.data
- new cozy service that calculates the enedis monthly analysis
- require Enedis-Konnector version 1.1.0 to get maxPower values
parent 475ec2f7
......@@ -130,6 +130,11 @@
}
},
"services": {
"enedisHalfHourMonthlyAnalysis": {
"type": "node",
"file": "services/enedisHalfHourMonthlyAnalysis/ecolyo.js",
"trigger": "@cron 0 0 8 3 * *"
},
"monthlyReportNotification": {
"type": "node",
"file": "services/monthlyReportNotification/ecolyo.js",
......
......@@ -107,7 +107,7 @@ const generateHalfAnHourData = (_startingdate, _endingDate, min, max) => {
hour: 0,
minute: 0,
})
monthDumpArray.push({
load: Math.round(monthlyLoad * 100) / 100,
year: lastYear,
......
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="7" y="6" width="25" height="2" rx="1" fill="white"/>
<path d="M17 14C17 12.8954 17.8954 12 19 12H20C21.1046 12 22 12.8954 22 14V33H17V14Z" fill="#D87B39"/>
<path d="M9 24C9 22.8954 9.89543 22 11 22H12C13.1046 22 14 22.8954 14 24V33H9V24Z" fill="#D87B39"/>
<path d="M25 28C25 26.8954 25.8954 26 27 26H28C29.1046 26 30 26.8954 30 28V33H25V28Z" fill="#D87B39"/>
</svg>
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M29.0636 23.2831C26.3793 27.021 21.6698 29.0536 16.8631 28.0942C10.3113 26.7865 6.05999 20.4151 7.36766 13.8632C8.03388 10.5253 10.0144 7.78445 12.6635 6.07163C7.31475 7.17394 2.8792 11.3674 1.74304 17.0599C0.228052 24.6505 5.15331 32.0321 12.7439 33.5471C20.086 35.0125 27.2325 30.4524 29.0636 23.2831Z" fill="#D87B39"/>
<path d="M24 9.75311C24 9.9864 23.9379 10.1994 23.8138 10.3921L18.4996 18.3645H23.8782V20H16V19.1936C16 19.0922 16.0167 18.9959 16.0501 18.9046C16.0836 18.8082 16.1265 18.7195 16.1791 18.6383L21.5076 10.6279H16.3366V9H24V9.75311Z" fill="white"/>
<path d="M35 5.75311C35 5.9864 34.9379 6.1994 34.8138 6.39212L29.4996 14.3645H34.8782V16H27V15.1936C27 15.0922 27.0167 14.9959 27.0501 14.9046C27.0836 14.8082 27.1265 14.7195 27.1791 14.6383L32.5076 6.62794H27.3366V5H35V5.75311Z" fill="white"/>
</svg>
import React from 'react'
import { mount } from 'enzyme'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import ElecHalfHourChart from './ElecHalfHourChart'
import * as reactRedux from 'react-redux'
import { DateTime } from 'luxon'
import { dataLoadArray } from '../../../tests/__mocks__/datachartData.mock'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
const mockcompareStepDate = jest.fn()
jest.mock('services/dateChart.service', () => {
return jest.fn(() => {
return {
compareStepDate: mockcompareStepDate,
}
})
})
const mockStore = configureStore([])
const mockUseSelector = jest.spyOn(reactRedux, 'useSelector')
describe('ElecHalfHourChart component', () => {
it('should be rendered correctly', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
mockUseSelector.mockReturnValue(
DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})
)
const wrapper = mount(
<Provider store={store}>
<ElecHalfHourChart dataLoad={dataLoadArray} isWeekend={true} />
</Provider>
).getElement()
expect(wrapper).toMatchSnapshot()
})
it('should render week data', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
mockUseSelector.mockReturnValue(
DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})
)
const wrapper = mount(
<Provider store={store}>
<ElecHalfHourChart dataLoad={dataLoadArray} isWeekend={false} />
</Provider>
)
expect(wrapper.find('.week')).toBeTruthy()
})
})
import React, { useEffect, useRef, useState } from 'react'
import Bar from 'components/Charts/Bar'
import AxisBottom from 'components/Charts/AxisBottom'
import AxisRight from 'components/Charts/AxisRight'
import { FluidType } from 'enum/fluid.enum'
import { scaleBand, ScaleBand, scaleLinear, ScaleLinear } from 'd3-scale'
import { DateTime } from 'luxon'
import { TimeStep } from 'enum/timeStep.enum'
import { Dataload } from 'models'
import './elecHalfHourMonthlyAnalysis.scss'
interface ElecHalfHourChartProps {
dataLoad: Dataload[]
isWeekend: boolean
}
const ElecHalfHourChart = ({ dataLoad, isWeekend }: ElecHalfHourChartProps) => {
const [width, setWidth] = useState<number>(0)
const [height, setHeight] = useState<number>(0)
const chartContainer = useRef<HTMLDivElement>(null)
const marginLeft = 10
const marginRight = 10
const marginTop = 20
const marginBottom = 50
const getContentWidth = () => {
return width - marginLeft - marginRight
}
const getContentHeight = () => {
return height - marginTop - marginBottom
}
const getMaxLoad = () => {
const maxLoad = dataLoad
? Math.max(...dataLoad.map((d: Dataload) => d.value))
: 0
return maxLoad
}
const xScale: ScaleBand<string> = scaleBand()
.domain(
dataLoad.map((d: Dataload) =>
d.date.toLocaleString(DateTime.DATETIME_SHORT)
)
)
.range([0, getContentWidth()])
.padding(0.2)
const yScale: ScaleLinear<number, number> = scaleLinear()
.domain([0, getMaxLoad()])
.range([getContentHeight(), 0])
useEffect(() => {
function handleResize() {
const maxWidth = 940
const maxHeight = 200
const _width = chartContainer.current
? chartContainer.current.offsetWidth > maxWidth
? maxWidth
: chartContainer.current.offsetWidth
: 400
setWidth(_width)
const _height = chartContainer.current
? chartContainer.current.offsetHeight > maxHeight
? maxHeight
: chartContainer.current.offsetHeight
: 200
setHeight(_height)
}
handleResize()
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return (
<div className="graph-elec-half-hour" ref={chartContainer}>
<svg width={width} height={height}>
<AxisRight
fluidType={FluidType.ELECTRICITY}
yScale={yScale}
width={width}
marginRight={marginRight}
marginTop={marginTop}
isAnalysis={true}
/>
<g transform={`translate(${10},${10})`}>
{dataLoad.map((value, index) => {
return (
<Bar
key={index}
index={index}
dataload={value}
compareDataload={null}
fluidType={FluidType.ELECTRICITY}
timeStep={TimeStep.HALF_AN_HOUR}
showCompare={false}
xScale={xScale}
yScale={yScale}
height={getContentHeight()}
isSwitching={false}
isDuel={false}
weekdays={isWeekend ? 'weekend' : 'week'}
/>
)
})}
</g>
<AxisBottom
data={dataLoad}
timeStep={TimeStep.HALF_AN_HOUR}
xScale={xScale}
height={height}
marginLeft={marginLeft}
marginBottom={marginBottom}
isDuel={false}
/>
</svg>
</div>
)
}
export default ElecHalfHourChart
import React from 'react'
import { mount } from 'enzyme'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { Provider } from 'react-redux'
import configureStore from 'redux-mock-store'
import * as reactRedux from 'react-redux'
import { DateTime } from 'luxon'
import ElecHalfHourMonthlyAnalysis from './ElecHalfHourMonthlyAnalysis'
import { IconButton } from '@material-ui/core'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
const mockcompareStepDate = jest.fn()
jest.mock('services/dateChart.service', () => {
return jest.fn(() => {
return {
compareStepDate: mockcompareStepDate,
}
})
})
const mockStore = configureStore([])
const mockUseSelector = jest.spyOn(reactRedux, 'useSelector')
describe('ElecHalfHourMonthlyAnalysis component', () => {
it('should be rendered correctly', () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
mockUseSelector.mockReturnValue(
DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})
)
const wrapper = mount(
<Provider store={store}>
<ElecHalfHourMonthlyAnalysis
analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
/>
</Provider>
).getElement()
expect(wrapper).toMatchSnapshot()
})
it('should change from weekend to week', async () => {
const store = mockStore({
ecolyo: {
global: globalStateData,
},
})
mockUseSelector.mockReturnValue(
DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})
)
const wrapper = mount(
<Provider store={store}>
<ElecHalfHourMonthlyAnalysis
analysisDate={DateTime.fromISO('2021-07-01T00:00:00.000Z', {
zone: 'utc',
})}
/>
</Provider>
)
wrapper
.find(IconButton)
.first()
.simulate('click')
expect(wrapper.find('.weekend')).toBeTruthy()
})
})
import React, { useEffect, useState } from 'react'
import { useI18n } from 'cozy-ui/transpiled/react/I18n'
import LeftArrowIcon from 'assets/icons/ico/left-arrow.svg'
import RigthArrowIcon from 'assets/icons/ico/right-arrow.svg'
import MinIcon from 'assets/icons/ico/minimum.svg'
import MaxPowerIcon from 'assets/icons/ico/maxPower.svg'
import IconButton from '@material-ui/core/IconButton'
import Icon from 'cozy-ui/transpiled/react/Icon'
import { FluidType } from 'enum/fluid.enum'
import iconEnedisLogo from 'assets/icons/visu/enedis-logo.svg'
import { UserExplorationID } from 'enum/userExploration.enum'
import { getNavPicto } from 'utils/picto'
import { DateTime } from 'luxon'
import { useClient } from 'cozy-client'
import EnedisMonthlyAnalysisDataService from 'services/enedisMonthlyAnalysisData.service'
import ConsumptionService from 'services/consumption.service'
import {
AggregatedEnedisMonthlyDataloads,
EnedisMonthlyAnalysisData,
} from 'models/enedisMonthlyAnalysis'
import ElecHalfHourChart from './ElecHalfHourChart'
import './elecHalfHourMonthlyAnalysis.scss'
import StyledSpinner from 'components/CommonKit/Spinner/StyledSpinner'
import { TimeStep } from 'enum/timeStep.enum'
import { Button } from '@material-ui/core'
import StyledIcon from 'components/CommonKit/Icon/StyledIcon'
import useExploration from 'components/Hooks/useExploration'
import { FluidConfig } from 'models'
import ConfigService from 'services/fluidConfig.service'
interface ElecHalfHourMonthlyAnalysisProps {
analysisDate: DateTime
}
const ElecHalfHourMonthlyAnalysis: React.FC<ElecHalfHourMonthlyAnalysisProps> = ({
analysisDate,
}: ElecHalfHourMonthlyAnalysisProps) => {
const { t } = useI18n()
const client = useClient()
const fluidConfig: Array<FluidConfig> = new ConfigService().getFluidConfig()
const [, setValidExploration] = useExploration()
const [isWeekend, setisWeekend] = useState(true)
const [isHalfHourActivated, setisHalfHourActivated] = useState(true)
const [isLoading, setisLoading] = useState(true)
const [monthDataloads, setMonthDataloads] = useState<
AggregatedEnedisMonthlyDataloads
>()
const [enedisAnalysisValues, setenedisAnalysisValues] = useState<
EnedisMonthlyAnalysisData
>()
const handleChangeWeek = () => {
setisWeekend(prev => !prev)
}
useEffect(() => {
let subscribed = true
async function getEnedisAnalysisData() {
const cs = new ConsumptionService(client)
const activateHalfHourLoad = await cs.checkDoctypeEntries(
FluidType.ELECTRICITY,
TimeStep.HALF_AN_HOUR
)
if (activateHalfHourLoad) {
const emas = new EnedisMonthlyAnalysisDataService(client)
const data = await emas.getEnedisMonthlyAnalysisByDate(
analysisDate.year,
analysisDate.month - 1
)
if (data && data.length) {
const aggregatedData = emas.aggregateValuesToDataLoad(data[0])
setenedisAnalysisValues(data[0])
setMonthDataloads(aggregatedData)
}
} else {
setisHalfHourActivated(false)
}
setisLoading(false)
}
if (subscribed) {
getEnedisAnalysisData()
}
return () => {
subscribed = false
}
}, [analysisDate, client])
return (
<div className="special-elec-container">
<Icon
className="elec-icon"
icon={getNavPicto(FluidType.ELECTRICITY, true, true)}
size={42}
/>
<div className="text-18-normal title">{t('special_elec.title')}</div>
{isHalfHourActivated ? (
<>
<div className="navigator">
<IconButton
aria-label={t('consumption.accessibility.button_previous_value')}
onClick={handleChangeWeek}
className="arrow-prev"
>
<Icon icon={LeftArrowIcon} size={24} />
</IconButton>
<div className="average text-18-normal">
<div className="text-1">{t('special_elec.average')}</div>
<div className="text-2 text-18-bold">
{t('special_elec.weektype')}{' '}
<span className={isWeekend ? 'weekend' : 'week'}>
{isWeekend
? t('special_elec.weekend')
: t('special_elec.week')}
</span>
</div>
</div>
<IconButton
aria-label={t('consumption.accessibility.button_previous_value')}
onClick={handleChangeWeek}
className="arrow-next"
>
<Icon icon={RigthArrowIcon} size={24} />
</IconButton>
</div>
{!isLoading ? (
<>
{monthDataloads && (
<ElecHalfHourChart
dataLoad={
isWeekend ? monthDataloads.weekend : monthDataloads.week
}
isWeekend={isWeekend}
/>
)}
{enedisAnalysisValues && (
<div className="min-max">
<div className="container">
<Icon icon={MinIcon} size={40} className="minIcon" />
<div className="text">
<div className="min text-18-normal">
{t('special_elec.min')}
</div>
<div className="value text-18-bold">
{enedisAnalysisValues.minLoad !== 0 &&
enedisAnalysisValues.minLoad !== null ? (
<>
{enedisAnalysisValues.minLoad} <span> kWh</span>
</>
) : (
<span>----</span>
)}
</div>
</div>
</div>
<div className="container">
<Icon icon={MaxPowerIcon} size={40} className="minIcon" />
<div className="text">
<div className="min text-18-normal">
{t('special_elec.maxPower')}
</div>
<div className="value text-18-bold">
{enedisAnalysisValues.maxPower !== 0 &&
enedisAnalysisValues.maxPower !== null ? (
<>
{enedisAnalysisValues.maxPower.toFixed(2)}
<span> kVA</span>
</>
) : (
<span>----</span>
)}
</div>
</div>
</div>
</div>
)}
</>
) : (
<div className="loader-container">
<StyledSpinner size="5em" fluidType={FluidType.ELECTRICITY} />
</div>
)}
</>
) : (
<>
<div className="activation-text text-18-normal">
{t(`timestep.activate.enedis.no_consent_active.text_analysis`)}
</div>
<Button
aria-label={t(
`timestep.activate.enedis.no_consent_active.accessibility.button_activate`
)}
onClick={() => {
setValidExploration(UserExplorationID.EXPLORATION004)
window.open(fluidConfig[0].konnectorConfig.activation, '_blank')
}}
classes={{
root: 'btn-highlight',
label: 'text-16-bold',
}}
>
<div className="oauthform-button-content">
<div className="oauthform-button-content-icon">
<StyledIcon icon={iconEnedisLogo} size={48} />
</div>
<div className="oauthform-button-text text-18-bold">
<div>
{t(
`timestep.activate.enedis.no_consent_active.accessibility.button_activate`
)}
</div>
</div>
</div>
</Button>
</>
)}
</div>
)
}
export default ElecHalfHourMonthlyAnalysis
......@@ -23,6 +23,7 @@ import { DateTime } from 'luxon'
import MaxConsumptionCard from './MaxConsumptionCard'
import AnalysisIcon from 'assets/icons/visu/analysis/analysis.svg'
import TotalAnalysisChart from './TotalAnalysisChart'
import ElecHalfHourMonthlyAnalysis from './ElecHalfHourMonthlyAnalysis'
interface MonthlyAnalysisProps {
analysisDate: DateTime
......@@ -180,6 +181,13 @@ const MonthlyAnalysis: React.FC<MonthlyAnalysisProps> = ({
/>
</div>
</div>
{fluidTypes.includes(FluidType.ELECTRICITY) && (
<div className="analysis-content">
<div className="card">
<ElecHalfHourMonthlyAnalysis analysisDate={analysisDate} />
</div>
</div>
)}
</>
) : (
<AnalysisErrorModal />
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ElecHalfHourChart component should be rendered correctly 1`] = `
<Provider
store={
Object {
"clearActions": [Function],
"dispatch": [Function],
"getActions": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
}
}
>
<ElecHalfHourChart
dataLoad={
Array [
Object {
"date": "2021-09-23T00:00:00.000Z",
"value": 12,
"valueDetail": null,
},
Object {
"date": "2021-09-23T00:00:00.000Z",
"value": 12,
"valueDetail": null,
},
Object {
"date": "2021-09-23T00:00:00.000Z",
"value": 12,
"valueDetail": null,
},
Object {
"date": "2021-09-23T00:00:00.000Z",
"value": 12,