Speak the weather

Here at Ice Moon Prison, going outside with insufficient clothing can be fatal. Hence we find ourselves checking the Bureau of Meteorology’s forecast often, even before we get up in the morning. Fumbling for a smartphone while still in bed is out of the question—LCD screens are off-limits in sleeping quarters—so we are experimenting with audio announcements.

The BOM publishes all of its forecasts as easy-to-process XML documents, which it pushes to its anonymous FTP server twice a day.

# Crontab entry to fetch the BOM XML for Melbourne metropolitan region.
BOM_PRODUCT=IDV10450
37 1,5,7,9,12,15,17,21 * * * /usr/bin/curl -s -S ftp://ftp2.bom.gov.au/anon/gen/fwo/$BOM_PRODUCT.xml > /var/speak-weather/$BOM_PRODUCT.xml.new && mv /var/speak-weather/$BOM_PRODUCT.xml.new /var/speak-weather/$BOM_PRODUCT.xml

Transforming one of these to a string of English text is a simple matter of XSLT.  Ice Moon Prison uses and recommends Saxon Home Edition.

<?xml version="1.0" encoding="UTF-8"?>
<!-- speak-weather.xsl -->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema">

    <!-- Location to get temperature forecast for. -->
    <xsl:param name="aacTemperature"/>

    <!-- Location to get precipitation forecast for. -->
    <xsl:param name="aacRain"/>

    <!-- Time to get current conditions for. -->
    <xsl:param name="conditionTime" select="current-dateTime()"/>

    <!-- Time to get forecast for. -->
    <xsl:param name="forecastTime" select="current-dateTime() + xs:dayTimeDuration('PT18H')"/>

    <xsl:output method="text"/>

    <!-- "Tonight" forecast (from 5 pm to midnight). -->
    <xsl:template name="tonight">
        <!-- Current conditions forecast -->
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="tonight">
            <xsl:with-param name="time" select="$conditionTime"/>
        </xsl:apply-templates>
        <xsl:text> </xsl:text>
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="tomorrow-minimum">
            <xsl:with-param name="time" select="$forecastTime"/>
        </xsl:apply-templates>
        <xsl:text> </xsl:text>
        <!-- Tomorrow's conditions forecast. -->
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="tomorrow">
            <xsl:with-param name="time" select="$forecastTime"/>
        </xsl:apply-templates>
        <xsl:text> </xsl:text>
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="tomorrow-maximum">
            <xsl:with-param name="time" select="$forecastTime"/>
        </xsl:apply-templates>
    </xsl:template>

    <!-- "Today" forecast (from 5 am to 5 pm). -->
    <xsl:template name="today">
        <!-- Today's conditions forecast. -->
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="today">
            <xsl:with-param name="time" select="$conditionTime"/>
        </xsl:apply-templates>
        <xsl:text> </xsl:text>
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacTemperature]" mode="today-maximum">
            <xsl:with-param name="time" select="$conditionTime"/>
        </xsl:apply-templates>       
        <xsl:text> </xsl:text>
        <xsl:apply-templates select="/product/forecast/area[@aac=$aacRain]" mode="today-rain">
            <xsl:with-param name="time" select="$conditionTime"/>
        </xsl:apply-templates>
    </xsl:template>

    <!-- Recurse throuch all parent AACs to get local, district, state levels. -->
    <xsl:template match="area" mode="#all">
        <xsl:param name="time"/>
        <xsl:apply-templates select="forecast-period[xs:dateTime(@start-time-utc) le $time][xs:dateTime(@end-time-utc) gt $time]" mode="#current">
            <xsl:sort select="@index" data-type="number"/>
        </xsl:apply-templates>
        <xsl:apply-templates select="/product/forecast/area[@aac=current()/@parent-aac]" mode="#current">
            <xsl:with-param name="time" select="$time"/>
        </xsl:apply-templates>
    </xsl:template>

    <!--
        Select parts of each forecast-period for converting into text.
    -->

    <xsl:template match="forecast-period" mode="tonight">
        <xsl:apply-templates select="text[@type='forecast']|text[@type='precis']" mode="tonight"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="tomorrow-minimum">
        <xsl:apply-templates select="element[@type='air_temperature_minimum']" mode="tomorrow-minimum"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="tomorrow">
        <xsl:apply-templates select="text[@type='forecast']|text[@type='precis']" mode="tomorrow"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="tomorrow-maximum">
        <xsl:apply-templates select="element[@type='air_temperature_maximum']" mode="tomorrow-maximum"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="today">
        <xsl:apply-templates select="text[@type='forecast']|text[@type='precis']" mode="today"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="today-maximum">
        <xsl:apply-templates select="element[@type='air_temperature_maximum']" mode="today-maximum"/>
    </xsl:template>

    <xsl:template match="forecast-period" mode="today-rain">
        <xsl:if test="text[@type='probability_of_precipitation'] ne '0%' and element[@type='precipitation_range']">
            <xsl:text>Chance of rain: </xsl:text>
            <xsl:apply-templates select="text[@type='probability_of_precipitation']" mode="today-rain"/>
            <xsl:text> of </xsl:text>
            <xsl:apply-templates select="element[@type='precipitation_range']" mode="today-rain"/>
            <xsl:text>.</xsl:text>
        </xsl:if>
    </xsl:template>

    <!--
        Turn elements into text.
    -->

    <xsl:template match="forecast-period/text[@type='forecast']|forecast-period/text[@type='precis']" mode="tonight">
        <xsl:text>For the rest of today: </xsl:text>
        <xsl:apply-templates select="."/>
    </xsl:template>

    <xsl:template match="forecast-period/text[@type='forecast']|forecast-period/text[@type='precis']" mode="tomorrow">
        <xsl:text>Tomorrow: </xsl:text>
        <xsl:apply-templates select="."/>
    </xsl:template>

    <xsl:template match="forecast-period/text[@type='forecast']|forecast-period/text[@type='precis']" mode="today">
        <xsl:text>Today: </xsl:text>
        <xsl:apply-templates select="."/>
    </xsl:template>

    <xsl:template match="forecast-period/element[@type='air_temperature_minimum']" mode="tomorrow-minimum">
        <xsl:text>Tonight, down to </xsl:text>
        <xsl:value-of select="text()"/>
        <xsl:text>.</xsl:text>
    </xsl:template>

    <xsl:template match="forecast-period/element[@type='air_temperature_maximum']" mode="tomorrow-maximum today-maximum">
        <xsl:text>Maximum: </xsl:text>
        <xsl:value-of select="text()"/>
        <xsl:text>.</xsl:text>
    </xsl:template>

    <xsl:template match="forecast-period/text[@type='probability_of_precipitation']" mode="today-rain">
           <xsl:value-of select="text()"/>
    </xsl:template>

    <xsl:template match="forecast-period/element[@type='precipitation_range']" mode="today-rain">
        <xsl:choose>
            <xsl:when test="text() = '0 mm'">
                <xsl:text>a trace</xsl:text>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="text()"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:template>

    <!-- Not interesting. Omit. -->
    <xsl:template match="text[@type='warning-summary-footer']"/>
    <xsl:template match="text[@type='product-footer']"/>

</xsl:stylesheet>
# Crontab entry to transform stylesheet into English text.
AAC_TEMPERATURE=VIC_PT065
AAC_RAIN=VIC_PT042
38 0-4,17-23 * * * /usr/bin/java -cp /usr/local/lib/saxon9he.jar net.sf.saxon.Transform -it:tonight -xsl:$HOME/lib/speak-weather.xsl -s:/var/speak-weather/$BOM_PRODUCT.xml aacTemperature=$AAC_TEMPERATURE aacRain=$AAC_RAIN > /var/speak-weather/scoresby.txt.new && mv /var/speak-weather/scoresby.txt.new /var/speak-weather/scoresby.txt
38 5-16 * * * /usr/bin/java -cp /usr/local/lib/saxon9he.jar net.sf.saxon.Transform -it:today -xsl:$HOME/lib/speak-weather.xsl -s:/var/speak-weather/$BOM_PRODUCT.xml aacTemperature=$AAC_TEMPERATURE aacRain=$AAC_RAIN > /var/speak-weather/scoresby.txt.new && mv /var/speak-weather/scoresby.txt.new /var/speak-weather/scoresby.tx

Text-to-speech has improved by leaps and bounds in recent years, to the point where Ice Moon Prison has freed its now-redundant radio announcers (except the ones who say “nucular”). A Mac Mini running OS X Mountain Lion can “say” the forecast in a number of realistic accents and produce an audio file that can be stored on a web server.

# Crontab entry to produce an AAC file of the forecast.
39 * * * * (echo "[[slnc 50]]" && sed 's/\([Ww]\)inds/\1innds/g;s/\([Ss]\)eabreeze/\1ea breeze/g' /var/speak-weather/scoresby.txt ) | /usr/bin/say -v Lee -o /var/speak-weather/scoresby.m4a -f - && cp /var/speak-weather/scoresby.m4a /Library/Server/Web/Data/Sites/Default/speak-weather/scoresby.m4a

(Some words are mispronounced by Lee, so a little search-and-replace is in order to prevent “winds” rhyming with “kinds”.  Inserting a short silence at the beginning ensures that the first word is not clipped on playback.)

Here is a forecast from January 1 2014 as an example:

Announcing the forecast in the sleeping quarters is trivially easy with a Sonos Internet-connected speaker and a Vera home automation gateway. A scene is activated by a button press on a remote control, instructing the Sonos to play the most recent audio file.

Vera scene editor

Advanced tab of Vera Scene editor, showing HTTP URL of forecast AAC (m4a) file.

Leave a Reply

Your email address will not be published. Required fields are marked *