in Checking In, MT.Net, Science, Weather, X-Geek

Spoken stats from my weather station

Last Christmas (2016), I got an AcuRite weather station from Costco as a gift to replace my falling-apart Oregon Scientific station. It’s a decent little setup, with wireless transmission from a multi-sensor box outside to the panel inside. For the longest time my biggest complaint was its need to use Windows software to archive its data.

Acurite weather station

Then early last year I hooked up the open source weather software weewx to my station. Weewx creates a nice (if simple) graph of weather data (as seen at https://www.markturner.net/wx) and also kicks the data over to my MySQL database so I can save and query those stats. Last month I was able to create a fancy Grafana dashboard that dynamically displays that data in a beautiful format. Now I had taken a $75 weather station and made it much more useful!

Grafana weather graph

But I wasn’t ready to stop there. I got an Amazon Echo Dot as a Christmas gift and decided I wanted to learn how to teach it tricks, including reading me weather from my weather station, not someone else’s. I found a YouTube video of someone using the Echo to call phone numbers. This a neat trick but what really caught my attention was the guy’s phone system reading out his weather data.

I’ve used Asterisk as a home phone server for well over a decade now and have long thought it would be neat to add some text-to-speech (TTS) capability to it. The open source TTS tools are good but not great. The commercial ones cost more money than I want to spend on an experimental setup (though all in all not too expensive … if I weren’t a cheapskate!). That left some middle ground to be explored.

Google has excellent TTS services as everyone knows from using it on their smartphones. Luckily for me, someone built a perl-based tool to send text to Google and fetch the corresponding speech in a wav file. This tool has been rolled into a Github project called asterisk-googletts which is an Asterisk AGI application that gives your Asterisk server the ability to speak. After adding a few dependencies and putting the sample text into my dialplan, I was delighted to dial an extension and hear my Asterisk server talking to me smoothly and legibly.

Once I had that figured out my attention turned to my weather data. Weewx is very extensible and uses the concept of reports to distribute its data. The default one creates a very readable page, the MySQL one dishes the data to my database, and there’s even a forecast one that fetches info from the National Weather Service. All of these are fancy but what I needed was a simple report that put the data into a narrative format that I could feed into Google TTS. After several searches, I was unable to find one I needed. So I built my own.

My narrative Weewx report is built from weewx’s “Standard” report, stripping out the HTML markup. It uses an “if” statement to provide info on whether the barometer’s rising, steady, or falling. I also adjusted the labels in the report to change the abbreviations to their full phrases so that the speech translation said everything properly. The result is pretty good, I think!

The next obstacle was how to get this into my phone server, which lives in a different host. Weewx has an rsync report for sending data elsewhere but I am already using it to push data to my webserver. Rsync seemed overkill for this use, too. I opted for a cron job that pulls down the three-line text file from my weewx webserver and makes this available to Asterisk.

Then I decided that I didn’t want this script running every x minutes when I would likely only be calling it occasionally. Wouldn’t it be better if I had Asterisk fetch the narrative text on-demand? It turns out Asterisk has a CURL function that can be used to directly pull web data, rather than have to make a system call to get it. This was the perfect answer but after a few tries I realized I had not compiled Asterisk with libcurl support. D’oh! I then spent the next half-hour pulling down the latest Asterisk code, installing the libcurl library, and compiling it. I also added some new codecs while I was at it and improved some other stuff along the way.

With libcurl added my system was complete! I could now dial an extension and hear my Asterisk server read my weather stats. Success!

My setup works for me but there’s some cleaning up I need to do to make it public. I hope to put it in a wee_extension format so that it can be easily installed by others. And now that my weather station is properly kicking out narrative text it should be fairly straightforward to get Alexa reading it, especially if I can adapt one of the sample apps out there.

Here’s my current skin.conf file:

###############################################################################
# STANDARD SKIN CONFIGURATION FILE                                            #
# Copyright (c) 2010 Tom Keffer                            #
# Modified in 2018 by Mark Turner 
###############################################################################

[Extras]
    # Put any extra tags here that you want to be available in the templates
    
###############################################################################

[Units]
    # This section is for managing the selection and formatting of units.
    
    [[Groups]]
        # For each group of measurements, this section sets what units to
        # use for it.
        # NB: The unit is always in the singular. I.e., 'mile_per_hour',
        # NOT 'miles_per_hour'

        group_altitude     = foot                 # Options are 'foot' or 'meter'
        group_degree_day   = degree_F_day         # Options are 'degree_F_day' or 'degree_C_day'
        group_direction    = degree_compass
        group_moisture     = centibar
        group_percent      = percent
group_pressure     = mbar                 # Options are 'inHg', 'mmHg', 'mbar', or 'hPa'
group_radiation    = watt_per_meter_squared
group_rain         = inch                 # Options are 'inch', 'cm', or 'mm'
group_rainrate     = inch_per_hour        # Options are 'inch_per_hour', 'cm_per_hour', or 'mm_per_hour'
group_speed        = mile_per_hour        # Options are 'mile_per_hour', 'km_per_hour', 'knot', or 'meter_per_second'
group_speed2       = mile_per_hour2       # Options are 'mile_per_hour2', 'km_per_hour2', 'knot2', or 'meter_per_second2'
group_temperature  = degree_F             # Options are 'degree_F' or 'degree_C'
group_uv           = uv_index
group_volt         = volt

# The following are used internally and should not be changed:
group_count        = count
group_interval     = minute
group_time         = unix_epoch
group_elapsed      = second

[[StringFormats]]
# This section sets the string formatting for each type of unit.

centibar           = %.0f
cm                 = %.2f
cm_per_hour        = %.2f
degree_C           = %.0f
degree_F           = %.0f
degree_compass     = %.0f
foot               = %.0f
hPa                = %.1f
hour               = %.1f
inHg               = %.2f
inch               = %.2f
inch_per_hour      = %.2f
km_per_hour        = %.0f
km_per_hour2       = %.1f
knot               = %.0f
knot2              = %.1f
mbar               = %.1f
meter              = %.0f
meter_per_second   = %.1f
meter_per_second2  = %.1f
mile_per_hour      = %.0f
mile_per_hour2     = %.1f
mm                 = %.1f
mmHg               = %.1f
mm_per_hour        = %.1f
percent            = %.0f
second             = %.0f
uv_index           = %.1f
volt               = %.1f
watt_per_meter_squared = %.0f
NONE               = "   N/A"

[[Labels]]
# This section sets a label to be used for each type of unit.

centibar          = " cb"
cm                = " cm"
cm_per_hour       = " cm/hr"
degree_C          =   " degrees"
degree_F          =   " degrees"
degree_compass    =   °
foot              = " feet"
hPa               = " hPa"
inHg              = " inches"
inch              = " inches"
inch_per_hour     = " in/hr"
km_per_hour       = " km/h"
km_per_hour2      = " km/h"
knot              = " knots"
knot2             = " knots"
mbar              = " milli-bars"
meter             = " meters"
meter_per_second  = " m/s"
meter_per_second2 = " m/s"
mile_per_hour     = " miles per hour"
mile_per_hour2    = " miles per hour"
mm                = " mm"
mmHg              = " mmHg"
mm_per_hour       = " mm/hr"
percent           = " percent"
volt              = " V"
watt_per_meter_squared = " W/m²"
day               = " day",    " days"
hour              = " hour",   " hours"
minute            = " minute", " minutes"
second            = " second", " seconds"
NONE              = ""

[[TimeFormats]]
# This section sets the string format to be used for each time scale.
# The values below will work in every locale, but may not look
# particularly attractive. See the Customization Guide for alternatives.

day        = %X
week       = %X (%A)
month      = %x %X
year       = %x %X
rainyear   = %x %X
current    = %x %X
ephem_day  = %X
ephem_year = %x %X

[[Ordinates]]    
# The ordinal directions. The last one should be for no wind direction
directions = north, north-northeast, northeast, east-northeast, east, east-southeast, southeast, south-southeast, south, south-Southwest, southwest, west-southwest, west, west-northwest, northwest, north-northwest, N/A

[[DegreeDays]]
# This section sets the base temperatures used for the calculation
# of heating and cooling degree-days.
        
# Base temperature for heating days, with unit:
heating_base = 65, degree_F
# Base temperature for cooling days, with unit:
cooling_base = 65, degree_F

[[Trend]]
time_delta = 10800  # 3 hours
time_grace = 300    # 5 minutes 

###############################################################################

[Labels]
# Labels used in this skin

# Set to hemisphere abbreviations suitable for your location: 
hemispheres = N, S, E, W
# Formats to be used for latitude whole degrees, longitude whole degrees,
# and minutes:
latlon_formats = "%02d", "%03d", "%05.2f"

[[Generic]]
# Generic labels, keyed by an observation type.

barometer      = Barometer
        dewpoint       = Dew Point
        heatindex      = Heat Index
        inHumidity     = Inside Humidity
        inTemp         = Inside Temperature
        outHumidity    = Outside Humidity
        outTemp        = Outside Temperature
        radiation      = Radiation
        rain           = Rain
        rainRate       = Rain Rate
        rxCheckPercent = ISS Signal Quality
        UV             = UV Index
        windDir        = Wind Direction
        windGust       = Gust Speed
        windGustDir    = Gust Direction
        windSpeed      = Wind Speed
        windchill      = Wind Chill
        windgustvec    = Gust Vector
        windvec        = Wind Vector
    
###############################################################################

[Almanac]
    # The labels to be used for the phases of the moon:
    moon_phases = New, Waxing crescent, First quarter, Waxing gibbous, Full, Waning gibbous, Last quarter, Waning crescent

###############################################################################

[CheetahGenerator]
    # This section is used by the generator CheetahGenerator, and specifies
    # which files are to be generated from which template.

    # Possible encodings are 'html_entities', 'utf8', or 'strict_ascii'
    encoding = utf8
    [[PlainText]]
            encoding = utf8
            template = narrative.tmpl
            
###############################################################################


#
# The list of generators that are to be run:
#
[Generators]
        generator_list = weewx.cheetahgenerator.CheetahGenerator

And here’s the narrative.tmpl file that goes with it:

#errorCatcher Echo
##
## Specifying an encoding of UTF-8 is usually safe, but if your text is 
## actually in Latin-1, then you should replace the string "UTF-8" with "latin-1"
## If you do this, you should also change the 'Content-Type' metadata below.
#encoding UTF-8
##
#if $trend.barometer.raw > 6
#set $bartrend="rising very quickly"
#elif $trend.barometer.raw > 3.5
#set $bartrend="rising quickly"
#elif $trend.barometer.raw > 1.5
#set $bartrend="rising"
#elif $trend.barometer.raw > 0.1
#set $bartrend="rising slowly"
#elif $trend.barometer.raw > -0.1
#set $bartrend="steady"
#elif $trend.barometer.raw > -1.5
#set $bartrend="falling slowly"
#elif $trend.barometer.raw > -3.5
#set $bartrend="falling"
#elif $trend.barometer.raw > -6
#set $bartrend="falling quickly"
#else
#set $bartrend="falling very quickly"
#end if
At of $current.dateTime.format("%_I:%M %p") in $station.location, the temperature is $current.outTemp. The humidity is $current.outHumidity. The dewpoint is $current.dewpoint. Winds are from the $current.windDir.ordinal_compass at $current.windSpeed. The barometer is $current.barometer and $bartrend. Today's rainfall is $day.rain.sum.

Here’s the snippet from Asterisk’s extension.conf that calls googletts.agi:

exten => 8300,1,Answer()
exten => 8300,n,Set(wx=${CURL(http://weatherstation/weewx/narrative/narrative)})
exten => 8300,n,agi(googletts.agi,"${wx}. Goodbye.",en)
exten => 8300,n,HangUp

One thing I soon found out is that googletts.agi expects the text to be one line. If your weewx report contains multiple lines it will abort without saying anything. Keep that in mind as you’re crafting your template.

If you want to check the output of my weewx report, you can pull down the text-file report here. Note that Apache is serving this up without any MIME types so your browser will probably balk at displaying it, though wget or curl won’t have any problem with it.

Here’s a sample wav file from googletts so you can hear how it sounds:
Enjoy!