Perl
Asterisk
Development of communication systems
9 March 2012

Вариация на тему: прогноз погоды по телефону

Решил поделиться ещё одним способом организации сервиса прогноза погоды по телефону. Здесь, по сравнению с этим постом, больше интеллекта перенесено в Asterisk.
Weather


XML с погодой


XML с текущей погодой и прогнозом на два следующих дня беру с BBC.

Так как от Cron'а нам не избавиться, то добавляем:
0 2 * * * /home/alexandr/xml/weather/almaty/bbcweather.sh
0 14 * * * /home/alexandr/xml/weather/almaty/bbcweather.sh
30 16 * * * /home/alexandr/xml/weather/almaty/bbcweather.sh

Опытным путем было установлено, что XML обновляется три раза в сутки (по крайней мере для города Алматы).

Скрипт не только скачивает, но также обрабатывает XML-ку и раскладывает необходимые значения по файликам. Правда мой скрипт очень примитивный и скорее всего многих введёт в уныние своей неэффективностью, поэтому не буду заострять на нём внимание.

Текстовые файлы


Имена файлов для одного и того же параметра (например, минимальная температура) для разных дней будут отличаться цифрой-префиксом: 1 – сегодня, 2 и 3 – завтра и послезавтра соответственно. Например, минимальная температура для сегодняшнего дня (первый блок данных в XML) будет храниться в файле с именем «1Tmin», для завтрашнего (второй блок данных) – «2Tmin».

Теперь по содержимому файлов (которые, кстати, не должны содержать символа конца строки):
  • День недели (Wday) содержит «day-0» для воскресенья,… «day-6» для субботы;
  • Направление ветра (Wdir) содержит «N» для «северный»,… «NW» для «северо-западный»;
  • Числовые значения (влажность, температура, скорость ветра) записываются числом;
  • Текстовые – текстом :-) (облачность, видимость).

Для тестирования (а также для написания скрипта) пользовался параметром "-n" команды «echo» для того, чтобы не записывать в файл символ конца строки:
echo -n "day-5" > 1Wday

Итак, имеем список файликов с погодными данными. Теперь переходим к конфигурации extensions.conf.

extensions.conf


[weather]
exten => 071,1,Set(wpath="/home/your-directory/")
exten => 071,n,Goto(say-weather,s,1)

[say-weather]
exten => s,1,Answer()
exten => s,n,Set(E=1)
exten => s,n(play),Playback(digits/${SHELL(cat ${wpath}${E}Wday)})
exten => s,n,Playback(custom/temperatura)
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Tmin)})
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Tmax)})
exten => s,n,Playback(custom/wind)
exten => s,n,Playback(custom/wind/${SHELL(cat ${wpath}${E}Wdir)})
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Wspd)})
exten => s,n,Playback(custom/metrov-v-sekundu)
exten => s,n,Playback(custom/vlazhnost)
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Humd)})
exten => s,n,Playback(custom/procentov)

exten => s,n,WaitExten()
exten => s,n,Hangup()
exten => _[1-3],1,Set(E=${EXTEN})
exten => _[1-3],n,Goto(say-weather,s,play)


Разбор полётов

Публикуем extension, по которому будем дозваниваться до погоды и, заодно, выставляем переменную «wpath», где указываем каталог ранее созданных файликов:
exten => 071,1,Set(wpath="/home/your-directory/")

Переходим в контекст say-weather:
exten => 071,n,Goto(say-weather,s,1)

После инициализации значения «E» (по умолчанию выставляем сегодняшний день), воспроизводим название дня недели:
exten => s,n,Set(E=1)
exten => s,n(play),Playback(digits/${SHELL(cat ${wpath}${E}Wday)})

Команду Playback можно и не представлять, а вот дальше на всякий случай расскажу. Если указывается относительный путь, то по умолчанию (во всяком случаем на моём debian) подставляется "/usr/share/asterisk/sounds/en/" (или «ru/», если в sip.conf указан параметр «language=ru»). Значения «wpath» и «E» мы уже определили, поэтому получается следующее:
Playback(/usr/share/asterisk/sounds/ru/digits/${SHELL(cat /home/your-directory/1Wday)})

Результатом выполнения «cat /home/your-directory/1Wday» пусть будет, например, строка «day-0» (допустим, что сегодня – воскресенье), тогда:
Playback(/usr/share/asterisk/sounds/ru/digits/day-0)

Т.е. воспроизводится файл, содержащий название нужного нам дня недели, т.к. файл digits/day-0 как раз и содержит звуковую запись «Воскресенье».

По такому же принципу отрабатывает и эта строчка:
SayNumber(${SHELL(cat ${wpath}${E}Tmin)})

Проговаривается минимальная температура сегодняшнего дня, значение которой берётся из файла "/home/your-directory/1Tmin".

Увы, пришлось записывать часть сообщений самостоятельно, их я поместил в каталог «custom».
exten => s,n,Playback(custom/temperatura)

После окончания всех Playback и SayNumber, ждем ввода дополнительных цифр:
exten => s,n,WaitExten()

Дополнительные цифры включают в себя 1, 2 и 3, что видно по паттерну _[1-3]:
exten => _[1-3],1,Set(E=${EXTEN})
exten => _[1-3],n,Goto(say-weather,s,play)

При нажатии на любую из них, первая строчка присваивает введённое значение переменной «E», а вторая строчка осуществляет переход к метке «play» (третья строка этого контекста). Таким образом Asterisk снова сообщит звонящему погоду, но уже на завтра (если была введена цифра 2) или послезавтра (3).

Ничто не мешает нам написать ещё один extension рядом с 071 и воспользоваться тем же самым контекстом say-weather для воспроизведения погоды для другого города:
[weather]
exten => 071,1,Set(wpath="/home/user/weather/city1/files")
exten => 071,n,Goto(say-weather,s,1)
exten => 072,1,Set(wpath="/home/user/weather/city2/files")
exten => 072,n,Goto(say-weather,s,1)


Откуда качал архив с русскими сообщениями уже не вспомню, но он должен легко нагуглиться.
Подробные описания функций Asterisk на www.voip-info.org.

Бонус


В качестве бонуса выкладываю часть extensions.conf, с помощью которой я записывал свои звуковые сообщения:
[test-context]
exten => 051,1,Goto(rec-file,s,1)
[rec-file]
exten => s,1,Verbose(1,Recording application)
exten => s,n,Answer()
exten => s,n,Playback(record-enter-num)
exten => s,n(filename),Read(filename)
exten => s,n(rec-mes),Record(/tmp/recs/${filename}:gsm)
exten => s,n,Playback(beep)
exten => s,n,Wait(1)
exten => s,n(rec-play),Playback(/tmp/recs/${filename})
exten => s,n(rec-review),Background(vm-review)
exten => s,n,WaitExten(10)
exten => s,n(rec-del),System(rm /tmp/recs/${filename}.gsm)
exten => s,n,Playback(vm-deleted)
exten => s,n(read-hang),Read(rep,,1,,,2);Wait for 1 digit for 2 seconds to receive 1 which means that another file is to be recorded
exten => s,n,Gotoif($["${rep}" = "1"]?filename)
exten => s,n,Hangup()

exten => 1,1,Playback(vm-msgsaved)
exten => 1,n,SayDigits(${filename})
exten => 1,n,Goto(rec-file,s,read-hang)

exten => 2,1,Goto(rec-file,s,rec-play)

exten => 3,1,Wait(1)
exten => 3,n,Goto(rec-file,s,rec-mes)

exten => i,1,Goto(rec-file,s,rec-review)
exten => t,1,Goto(rec-file,s,rec-del)
exten => 0,1,Goto(rec-file,s,rec-del)

Готовим табличку со списком имен файлов (в виде цифр) и текстом, который нужно будет проговорить, звоним с более-менее хорошего телефона/гарнитуры на 051, «прогоняем» каждую строчку своей таблички и в конце забираем готовые файлики из /tmp/recs/ (не забывая менять их названия на что-то более понятное).

И подсказка для того, чтобы легче было пользоваться:

Recording Menu


UPD: Ссылка на bbcweather.sh: pastebin.com/DJYy6XRM и sed.bbc: pastebin.com/Xm4EeDst. Если кто будет делать с помощью нормального XML-парсера – не забудьте поделиться кодом :-).

UPD2: Выкладываю переписанный скрипт (на этот раз на Perl) и выдержку из extensions.conf:

weather.sh


#!/usr/bin/perl
  use XML::LibXML;
my $d = "/home/alexandr/xml/perl/bbc/moscow/";          # Working directory
my $f = "files/";                                       # Directory with weather files
my $wget = "wget -qO - http://newsrss.bbc.co.uk/weather/forecast/58/Next3DaysRSS.xml > ";
my $filename = "bbc.xml";                               # XML-file to process
my $i = 3;                                              # Counter: try to download XML-file $i time(s)
my $wait = 2;                                           # Wait for $wait second(s) between downloads
my $log = "/var/log/bbc_weather_moscow.log";            # Log-file :-) (used for logging failed download attempts)
my %days = ( "Sunday" => "day-0", "Monday" => "day-1", "Tuesday" => "day-2", "Wednesday" => "day-3", "Thursday" => "day-4", "Friday" => "day-5", "Saturday" => "day-6");

sub t2f{                                                # Subroutine for writing text to file
  my($text, $file, $app) = @_;                          # Read parameters $text, $file (add non-zero $app to append, but not rewrite file)
  if ($app) { open (OUT_FILE, '>>', $file); }
  else      { open (OUT_FILE, '>',  $file); }
  print OUT_FILE "$text";
  close (OUT_FILE);
}

sub noun{                                               # Choose noun for number
  my($number, $form1, $form2, $form5) = @_;             # Usage: &noun(1, градус, градуса, градусов);
  my $n10  = abs($number) % 10;
  my $n100 = abs($number) % 100;
  if ( $n100 > 4 && $n100 < 21 || $n10 == 0 || $n10 > 4 ) { return $form5 };
  if ( $n10  > 1 && $n10  < 5  ) { return $form2 };
  if ( $n10 == 1 ) { return $form1 };
  return $form5;
}

chdir $d;                                               # Go to working directory
DOWNLOAD:                                               # Label for 'Goto' (will be used in case XML has not been updated)
$i -= 1;
#print("Downloading...\n");
system($wget.$filename);
my $parser = XML::LibXML->new();
my $doc    = $parser->parse_file($filename);            # Parse this file ($filename)

#check if XML has been updated
open(FILE, 'date.txt');
my $date = <FILE>;
close(FILE);
my @xmldate = $doc->findnodes('//pubDate');
if ( $date eq @xmldate[0]->to_literal ) {
  my $t = localtime;
  if ($i) { &t2f("$t: XML has not been updated, I will try $i more ".&noun($i,"time","times","times")."\n",$log,1); }
  else    { &t2f("$t: XML has not been updated, I will not try any more\n",$log,1);
    exit;                                               # No more tries left -> Finish myself!
  }
  sleep $wait;
  goto DOWNLOAD;                                        # Tried to do with 'While' or "Do, while', but variables should be initialised outside loop, decided to use 'Goto'
}
else {
  &t2f(@xmldate[0]->to_literal,"date.txt");
}

#searching for //item/title and //item/description values
$i = 0;
foreach my $item ($doc->findnodes('//item')) {          # for each //item
  $i += 1;  # BTW, we should be counting from 1 to 3 (today, tomorrow and day after tomorrow)
  my $title = $item->findnodes('./title');      $title = $title->to_literal;    #take ./title and convert to string
  my $desc  = $item->findnodes('./description'); $desc = $desc->to_literal;     #take ./description and convert to string

  chdir $d.$f;                                                          # Go to directory with weather files
  if ( $title =~ m/^(\w+)/ ) {                                          # Match first word (weekday)
       &t2f($days{"$1"},$i."Wday"); }                                   # Export weekday in format day-0, day-1, etc
  if ( $title =~ m/: (\w+(\s\w+){0,}),/ ) {                             # Match one or more words, delimited with spaces (matched pattern should be followed by comma)
       &t2f("$1",$i."Prec"); }

  if ( $desc =~ m/Max Temp: (-{0,1}\d+)/ ) {
       &t2f("$1",$i."Tmax");                                            # Export Tmax (Max Temperature)
       &t2f(&noun("$1","gradus","gradusa","gradusov"),$i."DMax"); }     # Export Dmax (right form of "градус" for Tmax)

  if ( $desc =~ m/Min Temp: (-{0,1}\d+)/ ) {
       &t2f("$1",$i."Tmin");                                            # Export Tmin (Min Temperature)
       &t2f(&noun("$1","gradus","gradusa","gradusov"),$i."DMin"); }

  if ( $desc =~ m/Wind Direction: (\w+)/ ) {                            # Export Wdir (Wind Direction)
       &t2f("$1",$i."Wdir"); }

  if ( $desc =~ m/Wind Speed: (\d+)/ ) {
      &t2f(int("$1"*0.44704+0.5),$i."Wspd");                            # Export Wspd (mph*0.44704=kmh), int(kmh+0.5) - rounding to nearest integer
      &t2f(&noun("$1"*0.44704+0.5,"m_s","ma_s","mov_s"),$i."Wsms"); }           # Метр(а,ов) в секунду

  if ( $desc =~ m/Visibility: (\w+)/ ) {
      &t2f("$1",$i."Visb"); }                                           # Export Visb (Visibility)

  if ( $desc =~ m/Pressure: (\d+)/ ) {
      &t2f(int("$1"*0.75),$i."Pres");                                   # Export Pres (Pressure) (mb*0.75=mmHg)
      &t2f(&noun("$1","mm_hg","mma_hg","mmov_hg"),$i."Prmm"); }         # Миллиметр(а,ов) ртутного столба

  if ( $desc =~ m/Humidity: (\d+)/ ) {
      &t2f(int("$1"),$i."Humd");                                        # Export Humd (Humidity)
      &t2f(&noun("$1","procent","procenta","procentov"),$i."Hper"); }
}


extensions.conf


exten => 072,1,Set(wpath="/home/alexandr/xml/perl/bbc/moscow/files/")
exten => 072,n,System(echo "${STRFTIME(${EPOCH},,%Y.%m.%d %H:%M:%S)}\t${CALLERID(name)}\t${CALLERID(num)}" >> /var/log/calls_to_moscow_weather.log)
exten => 072,n,Goto(say-weather-perl,s,1)

[say-weather-perl]
exten => s,1,Answer()
exten => s,n,Set(E=1)
exten => s,n,Set(CHANNEL(language)=alex_01) ; By default use directory: /usr/share/asterisk/sounds/alex_01/
exten => s,n(play),Background(digits/${SHELL(cat ${wpath}${E}Wday)}) ; Weekday
exten => s,n,Background(weather/${SHELL(cat ${wpath}${E}Prec)}) ; Precipitation
exten => s,n,Background(weather/temperatura) ; Precipitation
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Tmin)}) ; Minimum temperature
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Tmax)}) ; Maximum temperature
exten => s,n,Background(weather/${SHELL(cat ${wpath}${E}DMax)}) ; Maximum degree(s)
exten => s,n,Background(weather/veter) ; Wind
exten => s,n,Background(weather/${SHELL(cat ${wpath}${E}Wdir)}) ; Wind Direction
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Wspd)}) ; Wind speed
exten => s,n,Background(weather/${SHELL(cat ${wpath}${E}Wsms)}) ; Meter(s) per seconD
exten => s,n,Background(weather/vlazhnost) ; Humidity
exten => s,n,SayNumber(${SHELL(cat ${wpath}${E}Humd)}) ; Humidity percentage
exten => s,n,Background(weather/${SHELL(cat ${wpath}${E}Hper)}) ; Percent(s)

exten => s,n,WaitExten()
exten => s,n,Hangup()
exten => _[1-3],1,Set(E=${EXTEN})
exten => _[1-3],n,Goto(say-weather-perl,s,play)

UPD3: Предоставляю вниманию хабрасообщества бесплатный сервис прогноза погоды по телефону для города Москва. Прогноз погоды доступен по телефону +7 (499) 753-00-09. Данный номер любезно предоставлен компанией Док.Телеком, которой выражается благодарность за оказание помощи в предоставлении данной услуги всем желающим. Спасибо :-)!

+8
5k 73
Comments 15
Top of the day