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") |