Консольная утилита погоды на C# с помощью .Net

от автора

Что необходимо получить и изучить, чтобы начать получать прогноз погоды на 5 дней? 

Во-первых, определиться с поставщиком погодных данных. Во-вторых, разобрать, в каком виде поставляются данные и как мы их можем собирать и отображать с помощью языка программирования C#. 

В качестве поставщика погодных данных я выбрал сервис Accuweather. В бесплатной учётной записи на данный момент можно сделать 50 запросов в сутки. Этого достаточно, чтобы можно было посмотреть данные о погоде несколько раз в сутки (даже можно поделиться с другом!). 

Для регистрации необходимо пройти по ссылке: https://developer.accuweather.com. После регистрации нужно нажать на кнопку «Add a new App» и заполнить небольшую анкету. По итогу, вы получите ваш личный ApiKey с помощью которого в дальнейшем можно получать обновлённые данные. 

Далее начинается самое интересно. Будем разбирать, как и в каком виде приходит информация и что нужно, чтобы получать погодные данные по конкретному городу. 

В разделе «API Reference» самым первым списке установлен раздел «Locations API» с него и начнём. Забегая вперёд, скажу сразу, нельзя так просто взять и отправить в GET запросе название города. Для этого, нам нужно сперва получить Location Key конкретного города. Это значение представлено в виде цифр и уникально для каждого города. 

Итак, в разделе Locations API нас интересует метод City Search. Читаем краткое описание к нему: Returns information for an array of cities that match the search text. Сразу берём себе на заметку, что нам возвращается массив с названиями городов. 

В странице для запроса вставляем ApiKey, название интересующего города и ставим RU, если хотим получать локализованные данные. 

После нажатия на кнопку «Send this request» ниже на странице вы получите результат выполнения. В моём случае он выглядит так:

[   {     "Version": 1,     "Key": "292332",     "Type": "City",     "Rank": 21,     "LocalizedName": "Chelyabinsk",     "EnglishName": "Chelyabinsk",     "PrimaryPostalCode": "",     "Region": {       "ID": "ASI",       "LocalizedName": "Asia",       "EnglishName": "Asia"     },     "Country": {       "ID": "RU",       "LocalizedName": "Russia",       "EnglishName": "Russia"     },     "AdministrativeArea": {       "ID": "CHE",       "LocalizedName": "Chelyabinsk",       "EnglishName": "Chelyabinsk",       "Level": 1,       "LocalizedType": "Oblast",       "EnglishType": "Oblast",       "CountryID": "RU"     },     "TimeZone": {       "Code": "YEKT",       "Name": "Asia/Yekaterinburg",       "GmtOffset": 5,       "IsDaylightSaving": false,       "NextOffsetChange": null     },     "GeoPosition": {       "Latitude": 55.16,       "Longitude": 61.403,       "Elevation": {         "Metric": {           "Value": 233,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 764,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Chelyabinsk",         "EnglishName": "Chelyabinsk"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts",       "ForecastConfidence"     ]   } ]

Как видим, нам вернулся массив из одного города. Как будет выглядеть результат, если городов с одинаковым названием два и больше:

[   {     "Version": 1,     "Key": "294021",     "Type": "City",     "Rank": 10,     "LocalizedName": "Москва",     "EnglishName": "Moscow",     "PrimaryPostalCode": "",     "Region": {       "ID": "ASI",       "LocalizedName": "Азия",       "EnglishName": "Asia"     },     "Country": {       "ID": "RU",       "LocalizedName": "Россия",       "EnglishName": "Russia"     },     "AdministrativeArea": {       "ID": "MOW",       "LocalizedName": "Москва",       "EnglishName": "Moscow",       "Level": 1,       "LocalizedType": "Город федерального подчинения",       "EnglishType": "Federal City",       "CountryID": "RU"     },     "TimeZone": {       "Code": "MSK",       "Name": "Europe/Moscow",       "GmtOffset": 3,       "IsDaylightSaving": false,       "NextOffsetChange": null     },     "GeoPosition": {       "Latitude": 55.752,       "Longitude": 37.619,       "Elevation": {         "Metric": {           "Value": 155,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 508,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Tsentralny",         "EnglishName": "Tsentralny"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts",       "ForecastConfidence"     ]   },   {     "Version": 1,     "Key": "1397263",     "Type": "City",     "Rank": 85,     "LocalizedName": "Москва",     "EnglishName": "Moskwa",     "PrimaryPostalCode": "",     "Region": {       "ID": "EUR",       "LocalizedName": "Европа",       "EnglishName": "Europe"     },     "Country": {       "ID": "PL",       "LocalizedName": "Польша",       "EnglishName": "Poland"     },     "AdministrativeArea": {       "ID": "10",       "LocalizedName": "Лодзинское воеводство",       "EnglishName": "Łódź",       "Level": 1,       "LocalizedType": "Воеводство",       "EnglishType": "Voivodship",       "CountryID": "PL"     },     "TimeZone": {       "Code": "CET",       "Name": "Europe/Warsaw",       "GmtOffset": 1,       "IsDaylightSaving": false,       "NextOffsetChange": "2021-03-28T01:00:00Z"     },     "GeoPosition": {       "Latitude": 51.816,       "Longitude": 19.657,       "Elevation": {         "Metric": {           "Value": 238,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 780,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Восточно-Лодзинский повят",         "EnglishName": "Łódź East"       },       {         "Level": 3,         "LocalizedName": "Новосольна",         "EnglishName": "Nowosolna"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts",       "ForecastConfidence",       "FutureRadar",       "MinuteCast",       "Radar"     ]   },   {     "Version": 1,     "Key": "580845",     "Type": "City",     "Rank": 85,     "LocalizedName": "Москва",     "EnglishName": "Moskva",     "PrimaryPostalCode": "",     "Region": {       "ID": "ASI",       "LocalizedName": "Азия",       "EnglishName": "Asia"     },     "Country": {       "ID": "RU",       "LocalizedName": "Россия",       "EnglishName": "Russia"     },     "AdministrativeArea": {       "ID": "KIR",       "LocalizedName": "Киров",       "EnglishName": "Kirov",       "Level": 1,       "LocalizedType": "Республика",       "EnglishType": "Republic",       "CountryID": "RU"     },     "TimeZone": {       "Code": "MSK",       "Name": "Europe/Moscow",       "GmtOffset": 3,       "IsDaylightSaving": false,       "NextOffsetChange": null     },     "GeoPosition": {       "Latitude": 57.968,       "Longitude": 49.104,       "Elevation": {         "Metric": {           "Value": 207,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 678,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Verkhoshizhemsky",         "EnglishName": "Verkhoshizhemsky"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts",       "ForecastConfidence"     ]   },   {     "Version": 1,     "Key": "2488304",     "Type": "City",     "Rank": 85,     "LocalizedName": "Москва",     "EnglishName": "Moskva",     "PrimaryPostalCode": "",     "Region": {       "ID": "ASI",       "LocalizedName": "Азия",       "EnglishName": "Asia"     },     "Country": {       "ID": "RU",       "LocalizedName": "Россия",       "EnglishName": "Russia"     },     "AdministrativeArea": {       "ID": "PSK",       "LocalizedName": "Псков",       "EnglishName": "Pskov",       "Level": 1,       "LocalizedType": "Область",       "EnglishType": "Oblast",       "CountryID": "RU"     },     "TimeZone": {       "Code": "MSK",       "Name": "Europe/Moscow",       "GmtOffset": 3,       "IsDaylightSaving": false,       "NextOffsetChange": null     },     "GeoPosition": {       "Latitude": 57.449,       "Longitude": 29.185,       "Elevation": {         "Metric": {           "Value": 161,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 528,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Porkhovsky",         "EnglishName": "Porkhovsky"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts",       "Radar"     ]   },   {     "Version": 1,     "Key": "580847",     "Type": "City",     "Rank": 85,     "LocalizedName": "Москва",     "EnglishName": "Moskva",     "PrimaryPostalCode": "",     "Region": {       "ID": "ASI",       "LocalizedName": "Азия",       "EnglishName": "Asia"     },     "Country": {       "ID": "RU",       "LocalizedName": "Россия",       "EnglishName": "Russia"     },     "AdministrativeArea": {       "ID": "TVE",       "LocalizedName": "Тверь",       "EnglishName": "Tver'",       "Level": 1,       "LocalizedType": "Область",       "EnglishType": "Oblast",       "CountryID": "RU"     },     "TimeZone": {       "Code": "MSK",       "Name": "Europe/Moscow",       "GmtOffset": 3,       "IsDaylightSaving": false,       "NextOffsetChange": null     },     "GeoPosition": {       "Latitude": 56.918,       "Longitude": 32.163,       "Elevation": {         "Metric": {           "Value": 251,           "Unit": "m",           "UnitType": 5         },         "Imperial": {           "Value": 823,           "Unit": "ft",           "UnitType": 0         }       }     },     "IsAlias": false,     "SupplementalAdminAreas": [       {         "Level": 2,         "LocalizedName": "Penovsky",         "EnglishName": "Penovsky"       }     ],     "DataSets": [       "AirQualityCurrentConditions",       "AirQualityForecasts",       "Alerts"     ]   } ]

Как видно, нам вернулся массив уже из большего количества городов. Как я говорил раньше, нужно получить уникальное значение Key, чтобы получать данные о погоде нужного нам города. Это значение установлено вторым в каждом городе.

Итак, представление о том, с чем нам нужно работать с начала есть. Теперь, осталось переложить всё в C#. Для большего интереса, я буду программировать на языке C# и используя редактор VSCodium. Всё это работает на OpenSuSe Leap 15.2.

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

Класс, который описывает UserApi:

namespace habraweatherappconsole {     public class UserApi     {         public string UserApiProperty { get;set; }     } }

Далее, создаю класс, который будет реализовывать методы чтения и записи:

        /// <summary>         /// Метод реализуе возможность восстановления списка APIKey в памяти         /// </summary>         public static void ReadUserApiToLocalStorage()         {             XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<UserApi>));              try             {                 using (StreamReader sr = new StreamReader("UserApi.xml"))                 {                     userApiList = xmlSerializer.Deserialize(sr) as ObservableCollection<UserApi>;                 }             }              catch(Exception ex)             {                 /* Не вывожу никаких сообщений об ошибке. Потому как, если утилита была запущена впервые                 / то файла скорее всего нет. Даже если бы он был и из-за каких-то аппаратных проблем стал недоступен                 / то что я могу с этим поделать в таком случае?                 */             }         }

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

Сделано. Дальше, начинается та часть работы, которая требует внимательного подхода к ней. Нужно реализовать класс, который будет соответствовать получаемому Json объекту. Любая ошибка в переносе может привести к тому, что утилита будет падать с ошибками.

Итак, что мы видим в верхушке получаемого объекта:

    "Version": 1,     "Key": "292332",     "Type": "City",     "Rank": 21,     "LocalizedName": "Chelyabinsk",     "EnglishName": "Chelyabinsk",     "PrimaryPostalCode": "", 

Мы видим версию API в виде числа, уникальный ключ города в виде числа, тип населённого пункта в виде строки, Ранг (не смог найти, что это значит) в виде числа, оригинальное и локализованное название города в виде строки и почтовый индекс. Почтовый индекс вернулся пустой, поэтому, беру строку из-за её универсальности.

Следовательно, реализовываем эту часть так:

    public class RootBasicCityInfo    {         public int Version { get; set; }          public string Key { get; set; }          public string Type { get; set; }          public int Rank { get; set; }          public string LocalizedName { get; set; }          public string EnglishName { get; set; }          public string PrimaryPostalCode { get; set; }  

Так же, в json объекте присутствует и другие сведения об искомом городе:

      "Region": {         "ID": "ASI",         "LocalizedName": "Азия",         "EnglishName": "Asia"       },       "Country": {         "ID": "RU",         "LocalizedName": "Россия",         "EnglishName": "Russia"       },       "AdministrativeArea": {         "ID": "MOW",         "LocalizedName": "Москва",         "EnglishName": "Moscow",         "Level": 1,         "LocalizedType": "Город федерального подчинения",         "EnglishType": "Federal City",         "CountryID": "RU"       }, 

Поэтому, там же реализуем классы для Region, Country, AdministrativeArea примерно так:

public class Region    {         public string ID { get; set; }          public string LocalizedName { get; set; }          public string EnglishName { get; set; }      }      public class Country    {         public string ID { get; set; }          public string LocalizedName { get; set; }          public string EnglishName { get; set; }      }      public class AdministrativeArea    {         public string ID { get; set; }          public string LocalizedName { get; set; }          public string EnglishName { get; set; }          public int Level { get; set; }          public string LocalizedType { get; set; }          public string EnglishType { get; set; }          public string CountryID { get; set; }      } 

Итоговый класс должен выглядеть примерно так:

    public class RootBasicCityInfo    {         public int Version { get; set; }          public string Key { get; set; }          public string Type { get; set; }          public int Rank { get; set; }          public string LocalizedName { get; set; }          public string EnglishName { get; set; }          public string PrimaryPostalCode { get; set; }          public Region Region { get; set; }          public Country Country { get; set; }          public AdministrativeArea AdministrativeArea { get; set; }          public TimeZone TimeZone { get; set; }          public GeoPosition GeoPosition { get; set; }          public bool IsAlias { get; set; }          public List<SupplementalAdminArea> SupplementalAdminAreas { get; set; }          public List<string> DataSets { get; set; }      } 

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

using System; using System.Collections.ObjectModel; using System.Net; using System.Text.Json;  using static System.Console;  namespace habraweatherappconsole {     /// <summary>     /// Класс описывает возможность получения поиска     /// и добавления городов для их последующего мониторинга.     /// </summary>     public static class SearchCity     {         /// <summary>         /// Метод реализует возможность получения списка городов.         /// В качестве формального параметра принимается название города         /// которое должно быть указано в классе MainMenu.         /// </summary>         /// <param name="formalCityName"></param>         public static void GettingListOfCitiesOnRequest(string formalCityName)         {             // Получаю ApiKey из списка             string apiKey = UserApiManager.userApiList[0].UserApiProperty;             try             {                 string jsonOnWeb = $"http://dataservice.accuweather.com/locations/v1/cities/search?apikey={apiKey}&q={formalCityName}";                  WebClient webClient = new WebClient();                 string prepareString = webClient.DownloadString(jsonOnWeb);                  ObservableCollection<RootBasicCityInfo> rbci = JsonSerializer.Deserialize<ObservableCollection<RootBasicCityInfo>>(prepareString);                  DataRepo.PrintКeceivedСities(rbci);             }             catch (Exception ex)             {                 WriteLine("Неполучилось отобразить запрашиваемый город."                 + "Возможные причины: \n" +                  "* Неправильно указано название города\n"                 + "* Нет доступа к интернету\n"                 + "Подробнее ниже: \n"                 + ex.Message);             }          }     } }

Реализую метод, который выводит на экран полученный список:

        /// <summary>         /// Метод реализует возможность отображать список запрашиваемых городов         /// (Если городов с таким названием больше, чем 1).         /// </summary>         /// <param name="formalListOfCityes"></param>         public static void PrintКeceivedСities (ObservableCollection<RootBasicCityInfo> formalListOfCityes)         {             string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"             + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"             + "Тип: {5}\n" + "====\n";             int numberInList = 0;              foreach (var item in formalListOfCityes)             {                 WriteLine(pattern, numberInList.ToString(),                 item.EnglishName, item.LocalizedName, item.Country.LocalizedName,                 item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);                 numberInList++;             }              Write ("Номер какого города добавить в мониторинг: ");             int num = Convert.ToInt32(Console.ReadLine());              try             {                 listOfCityForMonitorWeather.Add(formalListOfCityes[num]);             }              catch (Exception ex)             {                 WriteLine("Похоже, вы ошиблись цифрой.\n");                 WriteLine(ex.Message);             }             WriteListOfCityMonitoring();         } 

Метод, который реализует запись списка со всеми отслеживаемыми городами на диск (чтобы при повторном запуске не тратить драгоценный APIKey)

         /// <summary>         /// Метод реализует возможность записывать список отслеживаемых городов         /// на жёсткий диск.         /// </summary>         private static void WriteListOfCityMonitoring()         {             XmlSerializer xmlSerializer = new XmlSerializer(typeof(ObservableCollection<RootBasicCityInfo>));              using (StreamWriter sw = new StreamWriter("RootBasicCityInfo.xml"))             {                 xmlSerializer.Serialize(sw, listOfCityForMonitorWeather);             }         } 

Отлично. Всё необходимое сделано для того, что бы перейти к следующей части реализации работы утилиты — получение информации и погоде на следующие 5 дней.

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

Accuweather может предоставить информацию на 1 текущий день, 5, 10 и 15 дней. В ответ будет приходить json объект одного и того же типа. Разница будет только в Get запросе и количестве возвращаемых дней.

Пример того, что возвращается в json объекте:

  "Headline": {     "EffectiveDate": "2021-02-23T07:00:00+03:00",     "EffectiveEpochDate": 1614052800,     "Severity": 3,     "Text": "Окончание понижения температуры: Среда",     "Category": "cold",     "EndDate": "2021-02-24T19:00:00+03:00",     "EndEpochDate": 1614182400,     "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/extended-weather-forecast/294021?unit=c",     "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?unit=c"   },   "DailyForecasts": [     {       "Date": "2021-02-23T07:00:00+03:00",       "EpochDate": 1614052800,       "Temperature": {         "Minimum": {           "Value": -24.4,           "Unit": "C",           "UnitType": 17         },         "Maximum": {           "Value": -20.6,           "Unit": "C",           "UnitType": 17         }       },       "Day": {         "Icon": 31,         "IconPhrase": "Холодно",         "HasPrecipitation": false       },       "Night": {         "Icon": 31,         "IconPhrase": "Холодно",         "HasPrecipitation": false       },       "Sources": [         "AccuWeather"       ],       "MobileLink": "http://m.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c",       "Link": "http://www.accuweather.com/ru/ru/moscow/294021/daily-weather-forecast/294021?day=1&unit=c"     }, 

Следовательно, класс должен выглядеть примерно так:

    public class DailyForecast    {         public DateTime Date { get; set; }          public int EpochDate { get; set; }          public Temperature Temperature { get; set; }          public Day Day { get; set; }          public Night Night { get; set; }          public List<string> Sources { get; set; }          public string MobileLink { get; set; }          public string Link { get; set; }      }            public class RootWeather    {         public Headline Headline { get; set; }          public List<DailyForecast> DailyForecasts { get; set; }      } 

Исходим из того, что пользователь может добавить (и уже добавил) один или несколько городов. Для того, чтобы показать погоду по интересующему городу, сперва выведем на экран все города, какие уже есть:

            string pattern = "=====\n" + "Номер в списке: {0}\n" + "Название в оригинале: {1}\n"             + "В переводе:  {2} \n" + "Страна: {3}\n" + "Административный округ: {4}\n"             + "Тип: {5}\n" + "====\n";             int numberInList = 0;              foreach (var item in DataRepo.listOfCityForMonitorWeather)             {                 WriteLine(pattern, numberInList.ToString(),                 item.EnglishName, item.LocalizedName, item.Country.LocalizedName,                 item.AdministrativeArea.LocalizedName, item.AdministrativeArea.LocalizedType);                 numberInList++;             }                          bool ifNotExists = false;             string cityKey = null;             int num = 0;             do             {                 ifNotExists = false;                 Write("Номер города для просмотра погоды: ");                 num = Convert.ToInt32(Console.ReadLine());                                  if (num < 0 || num > DataRepo.listOfCityForMonitorWeather.Count - 1)                 {                     WriteLine("Такого номера нет. Попробуйте ещё раз.");                     ifNotExists = true;                 }             } while(ifNotExists);                          cityKey = DataRepo.listOfCityForMonitorWeather[num].Key; 

А затем получаю информацию о погоде:

 // Получаю ApiKey из списка             string apiKey = UserApiManager.userApiList[0].UserApiProperty;                          string jsonUrl = $"http://dataservice.accuweather.com/forecasts/v1/daily/5day/{cityKey}?apikey={apiKey}&language=ru&metric=true";              jsonUrl = webClient.DownloadString(jsonUrl);              RootWeather weatherData = JsonSerializer.Deserialize<RootWeather>(jsonUrl);              string patternWeather = "=====\n" + "Дата: {0}\n" + "Температура минимальная: {1}\n"             +"Температура максимальная: {2}\n" + "Примечание на день: {3}\n" + "Примечание на ночь: {4}\n" + "====\n";              foreach (var item in weatherData.DailyForecasts)             {                 WriteLine(patternWeather, item.Date, item.Temperature.Minimum.Value,                 item.Temperature.Maximum.Value, item.Day.IconPhrase, item.Night.IconPhrase);             }

Таким образом будет получена и напечатана информация о погоде на следующие 5 дней.

В заключении: в этой статье я показал основные моменты, которые были необходимы для того, чтобы данные о погоде были показаны. Полный исходный код утилиты вы сможете найти на ГитЛаб и ГитХаб. Так же, я буду рад любой критике по делу и совету от старших программистов.

Спасибо за уделённое время, удачи!

ссылка на оригинал статьи https://habr.com/ru/post/544592/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *