diff --git a/docs_src/changes.md b/docs_src/changes.md index a0721080..cf6edb29 100644 --- a/docs_src/changes.md +++ b/docs_src/changes.md @@ -16,6 +16,10 @@ Example `xstats.py` had nonsensical time context values. Fixed in [PR #1072](https://github.com/weewx/weewx/pull/1072). Thanks to user evilbunny2008. +Added Cheetah generator seasons timespans `$season`, `$seasonsyear`, and seasons +iterator `.seasons`. [PR #1095](https://github.com/weewx/weewx/pull/1095). +Thanks to user roe-dl. + Prevent `AttributeError` when the `weectl` command is run without a subcommand. Guard against an empty string being passed to the copy generator. diff --git a/docs_src/custom/cheetah-generator.md b/docs_src/custom/cheetah-generator.md index 4452ec25..1f6571ee 100644 --- a/docs_src/custom/cheetah-generator.md +++ b/docs_src/custom/cheetah-generator.md @@ -446,6 +446,12 @@ There are several _aggregation periods_ that can be used: $month.outTemp.min The minimum temperature this month. + + $season + This season + $season.outTemp.avg + The average temparature this season so far + $year This year (since 1-Jan). @@ -460,6 +466,13 @@ There are several _aggregation periods_ that can be used: $rainyear.rain.sum The total rainfall for this rain year. + + $seasonsyear + From the beginning of the last spring to the beginning of the + next spring, observing hemisphere + $seasonsyear.outTemp.min + The minimum temperature during that time + $alltime @@ -509,15 +522,38 @@ Here are some examples: $month(months_ago=1).outTemp.max The maximum temperature last month. + + $season(seasons_ago=s) + $season(seasons_ago=1).outTemp.min + The minimum temperature of the previous season + + + $season(years_ago=y) + $season(years_ago=1).outTemp.min + The minimum temperature of the same season in the previous year + $year(years_ago=y) $year(years_ago=1).outTemp.max The maximum temperature last year. + + y + $seasonsyear(years_ago=1).outTemp.max + The maximum temperature of the previous seasons year + +There are two kinds of seasons: meterological and astronomical. The +meteorological season always starts at the 1st of March, June, September, +and December. The astronomical season starts at equinox or solstice. +There is an optional parameter `season_type` for `$season` and `$seasonsyear` +to specify, which one of them is to be used. Set it to `meteorological` +(the default if not provided) or `astronomical`. Both words can be +abbreviated. + ### Unit conversion options The option _`unit_conversion`_ can be used with either current observations @@ -1233,6 +1269,11 @@ It is possible to iterate over the following: .months Iterate by months + + .seasons + Iterate by seasons. Depending on the parameter `season_type` of + `$seasonsyear`, meteorological or astronomical seasons are returned. + .years Iterate by years diff --git a/src/weeutil/weeutil.py b/src/weeutil/weeutil.py index 30bf21c5..9950a45d 100644 --- a/src/weeutil/weeutil.py +++ b/src/weeutil/weeutil.py @@ -215,6 +215,7 @@ nominal_intervals = { 'day': 86400, 'week': 7 * 86400, 'month': int(365.25 / 12 * 86400), + 'season': int(365.25 / 4 * 86400), 'year': int(365.25 * 86400), } duration_synonyms = { diff --git a/src/weewx/almanac.py b/src/weewx/almanac.py index 33399fdc..10676df7 100644 --- a/src/weewx/almanac.py +++ b/src/weewx/almanac.py @@ -215,7 +215,7 @@ class Almanac: Args: - time_ts (int): A unix epoch timestamp with the time of the almanac. If None, the + time_ts (int|float): A unix epoch timestamp with the time of the almanac. If None, the present time will be used. lat (float): Observer's latitude in degrees. lon (float): Observer's longitude in degrees. diff --git a/src/weewx/cheetahgenerator.py b/src/weewx/cheetahgenerator.py index b7382f8f..f046402b 100644 --- a/src/weewx/cheetahgenerator.py +++ b/src/weewx/cheetahgenerator.py @@ -600,6 +600,8 @@ class Stats(SearchList): converter=self.generator.converter, week_start=self.generator.stn_info.week_start, rain_year_start=self.generator.stn_info.rain_year_start, + lat = self.generator.stn_info.latitude_f, + lon = self.generator.stn_info.longitude_f, trend=trend_dict, skin_dict=self.generator.skin_dict) diff --git a/src/weewx/defaults.py b/src/weewx/defaults.py index 1b516633..5d033327 100644 --- a/src/weewx/defaults.py +++ b/src/weewx/defaults.py @@ -225,8 +225,10 @@ log_failure = False day = %X week = %X (%A) month = %x %X + season = %x %X year = %x %X rainyear = %x %X + seasonsyear = %x %X current = %x %X ephem_day = %X ephem_year = %x %X diff --git a/src/weewx/tags.py b/src/weewx/tags.py index c73817cb..6fba35ab 100644 --- a/src/weewx/tags.py +++ b/src/weewx/tags.py @@ -5,9 +5,11 @@ # """Classes for implementing the weewx tag 'code' codes.""" +import time import weeutil.weeutil import weewx.units import weewx.xtypes +import weewx.almanac from weeutil.weeutil import to_int from weewx.units import ValueTuple @@ -85,7 +87,8 @@ class TimeBinder: def week(self, data_binding=None, weeks_ago=0): week_start = to_int(self.option_dict.get('week_start', 6)) return TimespanBinder( - weeutil.weeutil.archiveWeekSpan(self.report_time, startOfWeek=week_start, weeks_ago=weeks_ago), + weeutil.weeutil.archiveWeekSpan(self.report_time, startOfWeek=week_start, + weeks_ago=weeks_ago), self.db_lookup, data_binding=data_binding, context='week', formatter=self.formatter, converter=self.converter, **self.option_dict) @@ -97,6 +100,57 @@ class TimeBinder: context='month', formatter=self.formatter, converter=self.converter, **self.option_dict) + def season(self, data_binding=None, seasons_ago=0, years_ago=0, lat=None, lon=None, + season_type='m'): + years_ago, seasons_ago = seasons_ago // 4 + years_ago, seasons_ago % 4 + if season_type[0].lower() == 'm': + # meteorological season + time_dt = time.localtime(self.report_time) + start_year = time_dt.tm_year - years_ago + start_month = int(time_dt.tm_mon // 3 - seasons_ago) * 3 + while start_month <= 0: + start_month += 12 + start_year -= 1 + end_year = start_year + end_month = start_month + 3 + if end_month > 12: + end_month -= 12 + end_year += 1 + start_of_season = int(time.mktime((start_year, start_month, 1, 0, 0, 0, 0, 0, -1))) + end_of_season = int(time.mktime((end_year, end_month, 1, 0, 0, 0, 0, 0, -1))) + elif season_type[0].lower() == 'a': + # astronomical season + if lat is None: lat = self.option_dict.get('lat', 0.0) + if lon is None: lon = self.option_dict.get('lon', 0.0) + time_ts = self.report_time + if years_ago: + time_td = time.localtime(time_ts) + time_ts = time.mktime( + (time_td.tm_year - years_ago, time_td.tm_mon, time_td.tm_mday, time_td.tm_hour, + time_td.tm_min, time_td.tm_sec, 0, 0, time_td.tm_isdst)) + for i in range(seasons_ago, -1, -1): + alm = weewx.almanac.Almanac(time_ts, lat, lon) + equinox = alm.previous_equinox.unix_epoch.raw + solstice = alm.previous_solstice.unix_epoch.raw + if not i: break + time_ts = (equinox if equinox > solstice else solstice) - 90000 + if equinox > solstice: + # spring or autumn + start_of_season = weeutil.weeutil.startOfDay(equinox) + end_of_season = weeutil.weeutil.startOfDay(alm.next_solstice.unix_epoch.raw) + else: + # summer or winter + start_of_season = weeutil.weeutil.startOfDay(solstice) + end_of_season = weeutil.weeutil.startOfDay(alm.next_equinox.unix_epoch.raw) + else: + raise ValueError("season type can be 'meteorological' or 'astronomical'") + return TimespanBinder( + weeutil.weeutil.TimeSpan(start_of_season, end_of_season), + self.db_lookup, data_binding=data_binding, + context='season', formatter=self.formatter, converter=self.converter, + season_type=season_type, + **self.option_dict) + def year(self, data_binding=None, years_ago=0): return TimespanBinder( weeutil.weeutil.archiveYearSpan(self.report_time, years_ago=years_ago), @@ -123,6 +177,45 @@ class TimeBinder: context='rainyear', formatter=self.formatter, converter=self.converter, **self.option_dict) + def seasonsyear(self, data_binding=None, years_ago=0, lat=None, lon=None, season_type='m'): + """ meteorological or astronomical seasons year """ + if lat is None: lat = self.option_dict.get('lat', 0.0) + if lon is None: lon = self.option_dict.get('lon', 0.0) + if season_type[0].lower() == 'm': + # meteorological seasons + seasons_start_month = 9 if lat < 0.0 else 3 + time_dt = time.localtime(self.report_time) + year = (time_dt.tm_year if time_dt.tm_mon >= seasons_start_month else time_dt.tm_year - 1) - years_ago + start_of_seasons_year = int(time.mktime((year, seasons_start_month, 1, 0, 0, 0, 0, 0, -1))) + end_of_seasons_year = int(time.mktime((year + 1, seasons_start_month, 1, 0, 0, 0, 0, 0, -1))) + elif season_type[0].lower() == 'a': + # astronomical seasons + time_ts = self.report_time + if years_ago: + time_td = time.localtime(time_ts) + time_ts = time.mktime( + (time_td.tm_year - years_ago, time_td.tm_mon, time_td.tm_mday, time_td.tm_hour, + time_td.tm_min, time_td.tm_sec, 0, 0, time_td.tm_isdst)) + alm = weewx.almanac.Almanac(time_ts, lat, lon) + if lat < 0.0: + start_of_seasons_year = weeutil.weeutil.startOfDay( + alm.previous_autumnal_equinox.unix_epoch.raw) + end_of_seasons_year = weeutil.weeutil.startOfDay( + alm.next_autumnal_equinox.unix_epoch.raw) + else: + start_of_seasons_year = weeutil.weeutil.startOfDay( + alm.previous_vernal_equinox.unix_epoch.raw) + end_of_seasons_year = weeutil.weeutil.startOfDay( + alm.next_vernal_equinox.unix_epoch.raw) + else: + raise ValueError("season type can be 'meteorological' or 'astronomical'") + return TimespanBinder( + weeutil.weeutil.TimeSpan(start_of_seasons_year, end_of_seasons_year), + self.db_lookup, data_binding=data_binding, + context='seasonsyear', formatter=self.formatter, converter=self.converter, + season_type=season_type, + **self.option_dict) + def span(self, data_binding=None, time_delta=0, hour_delta=0, day_delta=0, week_delta=0, month_delta=0, year_delta=0, boundary=None): return TimespanBinder( @@ -237,6 +330,42 @@ class TimespanBinder: 'year', self.formatter, self.converter, **self.option_dict) + # Iterate over seasons in the time period: + def seasons(self): + season_type = self.option_dict.get('season_type', 'm')[0].lower() + if season_type == 'm': + # meteorological season + def genSeasonSpans(start, stop): + while start < stop: + start_dt = time.localtime(start) + end_year = start_dt.tm_year + end_month = (start_dt.tm_mon // 3 + 1) * 3 + if end_month > 12: + end_year += 1 + end_month -= 12 + end = int(time.mktime((end_year, end_month, 1, 0, 0, 0, 0, 0, -1))) + yield weeutil.weeutil.TimeSpan(start, end) + start = end + elif season_type == 'a': + # astronomical season + lat = self.option_dict.get('lat', 0.0) + lon = self.option_dict.get('lon', 0.0) + + def genSeasonSpans(start, stop): + while start < stop: + alm = weewx.almanac.Almanac(start + 90000, lat, lon) + equinox = weeutil.weeutil.startOfDay(alm.next_equinox.unix_epoch.raw) + solstice = weeutil.weeutil.startOfDay(alm.next_solstice.unix_epoch.raw) + end = equinox if equinox < solstice else solstice + yield weeutil.weeutil.TimeSpan(start, end) + start = end + else: + raise ValueError("season type can be 'meteorological' or 'astronomical'") + return TimespanBinder._seqGenerator(genSeasonSpans, self.timespan, + self.db_lookup, self.data_binding, + 'season', self.formatter, self.converter, + **self.option_dict) + # Static method used to implement the iteration: @staticmethod def _seqGenerator(genSpanFunc, timespan, *args, **option_dict): @@ -259,7 +388,8 @@ class TimespanBinder: # Return the length of the timespan @property def length(self): - val = weewx.units.ValueTuple(self.timespan.stop-self.timespan.start, 'second', 'group_deltatime') + val = weewx.units.ValueTuple(self.timespan.stop - self.timespan.start, 'second', + 'group_deltatime') return weewx.units.ValueHelper(val, self.context, self.formatter, self.converter) # Alias for the start time: diff --git a/src/weewx/tests/test_daily.py b/src/weewx/tests/test_daily.py index b87061f3..270239fa 100644 --- a/src/weewx/tests/test_daily.py +++ b/src/weewx/tests/test_daily.py @@ -258,6 +258,12 @@ def testTags(config_dict): assert str(tagStats.month().barometer.max) == "31.000 inHg" assert str(tagStats.month().barometer.mintime) == "03/03/10 00:00:00" assert str(tagStats.month().barometer.maxtime) == "03/05/10 00:00:00" + assert str(tagStats.season().barometer.avg) == "30.000 inHg" + assert str(tagStats.season().barometer.min) == "29.000 inHg" + assert str(tagStats.season().barometer.max) == "31.000 inHg" + assert str(tagStats.season(season_type='astronomical').barometer.avg) == "30.007 inHg" + assert str(tagStats.season(season_type='astronomical').barometer.min) == "29.000 inHg" + assert str(tagStats.season(season_type='astronomical').barometer.max) == "31.000 inHg" assert str(tagStats.year().barometer.avg) == "29.996 inHg" assert str(tagStats.year().barometer.min) == "29.000 inHg" assert str(tagStats.year().barometer.max) == "31.000 inHg" @@ -278,6 +284,12 @@ def testTags(config_dict): assert str(tagStats.month().outTemp.max) == "58.9°F" assert str(tagStats.month().outTemp.mintime) == "03/01/10 03:00:00" assert str(tagStats.month().outTemp.maxtime) == "03/31/10 16:00:00" + assert str(tagStats.season().outTemp.avg) == "48.4°F" + assert str(tagStats.season().outTemp.min) == "-1.0°F" + assert str(tagStats.season().outTemp.max) == "94.1°F" + assert str(tagStats.season(season_type='astronomical').outTemp.avg) == "59.4°F" + assert str(tagStats.season(season_type='astronomical').outTemp.min) == "11.0°F" + assert str(tagStats.season(season_type='astronomical').outTemp.max) == "99.2°F" assert str(tagStats.year().outTemp.avg) == "48.3°F" assert str(tagStats.year().outTemp.min) == "-20.0°F" assert str(tagStats.year().outTemp.max) == "100.0°F" diff --git a/src/weewx_data/skins/Seasons/lang/cz.conf b/src/weewx_data/skins/Seasons/lang/cz.conf index ff6fd4e6..dbf33fa4 100644 --- a/src/weewx_data/skins/Seasons/lang/cz.conf +++ b/src/weewx_data/skins/Seasons/lang/cz.conf @@ -190,6 +190,7 @@ unit_system = metricwx "Right ascension" = "Vzestup" "Rise" = "Východ" "RMS Wind" = "Efektivní hodnota větru" + "Season" = "Roční období" "select month" = "Vyberte měsíc" "select year" = "Vyberte rok" "Sensor Status" = "Stav senzoru" diff --git a/src/weewx_data/skins/Seasons/lang/de.conf b/src/weewx_data/skins/Seasons/lang/de.conf index 0e568b62..48df480a 100644 --- a/src/weewx_data/skins/Seasons/lang/de.conf +++ b/src/weewx_data/skins/Seasons/lang/de.conf @@ -189,6 +189,7 @@ unit_system = metricwx "Right ascension" = "Rektaszension" "Rise" = "Aufgang" "RMS Wind" = "Windstärke (Effektivwert)" + "Season" = "Jahreszeit" "select month" = "Monat wählen" "select year" = "Jahr wählen" "Sensor Status" = "Sensorenstatus" diff --git a/src/weewx_data/skins/Seasons/lang/en.conf b/src/weewx_data/skins/Seasons/lang/en.conf index f7e70241..42d5272f 100644 --- a/src/weewx_data/skins/Seasons/lang/en.conf +++ b/src/weewx_data/skins/Seasons/lang/en.conf @@ -192,6 +192,7 @@ unit_system = us "Right ascension" = "Right ascension" "Rise" = "Rise" "RMS Wind" = "RMS Wind" + "Season" = "Season" "select month" = "Select Month" "select year" = "Select Year" "Sensor Status" = "Sensor Status" diff --git a/src/weewx_data/skins/Seasons/lang/fr.conf b/src/weewx_data/skins/Seasons/lang/fr.conf index b41a1001..0e3533d6 100644 --- a/src/weewx_data/skins/Seasons/lang/fr.conf +++ b/src/weewx_data/skins/Seasons/lang/fr.conf @@ -191,6 +191,7 @@ unit_system = metricwx "Right ascension" = "Ascension droite" "Rise" = "Lever" "RMS Wind" = "Valeur Efficace (RMS) Vent" + "Season" = "Saison" "select month" = "Choisir le mois" "select year" = "Choisir l'année" "Sensor Status" = "Statut capteur(s)" diff --git a/src/weewx_data/skins/Seasons/lang/gr.conf b/src/weewx_data/skins/Seasons/lang/gr.conf index 4f0dd155..3a9a691c 100644 --- a/src/weewx_data/skins/Seasons/lang/gr.conf +++ b/src/weewx_data/skins/Seasons/lang/gr.conf @@ -188,6 +188,7 @@ unit_system = metricwx "Right ascension" = "Δεξιά ανύψωση" "Rise" = "Aνατολή" "RMS Wind" = "RMS Ανέμου" + "Season" = "Εποχή" "select month" = "Επιλέξτε" "select year" = "Επιλέξτε" "Sensor Status" = "Κατάσταση Αισθητήρα" diff --git a/src/weewx_data/skins/Seasons/lang/it.conf b/src/weewx_data/skins/Seasons/lang/it.conf index 944d09c7..67d9b85b 100644 --- a/src/weewx_data/skins/Seasons/lang/it.conf +++ b/src/weewx_data/skins/Seasons/lang/it.conf @@ -197,6 +197,7 @@ unit_system = metricwx "Right ascension" = "Ascensione retta" "Rise" = "Sorge" "RMS Wind" = "RMS vento" + "Season" = "Stagione" "select month" = "Seleziona mese" "select year" = "Seleziona anno" "Sensor Status" = "Stato dei sensori" diff --git a/src/weewx_data/skins/Seasons/lang/nl.conf b/src/weewx_data/skins/Seasons/lang/nl.conf index 0222735a..6824ba9d 100644 --- a/src/weewx_data/skins/Seasons/lang/nl.conf +++ b/src/weewx_data/skins/Seasons/lang/nl.conf @@ -189,6 +189,7 @@ unit_system = metricwx "Right ascension" = "Rechte Klimming" "Rise" = "Opkomst" "RMS Wind" = "RMS Wind" + "Season" = "Seizoen" "select month" = "Selecteer Maand" "select year" = "Selecteer Jaar" "Sensor Status" = "Sensor Status" diff --git a/src/weewx_data/skins/Seasons/statistics.inc b/src/weewx_data/skins/Seasons/statistics.inc index cfc3f800..82fa23ee 100644 --- a/src/weewx_data/skins/Seasons/statistics.inc +++ b/src/weewx_data/skins/Seasons/statistics.inc @@ -4,7 +4,8 @@ #errorCatcher Echo #encoding UTF-8 -#set $time_tags = [$day, $week, $month, $year, $rainyear] +#set $season_type = $DisplayOptions.get('season_type','meteorological') +#set $time_tags = [$day, $week, $month, $season(season_type=$season_type), $year, $rainyear] ## Get the list of observations from the configuration file, otherwise fallback ## to a very rudimentary set of observations. @@ -29,6 +30,7 @@ $gettext("Today") $gettext("Week") $gettext("Month") + $gettext("Season") $gettext("Year") $gettext("Rain Year")