Создание домашнего погодного виджета с помощью Arduino и Raspberry

Создание домашнего погодного виджета с помощью Arduino и Raspberry

Продолжаем развивать наш «Умный дом» — следующий этап посвящён объединению устройств в единую сеть, часть 1.

Используем Open Weather Map

Чтобы построить умный дом, все интеллектуальные устройства нужно объединить в одну сеть. Стандартная Arduino UNO для этих целей не подходит. Как правило, эта плата выступает в роли периферийного узла, который связывает датчики и компоненты друг с другом, а затем передаёт данные в центральный «мозг». Эту роль «мозга» прекрасно выполняет Raspberry Pi. Останавливаться на подробном описании этой платы не стану. Она располагает всеми необходимыми функциями маломощного сервера, что идеально для IoT-устройств.

Связать два устройства можно несколькими способами, однако мы воспользуемся проводным соединением через UART – USB. На нашем сайте уже опубликована подробная статья об этом методе подключения: /arduinoplus/podkluchenie-raspberry-arduino/index, поэтому инструкцию по подключению и настройке мы здесь опустим.

Итак, первым делом мы задействуем готовый погодный сервис, из которого будем получать всю необходимую метеоинформацию по любым городам мира. Этот сервис — Open Weather Map. Официальный сайт — https://openweathermap.org/. Чтобы работать с функциями сервиса, потребуется специальный ключ, который выдаётся после регистрации аккаунта. Создав учётную запись, переходите по ссылке «API keys».

Перед вами тот самый ключ, который мы будем применять в нашем коде.

Ключ API на Open Weather Map

Если ключ уже получен — отлично, можно переходить ко второму пункту.

Сейчас я постараюсь кратко, но содержательно описать весь процесс обмена данными между Raspberry Pi и сервисом OpenWeatherMap. Raspberry отправляет HTTP-запрос на сервер, который в ответ возвращает JSON-ответ (также возможен XML- и HTML-ответ, однако их мы рассматривать не будем — подробности доступны на официальном сайте).

Выглядит это как-то так:

Запрос на сайт - http://api.openweathermap.org/data/2.5/weather?q=Rostov-na-Donu&appid=b6fc7c896bb814a45e2ea7e2b44758f3&units=metric

Ответ:

{"coord":{"lon":39.71,"lat":47.24},
"weather":[{"id":800,"main":"Clear","description":"clear sky","icon":"01n"}],"base":"stations",
"main":{"temp":23.24,"pressure":1017.01,"humidity":32,"temp_min":23.24,"temp_max":23.24,"sea_level":1025.15,"grnd_level":1017.01},
"wind":{"speed":3.42,"deg":43.0036},
"clouds":{"all":0},"dt":1534535099,
"sys":{"message":0.0033,"country":"RU","sunrise":1534472468,"sunset":1534523288},"id":501175,"name":"Rostov-na-Donu","cod":200}

На первый взгляд символов много и они кажутся непонятными, но в действительности каждый элемент запроса и ответа содержит полезную информацию.

Запрос включает несколько изменяемых элементов.

  1. Город: weather?q=Rostov-na-Donu

  2. Ключ: appid=b6fc7c896bb814a45e2ea7e2b44758f3

  3. Измерение: units=metric

В запросе допускается менять «город» — тогда ответ будет содержать данные для указанного города. «Ключ» подставляется тот, что вы получили в пункте 1. «Измерение» определяет единицы измерения в полученном ответе. Доступны 3 варианта: standard; metric; imperial.

Различия между единицами измерения описаны по ссылке: https://openweathermap.org/weather-data.

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

Программа для Raspberry pi

Raspberry Pi

Raspberry будет обращаться к погодному сервису по запросу от Arduino и затем отправлять полученные метеоданные по городу обратно на Arduino. С логической точки зрения в этом нет необходимости, ведь Raspberry справится с задачей самостоятельно, но подобный опыт взаимодействия двух плат окажется весьма полезным на практике.

Для начала организуем вывод полученных от сервера данных на экран, чтобы убедиться в корректной работе нашего кода и сервиса.

import requests
from pprint import pprint

city = input('Enter your city : ')

url = 'http://api.openweathermap.org/data/2.5/weather?q={}&appid='ваш ключ'&units=metric'.format(city)

res = requests.get(url)

data = res.json()

temp = data['main']['temp']
wind_speed = data['wind']['speed']

latitude = data['coord']['lat']
longitude = data['coord']['lon']

description = data['weather'][0]['description']

print('Temperature : {} degree celcius'.format(temp))
print('Wind Speed : {} m/s'.format(wind_speed))
print('Latitude : {}'.format(latitude))
print('Longitude : {}'.format(longitude))
print('Description : {}'.format(description))

После ввода города „Moscow“ программа выдала точные метеоданные по Москве на ближайшие 3 часа. Убедитесь, что название города введено правильно — на карте Open Weather Map можно посмотреть корректные названия. К примеру, мой родной город «Ростов-на-Дону» (англ. «Rostov-on-Don») на этом сервисе называется «Rostov-na-Donu», что поначалу может вызвать некоторое замешательство.

Результат выполнения программы для Raspberry Pi

Если всё заработало, доработайте код таким образом, чтобы из serial-порта поступал текст — в нашем случае названия городов. Raspberry подключается к погодному сервису, запрашивает актуальные метеоданные по присланному городу и затем отправляет часть этих данных обратно.

mport serial
import requests
from pprint import pprint

ser=serial.Serial("/dev/ttyACM0",9600)

def send():
ser.write(temp.encode())
ser.write(wind_speed.encode())
ser.write(latitude.encode())
ser.write(longitude.encode())
ser.write(humidity.encode())

while True:
     city=ser.readline().decode()
     if(city!=""):
             print(city)
             url = 'http://api.openweathermap.org/data/2.5/weather?q={}&appid='ваш ключ'f3&units=metric'.format(city)
             res = requests.get(url)
             data = res.json()
             temp = data['main']['temp']
             temp = str(int(temp))+"\r\n"
             wind_speed = data['wind']['speed']
             wind_speed = str(int(wind_speed))+"\r\n"
             latitude = data['coord']['lat']
             latitude = str(int(latitude))+"\r\n"
             longitude = data['coord']['lon']
             longitude = str(int(longitude))+"\r\n"
             humidity = data['main']['humidity']
             humidity = str(int(humidity))+"\r\n"
             send()

Программа на Arduino UNO

Arduino UNO

Эта плата будет выполнять функции, описанные в одной из предыдущих статей (управление на расстоянии): отправлять команды и принимать информацию, которую затем можно будет просмотреть на ЖК-дисплее.

При помощи потенциометра выбираем один из трёх городов (Ростов-на-Дону, Санкт-Петербург, Москва) и нажимаем кнопку для отправки данных о выбранном городе на Raspberry. При успешном получении данных на экране появится список погодных условий указанного города на ближайшее время, а также его географические координаты.

Полную электронную схему подключения модулей к Arduino вы найдёте в предыдущей статье «Управление умным домом на расстоянии (продолжение)» — она не отличается от нашей.

Схема подключения модулей к Arduino

Ниже приведён код, который можно скачать или скопировать:

Скачать kod_smarthome.ino

int data = 0; // присылаемые данные
int reading = 8; // пин, который считывает значение с кнопки `
int val = 0; // значение с реостата
boolean butwheather=0; // счетчик запуска меню
boolean but = 0; // значение кнопки до нажатия
boolean counter = 0; // еще один счетчик...
boolean currentButton = 0; // значение кнопки после нажатия
boolean zum = 0; // счетчик звука зуммера
String data1; // приходимые данные
String data2;
String data3;
String data4;
String data5;

#define BUZZER_PIN     7  // пин для зуммера
#include <LiquidCrystal.h> // библиотека для жк-дисплея
LiquidCrystal lcd(12, 11, 5, 4, 3, 2); // выбираем 6 пинов для подключения

  void setup()
{
  lcd.begin(16, 2);
  Serial.begin(9600);
  Serial.setTimeout(40); // При вызове parseInt() задержка ожидания последующего символа по умолчанию 1 секунда.
  // Функцией setTimeout() можно уменьшить эту задержку. Например на 40 миллисекунд, setTimeout(40);.
  pinMode(reading,INPUT); //
}

  void signaling() // функция для звучания зуммера
{
  tone(BUZZER_PIN, 50, 300);
  delay(300);
  zum+=1;
}

boolean Button( boolean got ) // функция, которая принимает предыдущее значение нажатия кнопки и отправляет текущие значение
{
  boolean current = digitalRead(reading);
  if( !got == current)
    {
      delay(10);
      current = digitalRead(reading);
    }
    return(current);
}

  void menu () // функция выводы на жк-дислей меню и отправки команды
{
  while(butwheather==0)
  {
  if ( val>=0 && val<=270)
     {
      zum = 0;
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Rostov-on-Don");
      while (val>=0 && val<=270)
      {
        currentButton = Button(but);
        if (currentButton == 0 && but ==0)
        {
          Serial.println("Rostov-na-Donu");
          butwheather=1;
          break;
        }
        but = currentButton;
        val = analogRead(A0);
      }
      if (zum==0)
      {
        signaling();
      }
     }
  if ( val>=271 && val<=700)
     {
      zum = 0;
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Saint Petersburg");
      while (val>=271 && val<=700)
      {
        currentButton = Button(but);
        if (currentButton == 0 && but ==0)
        {
          Serial.println("Saint Petersburg");
          butwheather=1;
          break;
        }
        but = currentButton;
        val = analogRead(A0);
      }
      if (zum==0)
      {
        signaling();
      }
     }
  if ( val>=701 && val<=1023)
     {
      zum = 0;
      lcd.clear();
      lcd.setCursor(0, 0);
      lcd.print("Moscow");
      while (val>=701 && val<=1023)
      {
        currentButton = Button(but);
        if (currentButton == 0 && but ==0)
        {
          Serial.println("Moscow");
          butwheather=1;
          break;
        }
        but = currentButton;
        val = analogRead(A0);
      }
      if (zum==0)
      {
        signaling();
      }
     }
  }
}

  void information() // функция приема данных с платы
{
  data1=Serial.parseInt();
  data2=Serial.parseInt();
  data3=Serial.parseInt();
  data4=Serial.parseInt();
  data5=Serial.parseInt();
}

  void readout() // функция вывода данных на жк-дисплей с помощью значения с потенциометра
{
  val = analogRead(A0);
  while(counter==0)
  {
    if(val>=0 && val<=204)
    {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Temp:");
      lcd.print(data1);
      lcd.print("C");
      lcd.setCursor(0,1);
      lcd.print("Wind:");
      lcd.print(data2);
      lcd.print("m/s");
      while(val>=0 && val<=204)
        val = analogRead(A0);
    }
    if(val>=205 && val<=409)
    {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Wind:");
      lcd.print(data2);
      lcd.print("m/s");
      lcd.setCursor(0,1);
      lcd.print("Lat:");
      lcd.print(data3);
      lcd.print("`");
      while(val>=205 && val<=409)
        val = analogRead(A0);
    }
    if(val>=410 && val<=614)
    {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Lat:");
      lcd.print(data3);
      lcd.print("`");
      lcd.setCursor(0,1);
      lcd.print("Lon:");
      lcd.print(data4);
      lcd.print("`");
      while(val>=410 && val<=614)
        val = analogRead(A0);
    }
    if(val>=615 && val<=819)
    {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Lon:");
      lcd.print(data4);
      lcd.print("`");
      lcd.setCursor(0,1);
      lcd.print("Humdi:");
      lcd.print(data5);
      lcd.print("%");
      while(val>=615 && val<=819)
        val = analogRead(A0);

    }
    if(val>=820 && val<=1023)
    {
      lcd.clear();
      lcd.setCursor(0,0);
      lcd.print("Exit?");
      while(val>=820 && val<=1023)
      {
        val = analogRead(A0);
        currentButton = Button(but);
        if (currentButton == 0 && but ==0)
        {
          counter+=1;
          break;
        }
        but = currentButton;
      }

    }
  }
  counter=0;
}

  void loop()
{
  val = analogRead(A0); //  читаем значение реостата и выбираем режим в зависимости от положения ручки
  menu(); // выбираем один из трех городов
  if (Serial.available()> 0)  // ждем, пока данных придут
  {
   information(); // считываем данные
   readout(); // показываем присланные данные
   butwheather=0; // онулируем счетчик
   delay(400); // немного ждем перед следующим запросом
  }
}
Результат работы погодного виджета на Arduino

Если всё прошло успешно, можете поэкспериментировать с погодным сервисом и расширить автоматизацию — подробности о возможностях погодного виджета легко найти в сети. На этом первая часть завершена, а скоро мы увидимся во второй части, где речь пойдёт о протоколе MQTT и его практической реализации. Развивайтесь вместе с вашим умным домом!