diff --git a/bin/user/weewx_wdc.py b/bin/user/weewx_wdc.py index 86235e46..ceb16be8 100644 --- a/bin/user/weewx_wdc.py +++ b/bin/user/weewx_wdc.py @@ -26,7 +26,7 @@ from weewx.wxformulas import beaufort from weeutil.weeutil import TimeSpan, rounder, to_bool, to_int, startOfDay, startOfArchiveDay from weeutil.config import search_up, accumulateLeaves - +from weewx.tags import TimespanBinder try: import weeutil.logger @@ -354,7 +354,20 @@ def __init__(self, generator): "wx_binding" ) - # print(self.generator.config_dict["StdWXCalculate"]["Calculations"]) + def format_raw_value(self, value, obs): + """ + Returns a ValueHelper for a raw value and an obs type. + + Args: + value (float): The value + obs (string): The observation + """ + target_unit_t = self.generator.converter.getTargetUnit( + obs_type=obs) + value_vt = (value, target_unit_t[0], target_unit_t[1]) + + return ValueHelper(value_t=value_vt, formatter=self.generator.formatter) + def get_software_obs(self): """ @@ -1148,6 +1161,48 @@ def get_dwd_warnings(self): class WdcArchiveUtil(SearchList): + def __init__(self, generator): + SearchList.__init__(self, generator) + + def get_extension_list(self, timespan, db_lookup): + """Returns a search list extension with two additions. + + Parameters: + timespan: An instance of weeutil.weeutil.TimeSpan. This will + hold the start and stop times of the domain of + valid times. + + db_lookup: This is a function that, given a data binding + as its only parameter, will return a database manager + object. + """ + + self.db_lookup = db_lookup + + search_list_extension = { + "get_stat_table_month_obs": self.get_stat_table_month_obs, + "get_day_archive_enabled": self.get_day_archive_enabled, + "get_archive_days_array": self.get_archive_days_array, + "filter_months": self.filter_months, + "fake_get_report_years": self.fake_get_report_years, + } + + return [search_list_extension] + + def get_stat_table_month_obs(self, start_ts, end_ts): + """ + Returns a TimeSpanBinder a period. + + Args: + start_ts (int): The start timestamp + end_ts (int): The end timestamp + """ + month_timespan = TimeSpan(start_ts, end_ts) + month_timespan_binder = TimespanBinder(month_timespan, self.db_lookup, + formatter=self.generator.formatter, + converter=self.generator.converter) + return month_timespan_binder + def get_day_archive_enabled(self): """ Get day archive enabled. diff --git a/changelog.md b/changelog.md index 6d43603d..e0e17b01 100644 --- a/changelog.md +++ b/changelog.md @@ -765,3 +765,4 @@ See https://github.com/Daveiano/weewx-wdc/compare/v3.3.0...580071ca175a03fe4924b - Updated sorting of min/max graphs in the `temp_min_max_avg` graph GH-247 - Added new property for charts: `legendPosition` - Change legend position GH-228 - Added option to set default theme (light/dark/auto) GH-241 +- added per-month statistic tables GH-215 diff --git a/install.py b/install.py index efdfdace..8ab40330 100644 --- a/install.py +++ b/install.py @@ -92,6 +92,7 @@ def __init__(self): "skins/weewx-wdc/includes/stat-tile.inc", "skins/weewx-wdc/includes/stat-tile-xaggs.inc", "skins/weewx-wdc/includes/stat-tile-modals.inc", + "skins/weewx-wdc/includes/stat-table.inc", "skins/weewx-wdc/includes/conditions-table.inc", "skins/weewx-wdc/includes/climatological-days.inc", "skins/weewx-wdc/includes/ui-shell.inc", diff --git a/skins/weewx-wdc/includes/climatological-days.inc b/skins/weewx-wdc/includes/climatological-days.inc index 6b3ce7bd..9c8c956d 100644 --- a/skins/weewx-wdc/includes/climatological-days.inc +++ b/skins/weewx-wdc/includes/climatological-days.inc @@ -1,4 +1,3 @@ - #errorCatcher Echo #encoding UTF-8 #from datetime import datetime @@ -28,7 +27,7 @@ type="module" src="https://1.www.s81c.com/common/carbon/web-components/version/v1.21.0/tabs.min.js" > - + $gettext('Table') #if $getattr($get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday), 'rain').has_data @@ -52,6 +51,20 @@ >$gettext('Climatogram') #end if + + #if 'stat_tables' in $DisplayOptions + #for $stat_table in $DisplayOptions.get('stat_tables') + #if $getattr($get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday), $DisplayOptions['stat_tables'][$stat_table]['observation']).has_data + + $DisplayOptions['stat_tables'][$stat_table]['label'] + + #end if + #end for + #end if
@@ -81,7 +94,6 @@ $gettext($clim_day) - #set $time_span = $get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday) $get_climatological_day($clim_day, $time_span.start.raw, $time_span.end.raw) @@ -90,21 +102,18 @@

- $get_climatological_day_description($clim_day)

- $get_climatological_day_description($clim_day) #end for - #if $context == 'alltime' - #set $start_dt = datetime.fromtimestamp($alltime.start.raw) #set $end_dt = datetime.fromtimestamp($latest.dateTime.raw) #set $delta = $end_dt - $start_dt @@ -134,12 +142,10 @@ - #if $show_climatological_days_per_month #include "includes/year-stats-table.inc" #end if - #if $getattr($get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday), 'rain').has_data
- #if $context == 'alltime' and $to_bool($DisplayOptions.get('show_last_rain', True)) and $last_rain is not None @@ -213,7 +218,6 @@ #end if @@ -227,7 +231,6 @@ >
#end if - #if $getattr($get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday), 'outTemp').has_data #end if - + #if $show_climatogram_year or $show_climatogram_alltime -
#end if + + #if 'stat_tables' in $DisplayOptions + #for $stat_table in $DisplayOptions.get('stat_tables') + #if $getattr($get_time_span_from_context($context, $day, $week, $month, $year, $alltime, $yesterday), $DisplayOptions['stat_tables'][$stat_table]['observation']).has_data + + #end if + #end for + #end if
diff --git a/skins/weewx-wdc/includes/stat-table.inc b/skins/weewx-wdc/includes/stat-table.inc new file mode 100644 index 00000000..8ead48d6 --- /dev/null +++ b/skins/weewx-wdc/includes/stat-table.inc @@ -0,0 +1,153 @@ +#errorCatcher Echo +#encoding UTF-8 + +#import calendar +#import datetime +#from weeutil.weeutil import startOfDay, startOfArchiveDay + + + +#if $context == 'alltime' + #set $archive_months = $SummaryByMonth + #if len($SummaryByYear) == 0 + #set $SummaryByYear = $fake_get_report_years($alltime.start.format("%Y"), $latest.dateTime.format("%Y")) + #end if +#elif $context == 'year' + #set $SummaryByYear = [str($year_name)] + #set $archive_months = $filter_months($SummaryByMonth, $year_name) +#end if + +#for $agg in $stat_table_aggregate_types +

+ $gettext($agg.capitalize()) $obs.label[$stat_table_obs] +

+ + #set $per_month = {} + #set $per_year = {} + #set $per_all = [] + + + + + #if $context == "alltime" + + $gettext('Year') + + #end if + + #for $month in range(1, 13) + + $gettext(calendar.month_abbr[$month]) + + #end for + + + $gettext($agg.capitalize()) + + + + + #for $year in $SummaryByYear + #if not $year in $per_year + #set $per_year[$year] = [] + #end if + + #if $context == "alltime" + $year + #end if + + #set $months_for_year = $filter_months($archive_months, $year) + + #for $month in range(1, 13) + #if not $month in $per_month + #set $per_month[$month] = [] + #end if + + #set $month_from_range = $year + '-' + str($month).zfill(2) + #set $time_span_start_ts = $startOfDay($datetime.datetime.strptime($month_from_range, "%Y-%m").replace(day=1).timestamp()) + #set $time_span_end_ts = $datetime.datetime.strptime($month_from_range, "%Y-%m").replace(day=$calendar.monthrange(int($year), int($month))[1], hour=23, minute=59).timestamp() + + #if $month_from_range in $months_for_year + #set $agg_for_value = $agg + + #if $stat_table_obs in $DisplayOptions.get('stat_tiles_show_sum', ['rain', 'ET', 'hail', 'snow', 'lightning_strike_count', 'windrun']) + #set $agg_for_value = 'sum' + #end if + + #set $stat_month = $get_stat_table_month_obs($time_span_start_ts, $time_span_end_ts) + #set $raw_value = $getattr($getattr($stat_month, $stat_table_obs), $agg_for_value).raw + + #silent $per_month[$month].append($raw_value) + #silent $per_year[$year].append($raw_value) + #silent $per_all.append($raw_value) + + + $getattr($getattr($stat_month, $stat_table_obs), $agg_for_value) + + #else + - + #end if + #end for + + + #if $year in $per_year + + #if $agg == 'max' + $format_raw_value($max($per_year[$year]), $stat_table_obs) + #elif $agg == 'min' + $format_raw_value($min($per_year[$year]), $stat_table_obs) + #elif $agg == 'sum' + $format_raw_value($sum($per_year[$year]), $stat_table_obs) + #elif $agg == 'avg' + $format_raw_value($sum($per_year[$year])/$len($per_year[$year]), $stat_table_obs) + #end if + + #else + - + #end if + + + #end for + + + $gettext($agg.capitalize()) + + + #for $month in range(1, 13) + #if $per_month[$month] + + #if $agg == 'max' + $format_raw_value($max($per_month[$month]), $stat_table_obs) + #elif $agg == 'min' + $format_raw_value($min($per_month[$month]), $stat_table_obs) + #elif $agg == 'sum' + $format_raw_value($sum($per_month[$month]), $stat_table_obs) + #elif $agg == 'avg' + $format_raw_value($sum($per_month[$month])/$len($per_month[$month]), $stat_table_obs) + #end if + + #else + - + #end if + #end for + + + + #if $agg == 'max' + $format_raw_value($max($per_all), $stat_table_obs) + #elif $agg == 'min' + $format_raw_value($min($per_all), $stat_table_obs) + #elif $agg == 'sum' + $format_raw_value($sum($per_all), $stat_table_obs) + #elif $agg == 'avg' + $format_raw_value($sum($per_all)/$len($per_all), $stat_table_obs) + #end if + + + + + +#end for \ No newline at end of file diff --git a/skins/weewx-wdc/includes/year-stats-table.inc b/skins/weewx-wdc/includes/year-stats-table.inc index 32478150..27806b19 100644 --- a/skins/weewx-wdc/includes/year-stats-table.inc +++ b/skins/weewx-wdc/includes/year-stats-table.inc @@ -1,4 +1,3 @@ - #errorCatcher Echo #encoding UTF-8 @@ -19,7 +18,6 @@ > - #for $clim_day in $DisplayOptions.get('climatological_days') #set $clim_day_class = $clim_day.replace(' ', '_').lower()
- #for $clim_day in $DisplayOptions.get('climatological_days') #set $clim_day_class = $clim_day.replace(' ', '_').lower() -
{ + test.beforeEach(async ({ page }) => { + await page.goto( + "artifacts-alternative-weewx-html/public_html/statistics.html" + ); + }); + + const outTempMinTests = [ + { + testid: "month-2021-10-min-outTemp", + expected: "2.9°C", + }, + { + testid: "month-2022-06-min-outTemp", + expected: "8.2°C", + }, + { + testid: "year-2021-min-outTemp", + expected: "-9.3°C", + }, + { + testid: "year-2022-min-outTemp", + expected: "-4.8°C", + }, + { + testid: "agg-month-3-min-outTemp", + expected: "-4.0°C", + }, + { + testid: "total-min-outTemp", + expected: "-9.3°C", + }, + ]; + + const outTempAvgTests = [ + { + testid: "month-2022-04-avg-outTemp", + expected: "7.5°C", + }, + { + testid: "month-2021-12-avg-outTemp", + expected: "1.6°C", + }, + { + testid: "year-2021-avg-outTemp", + expected: "5.3°C", + }, + { + testid: "year-2022-avg-outTemp", + expected: "8.5°C", + }, + { + testid: "agg-month-4-avg-outTemp", + expected: "7.5°C", + }, + { + testid: "total-avg-outTemp", + expected: "7.5°C", + }, + ]; + + const outTempMaxTests = [ + { + testid: "month-2021-10-max-outTemp", + expected: "18.7°C", + }, + { + testid: "month-2022-06-max-outTemp", + expected: "37.3°C", + }, + { + testid: "year-2021-max-outTemp", + expected: "18.7°C", + }, + { + testid: "year-2022-max-outTemp", + expected: "37.3°C", + }, + { + testid: "agg-month-5-max-outTemp", + expected: "27.1°C", + }, + { + testid: "total-max-outTemp", + expected: "37.3°C", + }, + ]; + + const rainTotalTests = [ + { + testid: "month-2021-10-sum-rain", + expected: "0.25 cm", + }, + { + testid: "month-2022-06-sum-rain", + expected: "1.67 cm", + }, + { + testid: "year-2021-sum-rain", + expected: "11.27 cm", + }, + { + testid: "year-2022-sum-rain", + expected: "18.74 cm", + }, + { + testid: "agg-month-5-sum-rain", + expected: "2.03 cm", + }, + { + testid: "total-sum-rain", + expected: "30.01 cm", + }, + ]; + + const rainAvgTests = [ + { + testid: "month-2021-10-avg-rain", + expected: "0.25 cm", + }, + { + testid: "month-2022-06-avg-rain", + expected: "1.67 cm", + }, + { + testid: "year-2021-avg-rain", + expected: "3.76 cm", + }, + { + testid: "year-2022-avg-rain", + expected: "3.12 cm", + }, + { + testid: "agg-month-5-avg-rain", + expected: "2.03 cm", + }, + { + testid: "total-avg-rain", + expected: "3.33 cm", + }, + ]; + + test("Out temp min", async ({ page }) => { + const panel = page.locator("#panel-tables_outtemp"); + const outTempMinTable = panel.getByTestId("min-outTemp"); + + await page.locator("bx-tab[value='tables_outtemp']").click(); + await expect(panel).toBeVisible(); + await expect(outTempMinTable).toBeVisible(); + + for (const { testid, expected } of outTempMinTests) { + const outTempMin = outTempMinTable.getByTestId(testid); + await expect(outTempMin).toContainText(expected); + } + }); + + test("Out temp avg", async ({ page }) => { + const panel = page.locator("#panel-tables_outtemp"); + const outTempAvgTable = panel.getByTestId("avg-outTemp"); + + await page.locator("bx-tab[value='tables_outtemp']").click(); + await expect(panel).toBeVisible(); + await expect(outTempAvgTable).toBeVisible(); + + for (const { testid, expected } of outTempAvgTests) { + const outTempAvg = outTempAvgTable.getByTestId(testid); + await expect(outTempAvg).toContainText(expected); + } + }); + + test("Out temp max", async ({ page }) => { + const panel = page.locator("#panel-tables_outtemp"); + const outTempMaxTable = panel.getByTestId("max-outTemp"); + + await page.locator("bx-tab[value='tables_outtemp']").click(); + await expect(panel).toBeVisible(); + await expect(outTempMaxTable).toBeVisible(); + + for (const { testid, expected } of outTempMaxTests) { + const outTempMax = outTempMaxTable.getByTestId(testid); + await expect(outTempMax).toContainText(expected); + } + }); + + test("Rain total", async ({ page }) => { + const panel = page.locator("#panel-tables_rain"); + const rainTotalTable = panel.getByTestId("sum-rain"); + + await page.locator("bx-tab[value='tables_rain']").click(); + await expect(panel).toBeVisible(); + await expect(rainTotalTable).toBeVisible(); + + for (const { testid, expected } of rainTotalTests) { + const rainTotal = rainTotalTable.getByTestId(testid); + await expect(rainTotal).toContainText(expected); + } + }); + + test("Rain avg", async ({ page }) => { + const panel = page.locator("#panel-tables_rain"); + const rainAvgTable = panel.getByTestId("avg-rain"); + + await page.locator("bx-tab[value='tables_rain']").click(); + await expect(panel).toBeVisible(); + await expect(rainAvgTable).toBeVisible(); + + for (const { testid, expected } of rainAvgTests) { + const rainAvg = rainAvgTable.getByTestId(testid); + await expect(rainAvg).toContainText(expected); + } + }); +}); diff --git a/test/test_install_report/src/skin.conf b/test/test_install_report/src/skin.conf index 94f5850d..46405386 100644 --- a/test/test_install_report/src/skin.conf +++ b/test/test_install_report/src/skin.conf @@ -100,6 +100,16 @@ SKIN_VERSION = 2.3.0 climatogram_enable_stats = True climatogram_enable_year_stats = True + [[stat_tables]] + [[[tables_outtemp]]] + observation = "outTemp" + label = "Temperature Stat Table" + aggregate_types = "min", "avg", "max" + [[[tables_rain]]] + observation = "rain" + label = "Rain Stat Table" + aggregate_types = "sum", "avg" + [[tables]] [[[day]]] aggregate_interval = 3600 # 1 hour