Inspired by some examples on the Tasker Wiki, I decided it’d be nice to make my new Android phone speak the weather to me in the morning. The current examples, while great demonstrations of the power of Tasker, didn’t seem quite right to me. The language produced by chopping up the XML from, say, AccuWeather didn’t seem all that natural to me, so I decided to offload that part of the processing to my web server. Bring on the PHP.
PHP
Code: github.com/darac/Weather-Sentence/WeatherSentence.php
The PHP class does four main jobs:
- Convert a latitude/longitude pair into a location name by using the Geonames.org findNearbyPlaceName function.
- Fetch the current and daily forecasts from http://forecast.io.
- Convert the current weather into a sentence
- Convert today’s forecast into a sentence
The sentences are constructed along the lines of “The weather for $location is $summary. $temperature, $wind and $precipitation.” For $location, I use the ‘name’ returned by Geonames (as that’s the preferred name) and $summary comes straight from Forecast.io.
For the second part, I construct a series of phrases and join those parts which exist (for example, if there’s no chance of rain, then there is no precipitation phrase), finally uppercasing the first letter to make a sentence.
The temperature phrase is easy. It’s either “it is $temperature degrees” for the current weather or “there will be a high of $temperatureMax and a low of $temperatureMin” for the forecast.
The wind is a little more complex. Rather than giving wind speeds in miles per hour, I convert them to Beaufort descriptions using the following function:
<?php // vim:ts=4:st=4:sw=4:ai class WeatherSentence { // properties public $loc = "51.0000,-1.0000"; protected $APIKEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; protected $geousername = 'demo'; private $weather; private $debug = false; // methods public function __construct($loc = null, $debug = false) { if (isset($loc) and $loc != null){ $this->loc = $loc; } $this->debug = $debug; } public function SetForecastAPIKey($key) { $this->APIKEY = $key; } public function SetGeonamesUser($user) { $this->geousername = $user; } public function LookupLocation() { // Convert a Lat/Long to a placename using Geonames if ($this->geousername == '' or $this->geousername == 'demo') { // Don't lookup without a (potentially) valid username return 'your location'; } list($lat, $lon) = explode(',', $this->loc); $json_string = file_get_contents("http://api.geonames.org/findNearbyPlaceNameJSON?lat=$lat&lng=$lon&username=$this->geousername"); $parsed_json = json_decode($json_string); if ($this->debug) echo "<pre><code>" . json_encode($parsed_json, JSON_PRETTY_PRINT) . "</code></pre><br />"; if (isset($parsed_json->{'geonames'}[0]->{'toponymName'})) { $location_name = $parsed_json->{'geonames'}[0]->{'toponymName'}; } else { $location_name = 'your location'; } return $location_name; } private function fetchWeather($force = false) { if (isset($this->weather) and !$force) { return $this->weather; } if ($this->APIKEY == '' or $this->APIKEY == 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx') { return null; } // Otherwise, fetch the current and daily weather (that is, we skip the minutely and hourly blocks, but get everything else) $json_string = file_get_contents("http://api.forecast.io/forecast/$this->APIKEY/$this->loc?units=uk&exclude=minutely,hourly"); $this->weather = json_decode($json_string); if ($this->debug) echo "<pre><code>" . json_encode($this->weather, JSON_PRETTY_PRINT) . "</code></pre><br />"; return $this->weather; } public function MakeCurrentSentence($weather = null) { $currentphrase = ''; $tempstring = ''; $windstring = ''; $precipstring = ''; if (!isset($weather) or $weather == null) { $weather = $this->fetchWeather(); } $currentphrase = 'The Weather for ' . $this->LookupLocation() . ' is ' . $weather->{'currently'}->{'summary'} . '. '; // Parts if (isset($weather->{'currently'}->{'temperature'})) { $tempstring = 'it is ' . number_format($weather->{'currently'}->{'temperature'}) . ' degrees'; } if (isset($weather->{'currently'}->{'windBearing'})) { $windspeed = $weather->{'currently'}->{'windSpeed'}; $windbearing = $weather->{'currently'}->{'windBearing'}; if ($windspeed <= 3) { // do nothing continue; } else { $windstring = 'there is a ' . $this->MPHtoBeaufort($windspeed); $windstring .= ' from the ' . $this->BearingToCardinal($windbearing); } } if (isset($weather->{'currently'}->{'precipType'})) { $precipstring = 'there is a ' . $this->ProbabilityToChance($weather->{'currently'}->{'precipProbability'}); $precipstring .= ' of ' . $this->PrecipIntensity($weather->{'currently'}->{'precipIntensity'}) . $weather->{'currently'}->{'precipType'}; } if ($tempstring != "" and $windstring != "" and $precipstring != "") { $currentphrase .= ucfirst($tempstring) . ", $windstring and $precipstring."; } elseif ($windstring != "" and $precipstring != "") { $currentphrase .= ucfirst($windstring) . " and $precipstring."; } elseif ($tempstring != "" and $precipstring != "") { $currentphrase .= ucfirst($tempstring) . " and $precipstring."; } elseif ($tempstring != "" and $windstring != "") { $currentphrase .= ucfirst($tempstring) . " and $windstring."; } else { // Only one string, just joining them will work $currentphrase .= ucfirst($tempstring . $windstring . $precipstring); } return $currentphrase; } public function MakeForecastSentence($weather = null) { $forecastphrase = ''; $tempstring = ''; $windstring = ''; $precipstring = ''; if (!isset($weather) or $weather == null) { $weather = $this->fetchWeather(); } // Forecast $forecast = $weather->{'daily'}->{'data'}[0]; $forecastphase = "Today's forecast is " . $forecast->{'summary'}; // Parts $tempstring = ""; $windstring = ""; $precipstring = ""; if (isset($forecast->{'temperatureMin'})) { $tempstring = 'there will be a high of ' . number_format($forecast->{'temperatureMax'}) . ' and a low of ' . number_format($forecast->{'temperatureMin'}); } if (isset($forecast->{'windBearing'})) { if ($forecast->{'windSpeed'} <= 3) { // do nothing ; } else { $windstring = 'there will be a ' . $this->MPHtoBeaufort($forecast->{'windSpeed'}); $windstring .= ' from the ' . $this->BearingToCardinal($forecast->{'windBearing'}); } } if (isset($forecast->{'precipType'})) { $precipstring = 'there is a ' . $this->ProbabilityToChance($forecast->{'precipProbability'}); $precipstring .= ' of ' . $this->PrecipIntensity($forecast->{'precipIntensity'}) . $forecast->{'precipType'}; } if ($tempstring != "" and $windstring != "" and $precipstring != "") { $forecastphrase .= ucfirst($tempstring) . ", $windstring and $precipstring."; } elseif ($windstring != "" and $precipstring != "") { $forecastphrase .= ucfirst($windstring) . " and $precipstring."; } elseif ($tempstring != "" and $precipstring != "") { $forecastphrase .= ucfirst($tempstring) . " and $precipstring."; } elseif ($tempstring != "" and $windstring != "") { $forecastphrase .= ucfirst($tempstring) . " and $windstring."; } else { // Only one string, just joining them will work $forecastphrase .= ucfirst($tempstring . $windstring . $precipstring); } return $forecastphrase; } public function MakeAlertSentence($weather = null) { $alertphrase = ''; if (!isset($weather) or $weather == null) { $weather = $this->fetchWeather(); } if (isset($weather->{'alert'})) { foreach ($weather->{'alert'} as $alert) { $alertphrase .= ' Alert: ' . $alert->{'description'}; } } } private function BearingToCardinal($bearing) { if ($bearing > 348.75 or $bearing <= 11.25) { return 'North'; } elseif ($bearing <= 33.75) { return 'North North East'; } elseif ($bearing <= 56.25) { return 'North East'; } elseif ($bearing <= 78.75) { return 'East North East'; } elseif ($bearing <= 101.25) { return 'East'; } elseif ($bearing <= 123.75) { return 'East South East'; } elseif ($bearing <= 146.25) { return 'South East'; } elseif ($bearing <= 168.75) { return 'South South East'; } elseif ($bearing <= 191.25) { return 'South'; } elseif ($bearing <= 213.75) { return 'South South West'; } elseif ($bearing <= 236.25) { return 'South West'; } elseif ($bearing <= 258.75) { return 'West South West'; } elseif ($bearing <= 281.25) { return 'West'; } elseif ($bearing <= 303.75) { return 'West North West'; } elseif ($bearing <= 326.25) { return 'North West'; } elseif ($bearing <= 348.75) { return 'North North West'; } } private function MPHtoBeaufort($mph) { // Beaufort definitions if ($mph <= 7) { return 'light breeze'; } elseif ($mph <= 12) { return 'gentle breeze'; } elseif ($mph <= 17) { return 'moderate breeze'; } elseif ($mph <= 24) { return 'fresh breeze'; } elseif ($mph <= 30) { return 'strong breeze'; } elseif ($mph <= 38) { return 'moderate gale'; } elseif ($mph <= 46) { return 'gale'; } elseif ($mph <= 54) { return 'strong gale'; } elseif ($mph <= 63) { return 'storm'; } elseif ($mph <= 73) { return 'violent storm'; } else { return 'hurricane'; } } private function ProbabilityToChance($prob) { if ($prob <= 0.2) { return 'remote chance'; } elseif ($prob <= 0.4) { return 'slight chance'; } elseif ($prob <= 0.6) { return 'strong chance'; } elseif ($prob <= 0.8) { return 'possibility'; } else { return 'certainty'; } } private function PrecipIntensity($amount) { // mm/hr if ($amount < 0.05) { return ''; } elseif ($amount < 0.432) { return 'very light '; } elseif ($amount < 2.54) { return 'light '; } elseif ($amount < 10.2) { return 'moderate '; } else { return 'heavy '; } } } ?>
Similarly, I convert the bearing into a cardinal direction. Thus the phrase ends up along the lines of “there is a strong breeze from the south south west”.
And again, for the precipitation, in order to make the phrase “there is a $chance of $intensity $type”, I use lookup functions to convert the probability (0 to 1) and the intensity (in millimetres per hour) into words.
When it’s all done, I concatenate the current and forecast sentences together, and tack on any alerts that forecast.io has also sent and echo the whole lot.
Tasker
Code: github.com/darac/Weather-Sentence/Weather Speaker.xml
Based on an entry in the wiki, I have a tasker profile with an entry and an exit task. The profile is for a time range, so the entry task happens at the start of the time period (6:55, in my case) and the exit task happens at the end of the time period (7:01, just after my alarm has gone off).
The entry task wakes up the phone and fetches the information. First of all it turns on the GPS (as my phone runs KitKat, I’ve had to restort to using the Secure Settings plugin for that), waits for up to 90 seconds to acquire a location and then turns off the GPS. From what I’ve read, if the GPS didn’t get a fix in that time, then Tasker should still give a valid location based on the cell towers (albeit with less accuracy).
Next up, the entry task calls the PHP script on the web server (a simple HTTP get). And this is where it finishes. The original example did lots of cutting and splicing of the HTTP output, but we know that we have a single line of text that’s ready for speaking.
When the time period is done, the exit task makes a note of whether the phone is in silent mode and what its volume is (these are restored at the end of the task). It then turns the volume up to a suitable level and sends “Good Morning, Paul. It is $time on $date. $weather” to the Text-To-Speech engine.
Job done.
Implementing this Yourself
As you can see, I’ve put this code up on github. There are a few files to look at. First, put WeatherSentence.php and weather-example.php on your web server and modify weather-example.php to use your forecast.io API key and Geonames username. Configure your web server to serve this as PHP (you may want to rename it as weather.php, too).
Next, copy Weather Speaker.xml to your phone. In Tasker, long-press the “Profiles” tab-header and import the xml. You will want to change the URL that it fetches to point to your own server, you may want to change the greeting (currently, it’s “Good Morning, Master”) and, if your Tasker allows you to enable/disable GPS, then disable the Secure Settings actions and enable the native ones instead.
Enjoy.
Todo
Like any good job, there are tweaks I would like to do in the future. One of them is to be able to set a variable that bypasses the whole profile. This is so that, if I go to bed with someone else or it’s a day off or something, I can tap a button before bed (which sets the flag) and won’t get annoyed in the morning.
There are also other items I could look at adding to the sentence, such as a news headline or a traffic report.