Skip to content

Main Index

Dr. Neil's Notes

Software > Coding

.NET Console Weather

Introduction

This exercise extends the .NET Console Clock to add the weather to the output. All the code in this note can run on any environment that supports .NET 6, that includes Microsoft Windows, Apple OSX, and Linux.

All the steps in this note assume the .NET Console Clock code exists, and adds to that code.

A video that accompanies this Note can be found here

Connect to OpenWeather

To obtain the weather for a location the OpenWeather service is used. To call the OpenWeather service a key is required. To obtain a key create an account on the OpenWeather site. On the site is a section API Keys, where you can manage and create keys. Keep the key handy, it is needed to complete this exercise.

Open the .csproj file in the project, this defines how the code is built. Add an ItemGroup element with the PackageReference to Weather.NET. The Weather.NET package contains code to make it simpler to get weather information from the OpenWeather service.

The project (.csproj) file should look like this:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

   <ItemGroup>
    <PackageReference Include="Weather.NET" Version="1.1.0" />
  </ItemGroup>
</Project>

Add OpenWeather API key

It is good practice to keep API keys separated from the code, often this is done in a configuration file that can be updated independently from the code. For this exercise, to keep it simple, the key will be stored in a different file.

In the folder containing the project add a new code file named Keys.cs. In this file create a class to store Keys, as follows:

internal static class Keys
{
    internal const string OpenWeather = "your key goes here";
}

Create a Celsius (or Fahrenheit) symbol

To display the temperature it is good to display the unit of measurement, helping to know if this is a temperature and which units are being used. In this exercise the temperature is being retrieved in Celsius.

After the other symbols, in the Program.cs code file, add a byte array for a Celsius symbol, like this:

byte[] Celsius = new byte[]{
        0b01000000,
        0b10100000,
        0b01000000,
        0b00001111,
        0b00011000,
        0b00011000,
        0b00001111,
    };

Display the weather

At the top of the Program.cs code file add the following using statements. These make it simpler to call the code in the Weather.NET package previously included.

using Weather.NET;
using Weather.NET.Enums;
using Weather.NET.Models.WeatherModel;

A using statement provides the compiler with information about the namespaces in which to find the code classes used in the file.

To retrieve the weather a WeatherClient object is created with the key that was defined earlier in the exercise. Then the current weather is retrieved for a city, in a measurement unit.

If you would prefer to use Fahrenheit, then set that as the metric unit. Replace city ("Sydney, NSW" in this example) with the city for which you want to display the weather. This code should go above the while loop that renders the clock.

WeatherClient client = new WeatherClient(Keys.OpenWeather);
WeatherModel currentWeather = client.GetCurrentWeather(cityName: "Sydney, NSW", measurement: Measurement.Metric);

To display the weather next to the clock add the following code inside the loop, after displaying the minutes for the clock. The code moves the cursor position to the right of the last minute digit and uses Console.Write to display the city and the title of the weather. Then the position is incremented and the temperature converted to an integer, as string, and displayed using the same DisplayDigits method as the hours and minutes in the clock. After the temperature digits the Celsius symbol is drawn.

Console.ResetColor();
Console.ForegroundColor = clockcolor;
position += digitWidth;
Console.SetCursorPosition(position, clockTop);
Console.Write($"{currentWeather.CityName}");
Console.SetCursorPosition(position, clockTop+6);
Console.Write($"{currentWeather.Weather[0].Title}");
position += digitWidth;
DisplayDigits(((int)currentWeather.Main.Temperature).ToString());
// draw the celsius symbol 
DrawDigit(Celsius, position, clockTop, clockcolor);

To run the code type dotnet run in the terminal and the following is an example of the output.

Displays the Weather

Update the weather

The code in the previous steps gets the weather once and displays that weather while the program is running. It would be better if the weather was updated every few minutes. The code below updates the weather once a minute, however that is probably more than is needed, once every five minutes would be fine.

    const string weatherCity = "Sydney, NSW";
    const int checkWeatherPeriod = 60;
    int currentPeriodSeconds = 0;
    WeatherClient client = new WeatherClient(Keys.OpenWeather);
    WeatherModel currentWeather = client.GetCurrentWeather(cityName: weatherCity, measurement: Measurement.Metric);

    while(Console.KeyAvailable is false)
    {
        Console.Clear();
        position = clockLeft;
        DateTime time = DateTime.Now;
        string hour = time.Hour.ToString().PadLeft(2, '0');
        DisplayDigits(hour);

        if (displayDots)
        {
            DrawDigit(Dots, position, clockTop, clockcolor);
        }

        displayDots = !displayDots;
        position += digitWidth;

        string minute = time.Minute.ToString().PadLeft(2, '0');
        DisplayDigits(minute);

        if (currentPeriodSeconds > checkWeatherPeriod)
        {
            currentWeather = client.GetCurrentWeather(cityName: weatherCity, measurement: Measurement.Metric);
            currentPeriodSeconds = 0;
        }
        currentPeriodSeconds++;

        Console.ResetColor();
        Console.ForegroundColor = clockcolor;
        position += digitWidth;
        Console.SetCursorPosition(position, clockTop);
        Console.Write($"{currentWeather.CityName}");
        Console.SetCursorPosition(position, clockTop+6);
        Console.Write($"{currentWeather.Weather[0].Title}");
        position += digitWidth;
        DisplayDigits(((int)currentWeather.Main.Temperature).ToString());
        // draw te celsius symbol
        DrawDigit(Celsius, position, clockTop, clockcolor);

        Console.ResetColor();
        await Task.Delay(1000);
    }

Before the while loop is started some variables are declared to support the weather being updated.

    const string weatherCity = "Sydney, NSW";
    const int checkWeatherPeriod = 60;
    int currentPeriodSeconds = 0;

The city is defined as a constant string (const means it will not be changed while the program is running).

The checkWeatherPeriod is set to 60, meaning the code should check the weather every 60 seconds. To change the code to check every five minutes, edit the line to const int checkWeatherPeriod = 5 * 60;

The currentPeriodSeconds is used to count up to the checkWeatherPeriod.

Inside the while loop the weather is retrieved every checkWeatherPeriod by the following code

    if (currentPeriodSeconds > checkWeatherPeriod)
    {
        currentWeather = client.GetCurrentWeather(cityName: weatherCity, measurement: Measurement.Metric);
        currentPeriodSeconds = 0;
    }
    currentPeriodSeconds++;

When the currentPeriodSeconds is greater than the checkWeatherPeriod, the latest weather is retrieved, and the currentPeriodSeconds is reset to 0.

The code currentPeriodSeconds++; increments the currentPeriodSeconds by 1.

The timing is not going to be exact using this technique, using the time it takes to run the while loop (including the Task.Delay), is not going to be exactly 1 second, so the weather will update approximately every checkWeatherPeriod seconds.

Splitting up the code

The code in the Program.cs file is getting longer as it is being added to. When building software it is a good practice to keep looking for ways to simplify the code, and break out code that has a specific purpose into a separate code file. This was done with the Keys.cs to manage the API key for the weather service. IT could also be done for the bytes representing all the characters being displayed. In the same folder as the Program.cs file, create a new file called Chars.cs, in this file we will create a static class that holds all the character byte arrays representing the digits and symbols being displayed.

internal static class Chars
{
    internal static byte[] Zero = new byte[]{
        0b11111111,
        0b11000011,
        0b11000011,
        0b11000011,
        0b11000011,
        0b11000011,
        0b11111111,
    };

    internal static byte[] One = new byte[]{
        0b00011000,
        0b00011000,
        0b00011000,
        0b00011000,
        0b00011000,
        0b00011000,
        0b00011000,
    };

    internal static byte[] Two = new byte[]{
        0b11111111,
        0b00000011,
        0b00000011,
        0b11111111,
        0b11000000,
        0b11000000,
        0b11111111,
    };

    internal static byte[] Three = new byte[]{
        0b11111111,
        0b00000011,
        0b00000011,
        0b01111111,
        0b00000011,
        0b00000011,
        0b11111111,
    };

    internal static byte[] Four = new byte[]{
        0b11000011,
        0b11000011,
        0b11000011,
        0b11111111,
        0b00000011,
        0b00000011,
        0b00000011,
    };

    internal static byte[] Five = new byte[]{
        0b11111111,
        0b11000000,
        0b11000000,
        0b11111111,
        0b00000011,
        0b00000011,
        0b11111111,
    };

    internal static byte[] Six = new byte[]{
        0b11111110,
        0b11000000,
        0b11000000,
        0b11111111,
        0b11000011,
        0b11000011,
        0b11111111,
    };

    internal static byte[] Seven = new byte[]{
        0b11111111,
        0b00000011,
        0b00000011,
        0b00000011,
        0b00000011,
        0b00000011,
        0b00000011,
    };

    internal static byte[] Eight = new byte[]{
        0b11111111,
        0b11000011,
        0b11000011,
        0b11111111,
        0b11000011,
        0b11000011,
        0b11111111,
    };

    internal static byte[] Nine = new byte[]{
        0b11111111,
        0b11000011,
        0b11000011,
        0b11111111,
        0b00000011,
        0b00000011,
        0b01111111,
    };

    internal static byte[] Dots = new byte[]{
        0b00000000,
        0b00000000,
        0b00011000,
        0b00000000,
        0b00011000,
        0b00000000,
        0b00000000,
    };

    internal static byte[] Celsius = new byte[]{
        0b01000000,
        0b10100000,
        0b01000000,
        0b00001111,
        0b00011000,
        0b00011000,
        0b00001111,
    };

    internal static byte[][] DigitArray = new byte[][]{Zero, One, Two, Three, Four, Five, Six, Seven, Eight, Nine};
}

Creating the class and variables as internal tells the compiler that this code will only be used inside this program, and not used by other programs.

The static keyword is used to indicate that this program should only ever create one instance of the class, and variables. That one instance will be shared by all the code in this program.

All the references to the chars in the Program.cs file will need to be updated to use the Chars class. For example:

 DrawDigit(Chars.Dots, position, clockTop, clockcolor);
````
and in the `DisplayDigit` method

```cs
DrawDigit(Chars.DigitArray[n], position, clockTop, clockcolor);

Conclusions

In this note the code to add the weather to the .NET Console Clock has been developed and explained. This code has been tested on a Windows PC, Apple Mac, and Raspberry Pi. It should run anywhere that .NET 6.0 can run.

Complete Code Listing

The project file

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

   <ItemGroup>
    <PackageReference Include="Weather.NET" Version="1.1.0" />
  </ItemGroup>
</Project>

The Program.cs file

using Weather.NET;
using Weather.NET.Enums;
using Weather.NET.Models.WeatherModel;

Console.CursorVisible = false;
Console.Title = "Console Clock";
int clockTop = 5;
int clockLeft = 5;
int digitWidth = 10;

ConsoleColor clockcolor = ConsoleColor.Green;
bool displayDots = false;
int position = clockLeft;
const int checkWeatherPeriod = 60;
int currentPeriodSeconds = 0;
const string weatherCity = "Sydney, NSW";
WeatherClient client = new WeatherClient(Keys.OpenWeather);
WeatherModel currentWeather = client.GetCurrentWeather(cityName: weatherCity, measurement: Measurement.Metric);

while (Console.KeyAvailable is false)
{
    Console.Clear();
    position = clockLeft;
    DateTime time = DateTime.Now;
    string hour = time.Hour.ToString().PadLeft(2, '0');
    DisplayDigits(hour);

    if (displayDots)
    {
        DrawDigit(Chars.Dots, position, clockTop, clockcolor);
    }

    displayDots = !displayDots;
    position += digitWidth;

    string minute = time.Minute.ToString().PadLeft(2, '0');
    DisplayDigits(minute);

    if (currentPeriodSeconds > checkWeatherPeriod)
    {
        currentWeather = client.GetCurrentWeather(cityName: weatherCity, measurement: Measurement.Metric);
        currentPeriodSeconds = 0;
    }
    currentPeriodSeconds++;

    Console.ResetColor();
    Console.ForegroundColor = clockcolor;
    position += digitWidth;
    Console.SetCursorPosition(position, clockTop);
    Console.Write($"{currentWeather.CityName}");
    Console.SetCursorPosition(position, clockTop + 6);
    Console.Write($"{currentWeather.Weather[0].Title}");
    position += digitWidth;
    DisplayDigits(((int)currentWeather.Main.Temperature).ToString());
    DrawDigit(Chars.Celsius, position, clockTop, clockcolor);

    Console.ResetColor();
    await Task.Delay(1000);
}

Console.CursorVisible = true;

Console.WriteLine();
Console.WriteLine("Thank you for using the console clock");

void DisplayDigits(string digits)
{
    foreach (var c in digits)
    {
        int n = int.Parse($"{c}");
        DrawDigit(Chars.DigitArray[n], position, clockTop, clockcolor);
        position += digitWidth;
    }
}

void DrawDigit(byte[] digit, int X, int Y, ConsoleColor color)
{
    foreach (byte row in digit)
    {
        for (int bitPosition = 0; bitPosition < 8; bitPosition++)
        {
            var mark = (row & (1 << bitPosition)) != 0;
            if (mark)
            {
                Draw(X + 8 - bitPosition, Y, color);
            }
        }
        Y++;
    }
}

static void Draw(int X, int Y, ConsoleColor Color)
{
    Console.SetCursorPosition(X, Y);
    Console.BackgroundColor = Color;
    Console.Write(" ");
}

The Keys.cs file, remember to put your API key in the string.

internal static class Keys
{
    internal const string OpenWeather = "Your key goes here ";
}

Note: The complete Chars.cs file is shown in the step earlier.


Last update: April 18, 2022 07:13:01
Created: January 21, 2022 22:45:56
Authors: Neil Roodyn