From bf7a07f73d7cdcb16fd09ff4ceab0519142b7ab4 Mon Sep 17 00:00:00 2001 From: David HERNANDEZ Date: Tue, 4 Oct 2022 00:31:26 +0200 Subject: [PATCH] Feat - Added MagicMock to request methods and added the new Solar.Forecast API --- CHANGELOG.md | 2 + README.md | 2 + data/test_response_scrapper_method.pbz2 | Bin 11886 -> 2574 bytes data/test_response_solarforecast_method.pbz2 | Bin 0 -> 1193 bytes data/test_response_solcast_method.pbz2 | Bin 0 -> 1238 bytes docs/forecasts.md | 7 +++ secrets_emhass(example).yaml | 3 +- src/emhass/forecast.py | 41 +++++++++++++-- src/emhass/utils.py | 5 ++ tests/test_forecast.py | 50 ++++++++++++++----- 10 files changed, 92 insertions(+), 18 deletions(-) create mode 100644 data/test_response_solarforecast_method.pbz2 create mode 100644 data/test_response_solcast_method.pbz2 diff --git a/CHANGELOG.md b/CHANGELOG.md index bf87621b..8c233449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Added more detailed examples to the forecast module documentation. - Improved handling of datatime indexes in DataFrames on forecast module. - Added warning messages if passed list values contains non numeric items. +- Added missing unittests for forecast module with request.get dependencies using MagicMock. +- Added the Solar.Forecast method. ## [0.3.19] - 2022-09-14 ### Fix diff --git a/README.md b/README.md index b5b879d3..d99afa27 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,8 @@ Here is the list of the other additional dictionnary keys that can be passed at - `solcast_rooftop_id` for the ID of your rooftop for the SolCast service implementation. +- `solar_forecast_kwp` for the PV peak installed power in kW used for the solar.forecast API call. + ### A naive Model Predictive Controller A MPC controller was introduced in v0.3.0. This is an informal/naive representation of a MPC controller. diff --git a/data/test_response_scrapper_method.pbz2 b/data/test_response_scrapper_method.pbz2 index 50ac9deca444be05c982aadb352d343bf72ee941..1b3b580f53ec4fc5cdafd16fc00bd890f2dbb7c2 100644 GIT binary patch literal 2574 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 literal 11886 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 diff --git a/data/test_response_solarforecast_method.pbz2 b/data/test_response_solarforecast_method.pbz2 new file mode 100644 index 0000000000000000000000000000000000000000..ef8cc4f885ff66bda0134cef0d6578ed9b0f2867 GIT binary patch literal 1193 zcmV;a1XlY(T4*^jL0KkKSrSN`$rl4p502&$%8Z-ut007Vc0iZO<000Jn27mwn z00000&<#&eN_v?#jHjlJ1qL7*0iXyM44E1RfC5PpOq!ligvsh?&>Kh@13=IK zGzNeG00003KmY&$&@=|r&;Sg80HOdPz32g%ASXs<5Dd&C{ENdq=pN>BU}Te-V3}yg zrL1BSO?F{9>^UW9ctFG8X?l1nAr4Hz6SCn`RyvWhECg05vSWsh6)ql9e!LHWXdy;I z5P%~PS3oVwiq^{EUi-_stN+tf*ZifT?Or9ij8>t9ZozA28(eVhSFSL&4h!KelY-ed z=13NbBjbSJsT!X74A%e%Q6UbqQ#S7QmPhg!q5v791qgr$5719Gd7S|Z=W4w&t4#A? zAw+!Ru}fNQF)e#PQ=3ioWuhJ>S-?P8Tdzel7&+*bkqug{6~st(OzvQ2T-|7ji&R}h z`bqcQ9|Pib;lxTpNIIcfmzpdo0ZBqahS6-Vu%d_%%jukXW>!m67U<_3Kv2Lk)5^xN z{4}$gnK}19<=cdv#tN(AZ@(}`U>dZf69Ofz7P8dy__VzI3Hu~AY$CjC!2%OjwBex-!ULkjposvH45=s- zd5NJShFkWhTvYQOk=sMB6lq{-Vh58X?^)nOD#9TVNwW}_UV>n;M`WG*xRqKi)h$&$ zRZbpiNKg}&`y_`#bj6>YLQ0S*$wPw842@Rw&2E9Nu9E<>sUZ>46+h=z)s_!!Qf$w; z5n)t8Ml+q>B}4P6uT)( z2)jv(F$0IDP9$ZdI>HldPgh?rWh^r`+20W9b}~_@=GpMJ4N6fcawgI>jzwe;A!wcO z;7J#Nz#oMrw4}gBG=Q>zvMI=P=2<|JPM%x57az20#!S9;j^GO@yKHox0JT9s|V z&Sr~5wUCI4V*rXPGfTEmGjL3BsFPLdN4|lJX>@Ovn5u_`%&kNKkR?2V2Ru@QoiQ|)- zmVerk?rz-Aap)A;PVa=+Yo3k01y)YYB}BH|M(!ielv@wa#doWV%PMjPM??6Pn|X9R zb?1peh?X@sHe|gshg_2by8S=smC`y-g0u#Grj4e-@Y%zfk`{jm&>#fh6P#Vi6yZWZ HNn~d9yb}!j literal 0 HcmV?d00001 diff --git a/data/test_response_solcast_method.pbz2 b/data/test_response_solcast_method.pbz2 new file mode 100644 index 0000000000000000000000000000000000000000..cd7da0313588bef465105cff9fcc5bcfc0ec5bfb GIT binary patch literal 1238 zcmV;{1S$JMT4*^jL0KkKSvnPnUH}3#fB*mg`}bCR{bc_?*1rGm|Mu(tU-SRX{r&B4 ze}4bB^}WyoSe~;7W`^K|lT_18O`}nb2-EchX{HEm1jt|}0x~cFCYm%D2-69)14#5H z0yM58Z-a^0qOt%000000QCR>4FCXW z0j7}(ZAMc~l*C~MMvO*HG%{#300x6V0MG%T8UdlEfB*mhX`l@TKr}J{0Rf)~sa-8#2=POd< z%CB3ER<93ZgLO&Dwa?#Ja>LG9$o*cK^&mj=%1}>9VK#=HrlbGI3^Eq1oL$Ou#g)&=SBVx zDTl1?Kbc;Lp&e@M*;g%7Q;P7z=F_^oCR=hm9h~R zpwJGLpzuf$rNBfMk(A&Zu)+~U4FF8OtC1&3%zaNX0KNs-)ki3riiBBf4A5|n5(*>D zm3Tp}ViPjcJggH6p&=+_ot_ojbbXB)%fW{VEx^bjh8K%z1*8Xo2P(UEpJ05myp4UZaN=c{D&|uOkFo`z!At>j<>RlYHb(uw@ zt_mL_mEDUbc$qX;Vj@kw&J9Hdo>NkQ71WurFd%zN&3xWe5!@8bs+3 z%`rxl8;x`6O)vR%5k3B}gbP1$7DXi-F+Mdj7HFB7)ly%(wWK5Lalj(RpFrBTp*vPe zhCe9F6ta27y4Z0Gt}=!=P!_O&;BqO9Tnoyc|IWuyd8z)Fk0P^ ze5&xlkrT{D>c?%v;dGaz2dKO%NQ^zcaH-M)9%&2YrX}XJI-A37Bx&^Qsl5&;(6tZn zz5*}hl%rjvJ4MnE*dPcRA;;4Y+f0gJfoh5(+dlbpBOISqhC3`r$mP+IJYN2Y%6(#z zP_M^^pI*s_oDay@NZdo|^aii`i31EM`nmM*?!Esab^ss|;6L$qBvXY61EE-j;G*#x Ai2wiq literal 0 HcmV?d00001 diff --git a/docs/forecasts.md b/docs/forecasts.md index 069d1b8e..f0681328 100644 --- a/docs/forecasts.md +++ b/docs/forecasts.md @@ -27,6 +27,13 @@ For example: curl -i -H 'Content-Type:application/json' -X POST -d '{"solcast_rooftop_id":"","solcast_api_key":""}' http://localhost:5000/action/dayahead-optim ``` +A thrd method uses the Solar.Forecast service. You will need to set `method=solar.forecast` and use just one parameter `solar_forecast_kwp` (the PV peak installed power in kW) that should be passed at runtime. + +For example, for a 5 kWp installation: +``` +curl -i -H 'Content-Type:application/json' -X POST -d '{"solar_forecast_kwp":5}' http://localhost:5000/action/dayahead-optim +``` + ## Load power forecast The default method for load forecast is a naive method, also called persistence. This is obtained using `method=naive`. This method simply assumes that the forecast for a future period will be equal to the observed values in a past period. The past period is controlled using parameter `delta_forecast` and the default value for this is 24h. diff --git a/secrets_emhass(example).yaml b/secrets_emhass(example).yaml index 60e1bc2c..312ae7b3 100644 --- a/secrets_emhass(example).yaml +++ b/secrets_emhass(example).yaml @@ -8,4 +8,5 @@ lat: 45.83 lon: 6.86 alt: 4807.8 solcast_api_key: yoursecretsolcastapikey -solcast_rooftop_id: yourrooftopid \ No newline at end of file +solcast_rooftop_id: yourrooftopid +solar_forecast_kwp: 5 \ No newline at end of file diff --git a/src/emhass/forecast.py b/src/emhass/forecast.py index 100b0b54..0ca01af3 100644 --- a/src/emhass/forecast.py +++ b/src/emhass/forecast.py @@ -170,10 +170,6 @@ def get_weather_forecast(self, method: Optional[str] = 'scrapper', freq=freq_scrap).round(freq_scrap) # Using the clearoutside webpage response = get("https://clearoutside.com/forecast/"+str(round(self.lat, 2))+"/"+str(round(self.lon, 2))+"?desktop=true") - '''import bz2 # Uncomment to save a serialized response for tests - import _pickle as cPickle - with bz2.BZ2File("test_response_scrapper_method.pbz2", "w") as f: - cPickle.dump(response, f)''' soup = BeautifulSoup(response.content, 'html.parser') table = soup.find_all(id='day_0')[0] list_names = table.find_all(class_='fc_detail_label') @@ -201,6 +197,10 @@ def get_weather_forecast(self, method: Optional[str] = 'scrapper', data['relative_humidity'] = raw_data['Relative Humidity (%)'] data['precipitable_water'] = pvlib.atmosphere.gueymard94_pw( data['temp_air'], data['relative_humidity']) + '''import bz2 # Uncomment to save a serialized data for tests + import _pickle as cPickle + with bz2.BZ2File("test_response_scrapper_method.pbz2", "w") as f: + cPickle.dump(data, f)''' elif method == 'solcast': # using solcast API # Retrieve data from the solcast API headers = { @@ -224,6 +224,36 @@ def get_weather_forecast(self, method: Optional[str] = 'scrapper', data = pd.DataFrame.from_dict(data_dict) # Define index data.set_index('ts', inplace=True) + '''import bz2 # Uncomment to save a serialized data for tests + import _pickle as cPickle + with bz2.BZ2File("test_response_solcast_method.pbz2", "w") as f: + cPickle.dump(data, f)''' + elif method == 'solar.forecast': # using the solar.forecast API + # Retrieve data from the solar.forecast API + headers = { + "Accept": "application/json" + } + url = "https://api.forecast.solar/estimate/"+str(round(self.lat, 2))+"/"+str(round(self.lon, 2))+\ + "/"+str(self.plant_conf["surface_tilt"])+"/"+str(self.plant_conf["surface_azimuth"]-180)+\ + "/"+str(self.retrieve_hass_conf["solar_forecast_kwp"]) + response = get(url, headers=headers) + data_raw = response.json() + data_dict = {'ts':list(data_raw['result']['watts'].keys()), 'yhat':list(data_raw['result']['watts'].values())} + # Form the final DataFrame + data = pd.DataFrame.from_dict(data_dict) + data.set_index('ts', inplace=True) + data.index = pd.to_datetime(data.index) + data = data.tz_localize(self.forecast_dates.tz) + data = data.reindex(index=self.forecast_dates) + mask_up_data_df = data.copy(deep=True).fillna(method = "ffill").isnull() + mask_down_data_df = data.copy(deep=True).fillna(method = "bfill").isnull() + data.interpolate(inplace=True) + data.loc[data.index[mask_up_data_df['yhat']==True],:] = 0.0 + data.loc[data.index[mask_down_data_df['yhat']==True],:] = 0.0 + '''import bz2 # Uncomment to save a serialized data for tests + import _pickle as cPickle + with bz2.BZ2File("test_response_solarforecast_method.pbz2", "w") as f: + cPickle.dump(data, f)''' elif method == 'csv': # reading from a csv file weather_csv_file_path = self.root + csv_path # Loading the csv file, we will consider that this is the PV power in W @@ -317,7 +347,8 @@ def get_power_from_weather(self, df_weather: pd.DataFrame, """ # If using csv method we consider that yhat is the PV power in W - if self.weather_forecast_method == 'solcast' or self.weather_forecast_method == 'csv' or self.weather_forecast_method == 'list': + if self.weather_forecast_method == 'solcast' or self.weather_forecast_method == 'solar.forecast' or \ + self.weather_forecast_method == 'csv' or self.weather_forecast_method == 'list': P_PV_forecast = df_weather['yhat'] P_PV_forecast.name = None else: # We will transform the weather data into electrical power diff --git a/src/emhass/utils.py b/src/emhass/utils.py index 9864c060..1019f478 100644 --- a/src/emhass/utils.py +++ b/src/emhass/utils.py @@ -197,8 +197,13 @@ def treat_runtimeparams(runtimeparams: str, params:str, retrieve_hass_conf: dict optim_conf['set_def_constant'] = runtimeparams['set_def_constant'] if 'solcast_api_key' in runtimeparams.keys(): retrieve_hass_conf['solcast_api_key'] = runtimeparams['solcast_api_key'] + optim_conf['weather_forecast_method'] = 'solcast' if 'solcast_rooftop_id' in runtimeparams.keys(): retrieve_hass_conf['solcast_rooftop_id'] = runtimeparams['solcast_rooftop_id'] + optim_conf['weather_forecast_method'] = 'solcast' + if 'solar_forecast_kwp' in runtimeparams.keys(): + retrieve_hass_conf['solar_forecast_kwp'] = runtimeparams['solar_forecast_kwp'] + optim_conf['weather_forecast_method'] = 'solar.forecast' params = json.dumps(params) return params, retrieve_hass_conf, optim_conf diff --git a/tests/test_forecast.py b/tests/test_forecast.py index eb753795..6d3ff9c8 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- import unittest -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pandas as pd import pathlib, pickle, json, copy, yaml import bz2 @@ -24,7 +24,7 @@ class TestForecast(unittest.TestCase): def setUp(self): self.get_data_from_file = True params = None - retrieve_hass_conf, optim_conf, plant_conf = get_yaml_parse(pathlib.Path(root+'/config_emhass.yaml'), use_secrets=False) + retrieve_hass_conf, optim_conf, plant_conf = get_yaml_parse(pathlib.Path(root+'/config_emhass.yaml'), use_secrets=True) self.retrieve_hass_conf, self.optim_conf, self.plant_conf = \ retrieve_hass_conf, optim_conf, plant_conf self.rh = retrieve_hass(self.retrieve_hass_conf['hass_url'], self.retrieve_hass_conf['long_lived_token'], @@ -47,7 +47,7 @@ def setUp(self): self.fcst = forecast(self.retrieve_hass_conf, self.optim_conf, self.plant_conf, params, root, logger, get_data_from_file=self.get_data_from_file) # The default for test is csv read - self.df_weather_scrap = self.fcst.get_weather_forecast(method='csv') # Still need to unittest these methods: 'scrapper','solcast','forecast.solar' + self.df_weather_scrap = self.fcst.get_weather_forecast(method='csv') self.P_PV_forecast = self.fcst.get_power_from_weather(self.df_weather_scrap) self.P_load_forecast = self.fcst.get_load_forecast(method=optim_conf['load_forecast_method']) self.df_input_data_dayahead = pd.concat([self.P_PV_forecast, self.P_load_forecast], axis=1) @@ -85,17 +85,13 @@ def test_get_weather_forecast(self): self.assertEqual(P_PV_forecast.index.tz, self.fcst.time_zone) self.assertEqual(len(self.df_weather_csv), len(P_PV_forecast)) - @patch('emhass.forecast.requests') - def test_get_weather_forecast_scrapper_method(self, mock_requests): + def test_get_weather_forecast_scrapper_method(self): data = bz2.BZ2File(str(pathlib.Path(root+'/data/test_response_scrapper_method.pbz2')), "rb") - response = cPickle.load(data) - mock_response = MagicMock() - mock_response.content = copy.deepcopy(response.content) - mock_response.status_code = 200 - # specify the return value of the get() method - mock_requests.get.return_value = mock_response + data = cPickle.load(data) + self.fcst.get_weather_forecast = MagicMock(return_value=data) df_weather_scrap = self.fcst.get_weather_forecast(method='scrapper') - self.assertEqual(self.fcst.weather_forecast_method, 'scrapper') + self.fcst.get_weather_forecast.assert_called_with(method='scrapper') + self.fcst.get_weather_forecast.assert_called_once() self.assertIsInstance(df_weather_scrap, type(pd.DataFrame())) self.assertIsInstance(df_weather_scrap.index, pd.core.indexes.datetimes.DatetimeIndex) self.assertIsInstance(df_weather_scrap.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype) @@ -104,6 +100,36 @@ def test_get_weather_forecast_scrapper_method(self, mock_requests): self.assertEqual(len(df_weather_scrap), int(self.optim_conf['delta_forecast'].total_seconds()/3600/self.fcst.timeStep)) + def test_get_weather_forecast_solcast_method(self): + data = bz2.BZ2File(str(pathlib.Path(root+'/data/test_response_solcast_method.pbz2')), "rb") + data = cPickle.load(data) + self.fcst.get_weather_forecast = MagicMock(return_value=data) + df_weather_solcast = self.fcst.get_weather_forecast(method='solcast') + self.fcst.get_weather_forecast.assert_called_with(method='solcast') + self.fcst.get_weather_forecast.assert_called_once() + self.assertIsInstance(df_weather_solcast, type(pd.DataFrame())) + self.assertIsInstance(df_weather_solcast.index, pd.core.indexes.datetimes.DatetimeIndex) + self.assertIsInstance(df_weather_solcast.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype) + self.assertEqual(df_weather_solcast.index.tz, self.fcst.time_zone) + self.assertTrue(self.fcst.start_forecast < ts for ts in df_weather_solcast.index) + self.assertEqual(len(df_weather_solcast), + int(self.optim_conf['delta_forecast'].total_seconds()/3600/self.fcst.timeStep)) + + def test_get_weather_forecast_solcast_method(self): + data = bz2.BZ2File(str(pathlib.Path(root+'/data/test_response_solarforecast_method.pbz2')), "rb") + data = cPickle.load(data) + self.fcst.get_weather_forecast = MagicMock(return_value=data) + df_weather_solarforecast = self.fcst.get_weather_forecast(method='solar.forecast') + self.fcst.get_weather_forecast.assert_called_with(method='solar.forecast') + self.fcst.get_weather_forecast.assert_called_once() + self.assertIsInstance(df_weather_solarforecast, type(pd.DataFrame())) + self.assertIsInstance(df_weather_solarforecast.index, pd.core.indexes.datetimes.DatetimeIndex) + self.assertIsInstance(df_weather_solarforecast.index.dtype, pd.core.dtypes.dtypes.DatetimeTZDtype) + self.assertEqual(df_weather_solarforecast.index.tz, self.fcst.time_zone) + self.assertTrue(self.fcst.start_forecast < ts for ts in df_weather_solarforecast.index) + self.assertEqual(len(df_weather_solarforecast), + int(self.optim_conf['delta_forecast'].total_seconds()/3600/self.fcst.timeStep)) + def test_get_forecasts_with_lists(self): with open(root+'/config_emhass.yaml', 'r') as file: params = yaml.load(file, Loader=yaml.FullLoader)