diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..8c606e1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,19 @@
+name: Build for CI
+
+on:
+ push:
+ branches: [ "main" ]
+ paths-ignore:
+ - "**.md"
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build-plugin-ci:
+
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Build
+ run: dotnet build src\Indiko.OpenWeatherClient.sln -c Release
diff --git a/.github/workflows/release-nuget.yml b/.github/workflows/release-nuget.yml
new file mode 100644
index 0000000..0d74ea0
--- /dev/null
+++ b/.github/workflows/release-nuget.yml
@@ -0,0 +1,27 @@
+name: Create a (Pre)release on NuGet
+
+on:
+ push:
+ tags:
+ - "v[0-9]+.[0-9]+.[0-9]+"
+ - "v[0-9]+.[0-9]+.[0-9]+-preview[0-9]+"
+jobs:
+ release-nuget:
+
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v3
+ - name: Verify commit exists in origin/main
+ run: |
+ git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
+ git branch --remote --contains | grep origin/main
+ - name: Get version information from tag
+ id: get_version
+ uses: battila7/get-version-action@v2
+ - name: Pack
+ run: dotnet pack src\Indiko.OpenWeatherClient.sln -c Release -p:PackageVersion=${{ steps.get_version.outputs.version-without-v }}
+ - name: Push
+ run: dotnet nuget push src\Indiko.OpenWeatherClient\bin\Release\Indiko.OpenWeatherClient.${{ steps.get_version.outputs.version-without-v }}.nupkg -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.NUGET_API_KEY }}
diff --git a/README.md b/README.md
index b41d628..2c7d2a1 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,89 @@
+![](nuget.png)
+
# Indiko.OpenWeatherClient
-Another C# client for openweathermap.org API
+The `Indiko.OpenWeatherClient` is a .NET library for interacting with the OpenWeatherMap APIs. It provides easy access to real-time weather data and map tiles, ensuring seamless integration with .NET applications.
+
+## Build Status
+![ci](https://github.com/0xc3u/Indiko.OpenWeatherClient/actions/workflows/ci.yml/badge.svg)
+
+## Install Controls
+[![NuGet](https://img.shields.io/nuget/v/Indiko.OpenWeatherClient.svg?label=NuGet)](https://www.nuget.org/packages/Indiko.OpenWeatherClient/)
+
+Available on [NuGet](http://www.nuget.org/packages/Indiko.OpenWeatherClient).
+
+Install with the dotnet CLI: `dotnet add package Indiko.OpenWeatherClient`, or through the NuGet Package Manager in Visual Studio.
+
+## Features
+- Fetch current weather, hourly forecast, and daily forecast.
+- Access to weather map tiles.
+- Supports Dependency Injection and manual instantiation.
+- Configurable through a fluent API for building request parameters.
+
+
+> **Important note**:
+> To use this client wrapper for openweathermap.org you need have a valid api-key from https://openweathermap.org.
+
+## Platform supported
+
+| Platform | Version Supported | Reference |
+|----------|--------------------------|-|
+| .NET | 8.0 |
+
+
+## API Coverage
+
+- Weather Data: Access current weather, hourly forecast, daily forecast. (https://openweathermap.org/api/one-call-3)
+- Maps API: Retrieve specific map tiles based on geographical and zoom parameters. (https://openweathermap.org/api/weathermaps)
+
+> For detailed API usage and more examples, please refer to the official documentation or explore the OpenWeatherClient class definitions within the package.
+
+
+### Dependency Injection
+
+```csharp
+// Add the service to the service collection
+services.AddOpenWeatherClient();
+```
+
+```csharp
+// Inject the service
+public class MyClass
+{
+ private readonly IOpenWeatherClient _openWeatherClient;
+
+ public MyClass(IOpenWeatherClient openWeatherClient)
+ {
+ _openWeatherClient = openWeatherClient;
+ }
+}
+
+```
+
+### Manual instantiation
+
+```csharp
+// Create a new instance of the client
+IOpenWeatherClient _openWeatherClient = new OpenWeatherClient();
+```
+
+### Usage
+
+```csharp
+
+// Get the current weather for a New York
+
+var request = new OpenWeatherRequest
+{
+ ApiKey = "YOUR_API_KEY_HERE",
+ Latitude = 40.712776,
+ Longitude = -74.005974,
+ Language = Constants.Languages.English,
+ Unit = Constants.Units.Metric,
+ Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Hourly]
+};
+
+var response = await openWeatherClient.GetWeatherAsync(request);
+
+```
+
+
diff --git a/nuget.png b/nuget.png
new file mode 100644
index 0000000..7ed7e39
Binary files /dev/null and b/nuget.png differ
diff --git a/src/Indiko.OpenWeatherClient.sln b/src/Indiko.OpenWeatherClient.sln
new file mode 100644
index 0000000..29e0aa8
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient.sln
@@ -0,0 +1,44 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.9.34728.123
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Indiko.OpenWeatherClient", "Indiko.OpenWeatherClient\Indiko.OpenWeatherClient.csproj", "{C0DCC5AF-4C25-4BCA-AACE-095515D0C873}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{72619858-9C14-4668-8430-0A3BCFD33768}"
+ ProjectSection(SolutionItems) = preProject
+ ..\README.md = ..\README.md
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FAE6981C-18D1-42A7-8F35-F4B249BCEDAE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{BFB88B54-1F7C-4529-8C92-AD66B8DF2EF5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Indiko.OpenWeatherClient.Tests", "..\tests\Indiko.OpenWeatherClient.Tests\Indiko.OpenWeatherClient.Tests.csproj", "{0584C53B-D607-439C-81D8-6D3D712B9944}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C0DCC5AF-4C25-4BCA-AACE-095515D0C873}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0584C53B-D607-439C-81D8-6D3D712B9944}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0584C53B-D607-439C-81D8-6D3D712B9944}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0584C53B-D607-439C-81D8-6D3D712B9944}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0584C53B-D607-439C-81D8-6D3D712B9944}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {C0DCC5AF-4C25-4BCA-AACE-095515D0C873} = {FAE6981C-18D1-42A7-8F35-F4B249BCEDAE}
+ {0584C53B-D607-439C-81D8-6D3D712B9944} = {BFB88B54-1F7C-4529-8C92-AD66B8DF2EF5}
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B487E051-95D4-4F59-AE56-6F806E31652D}
+ EndGlobalSection
+EndGlobal
diff --git a/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs b/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs
new file mode 100644
index 0000000..a705c34
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Builder/OpenWeatherRequestBuilder.cs
@@ -0,0 +1,135 @@
+using Indiko.OpenWeatherClient.Constants;
+
+namespace Indiko.OpenWeatherClient.Builder;
+
+///
+/// Provides a builder pattern to create and configure an OpenWeather API request.
+///
+/// The API key to authenticate requests.
+public class OpenWeatherRequestBuilder(string apiKey)
+{
+ private readonly string _apiKey = apiKey;
+ private string _language = Languages.English;
+ private string _unit = Units.Metric;
+ private string[] _excludes = [];
+ private double _latitude;
+ private double _longitude;
+ private string _city;
+ private string _country;
+ private DateTime? _date;
+ private bool _useGeoCoding;
+
+ ///
+ /// Specifies the date for the request, typically used for historical data.
+ ///
+ /// The date of interest.
+ public OpenWeatherRequestBuilder WithDate(DateTime date)
+ {
+ _date = date;
+ return this;
+ }
+
+ ///
+ /// Specifies the language for the response data.
+ ///
+ /// The language code (ISO 639-1).
+ public OpenWeatherRequestBuilder WithLanguage(string language)
+ {
+ _language = language;
+ return this;
+ }
+
+ ///
+ /// Specifies the unit of measurement for temperature and other values.
+ ///
+ /// The unit system to use ('metric' or 'imperial').
+ public OpenWeatherRequestBuilder WithUnit(string unit)
+ {
+ _unit = unit;
+ return this;
+ }
+ ///
+ /// Specifies the geographical coordinates for the weather data request.
+ ///
+ /// The latitude coordinate.
+ /// The longitude coordinate.
+ public OpenWeatherRequestBuilder WithLocation(double latitude, double longitude)
+ {
+ _latitude = latitude;
+ _longitude = longitude;
+ _useGeoCoding = false;
+ return this;
+ }
+
+ ///
+ /// Specifies the city and country for the weather data request.
+ ///
+ /// The name of the city.
+ /// The country code (ISO 3166).
+ public OpenWeatherRequestBuilder WithCity(string city, string country)
+ {
+ _city = city;
+ _country = country;
+ _useGeoCoding = true;
+ return this;
+ }
+
+ ///
+ /// Specifies the features to exclude from the response data.
+ ///
+ /// An array of features to exclude.
+ public OpenWeatherRequestBuilder WithExcludes(string[] excludes)
+ {
+ _excludes = excludes;
+ return this;
+ }
+
+ ///
+ /// Builds the final URI for the OpenWeather API request based on the configured options.
+ /// Throws an exception if required parameters are not provided or incorrectly configured.
+ ///
+ /// The URI of the configured OpenWeather API request.
+ public Uri Build()
+ {
+ if (string.IsNullOrEmpty(_apiKey))
+ {
+ throw new ArgumentException("API Key must be provided");
+ }
+
+ string requestUrl = $"https://api.openweathermap.org/data/3.0/onecall?appid={_apiKey}&lang={_language}&units={_unit}";
+ if (_useGeoCoding)
+ {
+ if (_latitude != 0 && _longitude != 0)
+ {
+ throw new ArgumentException("Latitude and Longitude must be 0 when using city and country");
+ }
+ if (string.IsNullOrEmpty(_city) || string.IsNullOrEmpty(_country))
+ {
+ throw new ArgumentException("City and Country must be provided when using city and country");
+ }
+
+ requestUrl = $"{requestUrl}&q={_city},{_country}";
+ }
+ else
+ {
+ if (_latitude == 0 || _longitude == 0)
+ {
+ throw new ArgumentException("Latitude and Longitude must be provided when not using city and country");
+ }
+
+ requestUrl = $"{requestUrl}&lat={_latitude}&lon={_longitude}";
+ }
+
+ if (_date.HasValue)
+ {
+ requestUrl = $"{requestUrl}&date={_date.Value.ToShortDateString()}";
+ }
+
+ if (_excludes != null)
+ {
+ requestUrl = $"{requestUrl}&exclude={string.Join(",", _excludes)}";
+ }
+
+ return new Uri(requestUrl);
+ }
+}
diff --git a/src/Indiko.OpenWeatherClient/Constants/Excludes.cs b/src/Indiko.OpenWeatherClient/Constants/Excludes.cs
new file mode 100644
index 0000000..0e1430b
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Constants/Excludes.cs
@@ -0,0 +1,37 @@
+namespace Indiko.OpenWeatherClient.Constants;
+
+///
+/// Contains constant values that specify which data components to exclude from the responses in OpenWeather API requests.
+///
+public static class Excludes
+{
+ ///
+ /// Excludes the current weather data from the API response.
+ ///
+ public const string Current = "current";
+
+ ///
+ /// Excludes the minutely weather data (e.g., precipitation) from the API response.
+ ///
+ public const string Minutely = "minutely";
+
+ ///
+ /// Excludes the hourly forecast data from the API response.
+ ///
+ public const string Hourly = "hourly";
+
+ ///
+ /// Excludes the daily forecast data from the API response.
+ ///
+ public const string Daily = "daily";
+
+ ///
+ /// Excludes weather alerts from the API response.
+ ///
+ public const string Alerts = "alerts";
+
+ ///
+ /// Excludes all optional data components (current, minutely, hourly, daily, and alerts) from the API response.
+ ///
+ public const string All = "current,minutely,hourly,daily,alerts";
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Constants/Languages.cs b/src/Indiko.OpenWeatherClient/Constants/Languages.cs
new file mode 100644
index 0000000..ac41884
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Constants/Languages.cs
@@ -0,0 +1,54 @@
+namespace Indiko.OpenWeatherClient.Constants;
+
+///
+/// Contains constant values representing the language codes supported by the OpenWeather API for localizing responses.
+///
+public static class Languages
+{
+ public const string Afrikaans = "af";
+ public const string Albanian = "al";
+ public const string Arabic = "ar";
+ public const string Azerbaijani = "az";
+ public const string Bulgarian = "bg";
+ public const string Catalan = "ca";
+ public const string Czech = "cz";
+ public const string Danish = "da";
+ public const string German = "de";
+ public const string Greek = "el";
+ public const string English = "en";
+ public const string Basque = "eu";
+ public const string Persian = "fa";
+ public const string Finnish = "fi";
+ public const string French = "fr";
+ public const string Galician = "gl";
+ public const string Hebrew = "he";
+ public const string Hindi = "hi";
+ public const string Croatian = "hr";
+ public const string Hungarian = "hu";
+ public const string Indonesian = "id";
+ public const string Italian = "it";
+ public const string Japanese = "ja";
+ public const string Korean = "kr";
+ public const string Latvian = "la";
+ public const string Lithuanian = "lt";
+ public const string Macedonian = "mk";
+ public const string Norwegian = "no";
+ public const string Dutch = "nl";
+ public const string Polish = "pl";
+ public const string Portuguese = "pt";
+ public const string PortugueseBr = "pt_br";
+ public const string Romanian = "ro";
+ public const string Russian = "ru";
+ public const string Swedish = "se";
+ public const string Slovak = "sk";
+ public const string Slovenian = "sl";
+ public const string Spanish = "es";
+ public const string Serbian = "sr";
+ public const string Thai = "th";
+ public const string Turkish = "tr";
+ public const string Ukrainian = "ua";
+ public const string Vietnamese = "vi";
+ public const string ChineseSimplified = "zh_cn";
+ public const string ChineseTraditional = "zh_tw";
+ public const string Zulu = "zu";
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs b/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs
new file mode 100644
index 0000000..3bf32f7
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Constants/MapLayers.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Indiko.OpenWeatherClient.Constants;
+public static class MapLayers
+{
+ public static string Clouds = "clouds_new";
+ public static string Precipitation = "precipitation_new";
+ public static string SealevelPressure = "pressure_new";
+ public static string WindSpeed = "wind_new";
+ public static string Temperature = "temp_new";
+}
diff --git a/src/Indiko.OpenWeatherClient/Constants/Units.cs b/src/Indiko.OpenWeatherClient/Constants/Units.cs
new file mode 100644
index 0000000..f691664
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Constants/Units.cs
@@ -0,0 +1,22 @@
+namespace Indiko.OpenWeatherClient.Constants;
+
+///
+/// Contains constant values that specify the unit systems used for reporting temperature and other measurements in the OpenWeather API responses.
+///
+public static class Units
+{
+ ///
+ /// Uses the Standard unit system. Temperature in Kelvin, speed in meter/sec, and other measurements in standard metric units.
+ ///
+ public const string Standard = "standard";
+
+ ///
+ /// Uses the Metric unit system. Temperature in Celsius, speed in meter/sec, and other measurements in metric units.
+ ///
+ public const string Metric = "metric";
+
+ ///
+ /// Uses the Imperial unit system. Temperature in Fahrenheit, speed in miles/hour, and other measurements in imperial units.
+ ///
+ public const string Imperial = "imperial";
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs b/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs
new file mode 100644
index 0000000..79a6cc1
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Converter/UnixDateTimeConverter.cs
@@ -0,0 +1,47 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Converter;
+
+///
+/// A custom JSON converter that converts between Unix timestamps and nullable DateTime objects.
+/// This converter handles the conversion of DateTime values to and from Unix timestamps in JSON.
+///
+public class UnixDateTimeConverter : JsonConverter
+{
+ ///
+ /// Reads and converts the JSON token into a nullable DateTime value.
+ ///
+ /// The reader to read from.
+ /// The type of object to convert.
+ /// Options for the serializer.
+ /// A DateTime value if the JSON token is a number; otherwise, null.
+ public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+ {
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ long unixTime = reader.GetInt64();
+ return DateTimeOffset.FromUnixTimeSeconds(unixTime).UtcDateTime;
+ }
+ return null;
+ }
+
+ ///
+ /// Writes a DateTime value as a Unix timestamp to the JSON writer.
+ ///
+ /// The writer to write to.
+ /// The DateTime value to write.
+ /// Options for the serializer.
+ public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
+ {
+ if (value.HasValue)
+ {
+ long unixTime = ((DateTimeOffset)value.Value).ToUnixTimeSeconds();
+ writer.WriteNumberValue(unixTime);
+ }
+ else
+ {
+ writer.WriteNullValue();
+ }
+ }
+}
diff --git a/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs b/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs
new file mode 100644
index 0000000..55b0002
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Exceptions/OpenWeatherException.cs
@@ -0,0 +1,17 @@
+namespace Indiko.OpenWeatherClient.Exceptions;
+
+///
+/// Represents errors that occur during OpenWeather API operations.
+///
+public sealed class OpenWeatherException(int statusCode, string responseMessage) : Exception(responseMessage)
+{
+ ///
+ /// Gets the HTTP status code returned from the OpenWeather API when the error occurred.
+ ///
+ public int ResponseCode { get; } = statusCode;
+
+ ///
+ /// Gets the message describing the error, as returned by the OpenWeather API.
+ ///
+ public string ResponseMessage { get; } = responseMessage;
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs b/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs
new file mode 100644
index 0000000..8223754
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Extensions/IServiceCollectionExtensions.cs
@@ -0,0 +1,27 @@
+using Indiko.OpenWeatherClient.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Indiko.OpenWeatherClient.Extensions;
+
+///
+/// Provides extension methods for the to facilitate the registration of OpenWeather API related services.
+///
+public static class IServiceCollectionExtensions
+{
+ ///
+ /// Adds the OpenWeather client to the specified .
+ ///
+ /// The to add services to.
+ /// The so that additional calls can be chained.
+ ///
+ /// This method configures the dependency injection container to provide an
+ /// implementation whenever it is needed. It registers the as a scoped service,
+ /// which means a new instance is created once per client request.
+ ///
+ public static IServiceCollection AddOpenWeatherClient(this IServiceCollection services)
+ {
+ services.AddScoped();
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj b/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj
new file mode 100644
index 0000000..2a92c33
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Indiko.OpenWeatherClient.csproj
@@ -0,0 +1,39 @@
+
+
+
+ net8.0
+ disable
+ disable
+
+
+ INDIKO
+ Copyright © INDIKO and contributors
+ True
+ https://github.com/0xc3u/Indiko.OpenWeatherClient
+ https://github.com/0xc3u/Indiko.OpenWeatherClient
+ git
+ dotnet-maui;maui;ui;chips;control;
+ True
+ true
+ true
+ snupkg
+ INDIKO OpenWeather API Client
+ This package allows developers to easily integrate OpenWeatherMap's APIs into their .NET applications.
+ MIT
+ True
+ portable
+ icon.png
+ weather; api; openweathermap; .NET; maps; forecast
+ Initial release of the `Indiko.OpenWeatherClient` library.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs b/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs
new file mode 100644
index 0000000..beaf67e
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Interfaces/IOpenWeatherClient.cs
@@ -0,0 +1,17 @@
+using Indiko.OpenWeatherClient.Models;
+using System.Threading.Tasks;
+
+namespace Indiko.OpenWeatherClient.Interfaces;
+public interface IOpenWeatherClient
+{
+ Task GetCurrentWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default);
+
+ Task GetDailyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default);
+
+ Task GetHourlyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default);
+
+ Task GetWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default);
+
+ Uri GetMapTileUri(OpenWeatherMapTileRequest mapTileRequest);
+ Task GetMapTileAsync(OpenWeatherMapTileRequest mapTileRequest, CancellationToken cancellationToken = default);
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/Alert.cs b/src/Indiko.OpenWeatherClient/Models/Alert.cs
new file mode 100644
index 0000000..5eb4dcf
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Alert.cs
@@ -0,0 +1,60 @@
+using Indiko.OpenWeatherClient.Converter;
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+
+///
+/// Represents a weather alert issued by a weather data provider, detailing significant weather events.
+///
+public sealed class Alert
+{
+ ///
+ /// Gets the name of the organization that issued the alert.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("sender_name")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string SenderName { get; init; }
+
+ ///
+ /// Gets the name of the weather event that triggered the alert.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("event")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string Event { get; init; }
+
+ ///
+ /// Gets the start time of the weather event, represented as a nullable DateTime.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("start")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Start { get; init; }
+
+ ///
+ /// Gets the end time of the weather event, represented as a nullable DateTime.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("end")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? End { get; init; }
+
+ ///
+ /// Gets the detailed description of the weather event and its potential impact.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("description")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string Description { get; init; }
+
+ ///
+ /// Gets the tags associated with the alert, which categorize the nature of the event.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("tags")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string[] Tags { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/Current.cs b/src/Indiko.OpenWeatherClient/Models/Current.cs
new file mode 100644
index 0000000..6ff2e4b
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Current.cs
@@ -0,0 +1,53 @@
+using Indiko.OpenWeatherClient.Converter;
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+
+public sealed class Current : Weather
+{
+ ///
+ /// Gets the sunrise time for the current day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("sunrise")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Sunrise { get; init; }
+
+ ///
+ /// Gets the sunset time for the current day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("sunset")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Sunset { get; init; }
+
+ ///
+ /// Gets the current temperature in unit specified.
+ ///
+ [JsonPropertyName("temp")]
+ public float Temperature { get; init; }
+
+ ///
+ /// Gets the perceived temperature in unit specified, considering the impact of wind and humidity.
+ ///
+ [JsonPropertyName("feels_like")]
+ public float FeelsLike { get; init; }
+
+ ///
+ /// Gets the rainfall data for the last hour unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("rain")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public OneHour Rain { get; init; }
+
+ ///
+ /// Gets the snowfall data for the last hour unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("snow")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public OneHour Snow { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/Day.cs b/src/Indiko.OpenWeatherClient/Models/Day.cs
new file mode 100644
index 0000000..18f2e10
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Day.cs
@@ -0,0 +1,91 @@
+using Indiko.OpenWeatherClient.Converter;
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+public sealed class Day : Weather
+{
+ ///
+ /// Gets the sunrise time for the day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("sunrise")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Sunrise { get; init; }
+
+ ///
+ /// Gets the sunset time for the day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("sunset")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Sunset { get; init; }
+
+ ///
+ /// Gets the moonrise time for the day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("moonrise")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Moonrise { get; init; }
+
+ ///
+ /// Gets the moonset time for the day.
+ /// This property is represented as a Unix timestamp in the JSON data and will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("moonset")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? Moonset { get; init; }
+
+ ///
+ /// Gets the moon phase for the day.
+ /// The value ranges from 0.0 (new moon) to 1.0 (full moon).
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("moon_phase")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? MoonPhase { get; init; }
+
+ ///
+ /// Gets a summary of the day's weather conditions.
+ ///
+ [JsonPropertyName("summary")]
+ public string Summary { get; init; }
+
+ ///
+ /// Gets the detailed temperature information for the day,
+ /// including minimum, maximum, and average temperatures in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("temp")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Temperature Temperature { get; init; }
+
+ ///
+ /// Gets the perceived temperature details for the day,
+ /// considering factors such as wind and humidity in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("feels_like")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Temperature FeelsLike { get; init; }
+
+ ///
+ /// Gets the recorded rainfall in unit specified for the day.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("rain")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Rain { get; init; }
+
+ ///
+ /// Gets the recorded snowfall in unit specified for the day.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("snow")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Snow { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/Hour.cs b/src/Indiko.OpenWeatherClient/Models/Hour.cs
new file mode 100644
index 0000000..40324e4
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Hour.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+public sealed class Hour : Weather
+{
+ ///
+ /// Gets the temperature at the specific hour in unit specified.
+ ///
+ [JsonPropertyName("temp")]
+ public float Temperature { get; init; }
+
+ ///
+ /// Gets the perceived temperature at the specific hour in unit specified,
+ /// considering factors like wind and humidity.
+ ///
+ [JsonPropertyName("feels_like")]
+ public float FeelsLike { get; init; }
+
+ ///
+ /// Gets the rainfall data for the specific hour unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("rain")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public OneHour Rain { get; init; }
+
+ ///
+ /// Gets the snowfall data for the specific hour unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("snow")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public OneHour Snow { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/OneHour.cs b/src/Indiko.OpenWeatherClient/Models/OneHour.cs
new file mode 100644
index 0000000..d56dfa4
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/OneHour.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+
+///
+/// Represents precipitation data for a one-hour period.
+///
+public class OneHour
+{
+ ///
+ /// Gets the amount of precipitation in unit specified measured during the one-hour period.
+ /// This property is serialized into JSON using the key "1h".
+ ///
+ [JsonPropertyName("1h")]
+ public float Amount { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs
new file mode 100644
index 0000000..3b051ed
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherMapTileRequest.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Indiko.OpenWeatherClient.Models;
+public record OpenWeatherMapTileRequest
+{
+ public string ApiKey { get; init; }
+ public string Layer { get; init; }
+ public int ZoomLevel { get; init; }
+ public int X { get; init; }
+ public int Y { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs
new file mode 100644
index 0000000..366c108
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherRequest.cs
@@ -0,0 +1,53 @@
+namespace Indiko.OpenWeatherClient.Models;
+
+///
+/// Represents a request to the OpenWeather API, containing all necessary parameters to retrieve weather data.
+///
+public record OpenWeatherRequest
+{
+ ///
+ /// Gets the API key used for authentication with the OpenWeather API.
+ ///
+ public string ApiKey { get; init; }
+
+ ///
+ /// Gets the name of the city for which weather data is requested.
+ ///
+ public string City { get; init; }
+
+ ///
+ /// Gets the country code (ISO 3166) of the city for which weather data is requested.
+ ///
+ public string Country { get; init; }
+
+ ///
+ /// Gets the latitude coordinate for the location for which weather data is requested.
+ ///
+ public double Latitude { get; init; }
+
+ ///
+ /// Gets the longitude coordinate for the location for which weather data is requested.
+ ///
+ public double Longitude { get; init; }
+
+ ///
+ /// Gets the language code (ISO 639-1) for the response data from the OpenWeather API.
+ ///
+ public string Language { get; init; }
+
+ ///
+ /// Gets the unit of measurement for temperature and wind speed (e.g., 'metric', 'imperial').
+ ///
+ public string Unit { get; init; }
+
+ ///
+ /// Gets the array of weather data features to exclude from the API response (e.g., 'current', 'minutely', 'daily').
+ ///
+ public string[] Excludes { get; init; }
+
+ ///
+ /// Gets the specific date for which historical weather data is requested.
+ /// Optional; only needed for historical data requests.
+ ///
+ public DateTime? Date { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs b/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs
new file mode 100644
index 0000000..d9968ce
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/OpenWeatherResponse.cs
@@ -0,0 +1,57 @@
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+public sealed class OpenWeatherResponse
+{
+ ///
+ /// Gets or sets the latitude of the location for which weather data is provided.
+ ///
+ [JsonPropertyName("lat")]
+ public double Latitude { get; init; }
+
+ ///
+ /// Gets or sets the longitude of the location for which weather data is provided.
+ ///
+ [JsonPropertyName("lon")]
+ public double Longitude { get; init; }
+
+ ///
+ /// Gets or sets the IANA timezone name for the location.
+ ///
+ [JsonPropertyName("timezone")]
+ public string Timezone { get; init; }
+
+ ///
+ /// Gets or sets the offset in seconds from UTC for the location's timezone.
+ ///
+ [JsonPropertyName("timezone_offset")]
+ public int TimezoneOffset { get; init; }
+
+ ///
+ /// Gets or sets the current weather conditions data.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("current")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Current Current { get; init; }
+
+ ///
+ /// Gets or sets the array of hourly weather forecasts.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("hourly")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Hour[] Hourly { get; init; }
+
+ ///
+ /// Gets or sets the array of daily weather forecasts.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("daily")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Day[] Daily { get; init; }
+
+ [JsonPropertyName("alerts")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public Alert[] Alerts { get; init; }
+}
diff --git a/src/Indiko.OpenWeatherClient/Models/Temperature.cs b/src/Indiko.OpenWeatherClient/Models/Temperature.cs
new file mode 100644
index 0000000..2df59e8
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Temperature.cs
@@ -0,0 +1,53 @@
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+public class Temperature
+{
+ ///
+ /// Gets the average daytime temperature in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("day")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Day { get; init; }
+
+ ///
+ /// Gets the minimum recorded temperature in unit specified for the day.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("min")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Min { get; init; }
+
+ ///
+ /// Gets the maximum recorded temperature in unit specified for the day.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("max")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Max { get; init; }
+
+ ///
+ /// Gets the average nighttime temperature in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("night")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Night { get; init; }
+
+ ///
+ /// Gets the average evening temperature in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("eve")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Eve { get; init; }
+
+ ///
+ /// Gets the average morning temperature in unit specified.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("morn")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Morn { get; init; }
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Models/Weather.cs b/src/Indiko.OpenWeatherClient/Models/Weather.cs
new file mode 100644
index 0000000..66a6f74
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/Weather.cs
@@ -0,0 +1,76 @@
+using Indiko.OpenWeatherClient.Converter;
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+public class Weather
+{
+ ///
+ /// Gets the date and time of the weather data point.
+ /// This property is represented as a Unix timestamp in the JSON data.
+ ///
+ [JsonPropertyName("dt")]
+ [JsonConverter(typeof(UnixDateTimeConverter))]
+ public DateTime? DateTime { get; init; }
+
+ ///
+ /// Gets the atmospheric pressure in unit specified.
+ ///
+ [JsonPropertyName("pressure")]
+ public int Pressure { get; init; }
+
+ ///
+ /// Gets the humidity percentage.
+ ///
+ [JsonPropertyName("humidity")]
+ public int Humidity { get; init; }
+
+ ///
+ /// Gets the dew point temperature in unit specified.
+ ///
+ [JsonPropertyName("dew_point")]
+ public float DewPoint { get; init; }
+
+ ///
+ /// Gets the current ultraviolet index.
+ ///
+ [JsonPropertyName("uvi")]
+ public float UvIndex { get; init; }
+
+ ///
+ /// Gets the cloud coverage percentage.
+ ///
+ [JsonPropertyName("clouds")]
+ public int Clouds { get; init; }
+
+ ///
+ /// Gets the visibility in unit specified.
+ ///
+ [JsonPropertyName("visibility")]
+ public int Visibility { get; init; }
+
+ ///
+ /// Gets the wind speed in unit specified.
+ ///
+ [JsonPropertyName("wind_speed")]
+ public float WindSpeed { get; init; }
+
+ ///
+ /// Gets the wind direction in degrees (meteorological).
+ ///
+ [JsonPropertyName("wind_deg")]
+ public int WindDeg { get; init; }
+
+ ///
+ /// Gets the wind gust speed in unit specified.
+ ///
+ [JsonPropertyName("wind_gust")]
+ public float WindGust { get; init; }
+
+ ///
+ /// Gets an array of weather conditions information.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("weather")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public WeatherInformation[] WeatherInformation { get; init; }
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs b/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs
new file mode 100644
index 0000000..5a825d6
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Models/WeatherInformation.cs
@@ -0,0 +1,46 @@
+using System.Text.Json.Serialization;
+
+namespace Indiko.OpenWeatherClient.Models;
+
+///
+/// Represents detailed information about weather conditions.
+///
+public class WeatherInformation
+{
+ ///
+ /// Gets the weather condition ID, a unique identifier for a specific weather condition type.
+ ///
+ [JsonPropertyName("id")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.Never)]
+ public int Id { get; init; }
+
+ ///
+ /// Gets the main group to which the weather condition belongs, such as "Rain", "Snow", etc.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("main")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string Main { get; init; }
+
+ ///
+ /// Gets a more detailed description of the weather condition.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("description")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string Description { get; init; }
+
+ ///
+ /// Gets the icon code representing the weather condition visually.
+ /// This property will be ignored when writing to JSON if the value is null.
+ ///
+ [JsonPropertyName("icon")]
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string Icon { get; init; }
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
+ public string Icon100Url => !string.IsNullOrEmpty(Icon) ? $"https://openweathermap.org/img/wn/{Icon}@2x.png" : string.Empty;
+
+ [JsonIgnore(Condition = JsonIgnoreCondition.Always)]
+ public string Icon200Url => !string.IsNullOrEmpty(Icon) ? $"https://openweathermap.org/img/wn/{Icon}@4x.png" : string.Empty;
+}
diff --git a/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs b/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs
new file mode 100644
index 0000000..9b62415
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/OpenWeatherClient.cs
@@ -0,0 +1,189 @@
+using Indiko.OpenWeatherClient.Builder;
+using Indiko.OpenWeatherClient.Exceptions;
+using Indiko.OpenWeatherClient.Interfaces;
+using Indiko.OpenWeatherClient.Models;
+using System.Net.Http;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Indiko.OpenWeatherClient;
+
+///
+/// A client for interacting with the OpenWeather API to fetch weather data and map tile data.
+///
+public class OpenWeatherClient : IOpenWeatherClient, IDisposable
+{
+ private readonly JsonSerializerOptions _jsonSerializerOptions;
+ private HttpClient _httpClient;
+
+ public OpenWeatherClient()
+ {
+ _jsonSerializerOptions = new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true,
+ ReferenceHandler = ReferenceHandler.Preserve
+ };
+ _jsonSerializerOptions.Converters.Add(new Converter.UnixDateTimeConverter());
+
+ _httpClient = new HttpClient();
+ }
+
+ ///
+ /// Asynchronously retrieves the current weather data.
+ ///
+ /// Configuration for the API request.
+ /// Cancellation token for the async operation.
+ /// A object containing the current weather data.
+ public async Task GetCurrentWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default)
+ {
+ var openWeatherResponse = await GetResponseInternal(request, cancellationToken);
+
+ return openWeatherResponse.Current;
+ }
+
+ ///
+ /// Asynchronously retrieves the daily weather forecast.
+ ///
+ /// Configuration for the API request.
+ /// Cancellation token for the async operation.
+ /// An array of objects containing daily weather data.
+ public async Task GetDailyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default)
+ {
+ var openWeatherResponse = await GetResponseInternal(request, cancellationToken);
+ return openWeatherResponse.Daily;
+ }
+
+ ///
+ /// Asynchronously retrieves the hourly weather forecast.
+ ///
+ /// Configuration for the API request.
+ /// Cancellation token for the async operation.
+ /// An array of objects containing hourly weather data.
+ public async Task GetHourlyWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default)
+ {
+ var openWeatherResponse = await GetResponseInternal(request, cancellationToken);
+ return openWeatherResponse.Hourly;
+ }
+
+ ///
+ /// Asynchronously retrieves the complete weather data based on the specified request configuration.
+ ///
+ /// Configuration for the API request.
+ /// Cancellation token for the async operation.
+ /// An object containing comprehensive weather data.
+ public async Task GetWeatherAsync(OpenWeatherRequest request, CancellationToken cancellationToken = default)
+ {
+ var openWeatherResponse = await GetResponseInternal(request, cancellationToken);
+ return openWeatherResponse;
+ }
+
+ private async Task GetResponseInternal(OpenWeatherRequest request, CancellationToken cancellationToken = default)
+ {
+ OpenWeatherRequestBuilder requestBuilder = new(request.ApiKey);
+
+ if (!string.IsNullOrWhiteSpace(request.City) && !string.IsNullOrWhiteSpace(request.Country))
+ {
+ requestBuilder.WithCity(request.City, request.Country);
+ }
+ else if (request.Latitude != 0 && request.Longitude != 0)
+ {
+ requestBuilder.WithLocation(request.Latitude, request.Longitude);
+ }
+
+ if (!string.IsNullOrWhiteSpace(request.Language))
+ {
+ requestBuilder.WithLanguage(request.Language);
+ }
+
+ if (!string.IsNullOrWhiteSpace(request.Unit))
+ {
+ requestBuilder.WithUnit(request.Unit);
+ }
+
+ if (request.Date.HasValue)
+ {
+ requestBuilder.WithDate(request.Date.Value);
+ }
+
+ if (request.Excludes?.Length > 0)
+ {
+ requestBuilder.WithExcludes(request.Excludes);
+ }
+
+ var requestUrl = requestBuilder.Build();
+ var requestMessage = new HttpRequestMessage(HttpMethod.Get, requestUrl);
+
+ var response = await _httpClient.SendAsync(requestMessage, cancellationToken)
+ .ConfigureAwait(false);
+
+ switch (response.StatusCode)
+ {
+ case System.Net.HttpStatusCode.OK:
+ {
+ var content = await response.Content.ReadAsStringAsync(cancellationToken)
+ .ConfigureAwait(false);
+
+ return JsonSerializer.Deserialize(content, _jsonSerializerOptions);
+ }
+ default:
+ {
+ var content = await response.Content.ReadAsStringAsync(cancellationToken)
+ .ConfigureAwait(false);
+ throw new OpenWeatherException((int)response.StatusCode, content);
+ }
+ }
+ }
+
+ ///
+ /// Constructs a URI for a specific map tile from the OpenWeatherMap tile service.
+ ///
+ /// Configuration for the map tile request.
+ /// A URI pointing to the map tile image.
+ public Uri GetMapTileUri(OpenWeatherMapTileRequest mapTileRequest)
+ {
+ if (string.IsNullOrEmpty(mapTileRequest.ApiKey))
+ {
+ throw new ArgumentNullException(nameof(mapTileRequest.ApiKey));
+ }
+
+ if (string.IsNullOrEmpty(mapTileRequest.Layer))
+ {
+ throw new ArgumentNullException(nameof(mapTileRequest.Layer));
+ }
+
+ string url = $"https://tile.openweathermap.org/map/{mapTileRequest.Layer}/{mapTileRequest.ZoomLevel}/{mapTileRequest.X}/{mapTileRequest.Y}.png?appid={mapTileRequest.ApiKey}";
+ return new Uri(url);
+ }
+
+ ///
+ /// Asynchronously retrieves a map tile image from the OpenWeatherMap tile service.
+ ///
+ /// Configuration for the map tile request.
+ /// Cancellation token for the async operation.
+ /// A byte array containing the map tile image.
+ public async Task GetMapTileAsync(OpenWeatherMapTileRequest mapTileRequest, CancellationToken cancellationToken = default)
+ {
+ var url = GetMapTileUri(mapTileRequest);
+ return await GetInternalMapTileAsync(url, cancellationToken);
+ }
+
+ public async Task GetInternalMapTileAsync(Uri url, CancellationToken cancellationToken = default)
+ {
+ var response = await _httpClient.GetByteArrayAsync(url, cancellationToken)
+ .ConfigureAwait(false);
+
+ return response;
+ }
+
+ public void Dispose()
+ {
+ if (_httpClient != null)
+ {
+ _httpClient.CancelPendingRequests();
+ _httpClient.Dispose();
+ _httpClient = null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Indiko.OpenWeatherClient/Usings.cs b/src/Indiko.OpenWeatherClient/Usings.cs
new file mode 100644
index 0000000..fb5a3e3
--- /dev/null
+++ b/src/Indiko.OpenWeatherClient/Usings.cs
@@ -0,0 +1,2 @@
+global using System;
+global using System.Threading;
diff --git a/src/Indiko.OpenWeatherClient/icon.png b/src/Indiko.OpenWeatherClient/icon.png
new file mode 100644
index 0000000..7ed7e39
Binary files /dev/null and b/src/Indiko.OpenWeatherClient/icon.png differ
diff --git a/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj b/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj
new file mode 100644
index 0000000..05cfa3d
--- /dev/null
+++ b/tests/Indiko.OpenWeatherClient.Tests/Indiko.OpenWeatherClient.Tests.csproj
@@ -0,0 +1,34 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+ false
+ true
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs b/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs
new file mode 100644
index 0000000..ef224c6
--- /dev/null
+++ b/tests/Indiko.OpenWeatherClient.Tests/IntegrationTestFixture.cs
@@ -0,0 +1,21 @@
+namespace Indiko.OpenWeatherClient.Tests;
+public class IntegrationTestFixture : IDisposable
+{
+
+ public string ApiKey { get; }
+
+ public OpenWeatherClient OpenWeatherClient { get; }
+
+ public IntegrationTestFixture()
+ {
+ // Initialize the test environment
+ OpenWeatherClient = new OpenWeatherClient();
+ ApiKey = "912478bc99153f9f23fbbfbb05572322";
+ }
+
+ public void Dispose()
+ {
+ // Clean up the test environment
+ OpenWeatherClient.Dispose();
+ }
+}
diff --git a/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs b/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs
new file mode 100644
index 0000000..339dc66
--- /dev/null
+++ b/tests/Indiko.OpenWeatherClient.Tests/OpenWeatherClientIntegrationTests.cs
@@ -0,0 +1,93 @@
+using Indiko.OpenWeatherClient.Models;
+using FluentAssertions;
+
+namespace Indiko.OpenWeatherClient.Tests;
+
+public class OpenWeatherClientIntegrationTests(IntegrationTestFixture fixture) : IClassFixture
+{
+ private readonly IntegrationTestFixture _fixture = fixture;
+
+ [Fact]
+ public async Task GetCurrentWeatherAsync_ReturnsCorrectWeatherData()
+ {
+ // Arrange
+ var request = new OpenWeatherRequest
+ {
+ ApiKey = _fixture.ApiKey,
+ Latitude = 40.712776,
+ Longitude = -74.005974,
+ Language = Constants.Languages.English,
+ Unit = Constants.Units.Metric,
+ Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Hourly]
+ };
+
+ // Act
+ var result = await _fixture.OpenWeatherClient.GetCurrentWeatherAsync(request);
+
+ // Assert
+ result.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task GetDailyWeatherAsync_ReturnsCorrectWeatherData()
+ {
+ // Arrange
+ var request = new OpenWeatherRequest
+ {
+ ApiKey = _fixture.ApiKey,
+ Latitude = 40.712776,
+ Longitude = -74.005974,
+ Language = Constants.Languages.English,
+ Unit = Constants.Units.Metric,
+ Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Hourly, Constants.Excludes.Current]
+ };
+
+ // Act
+ var result = await _fixture.OpenWeatherClient.GetDailyWeatherAsync(request);
+
+ // Assert
+ result.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task GetHourlyWeatherAsync_ReturnsCorrectWeatherData()
+ {
+ // Arrange
+ var request = new OpenWeatherRequest
+ {
+ ApiKey = _fixture.ApiKey,
+ Latitude = 40.712776,
+ Longitude = -74.005974,
+ Language = Constants.Languages.English,
+ Unit = Constants.Units.Metric,
+ Excludes = [Constants.Excludes.Minutely, Constants.Excludes.Daily, Constants.Excludes.Current]
+ };
+
+ // Act
+ var result = await _fixture.OpenWeatherClient.GetHourlyWeatherAsync(request);
+
+ // Assert
+ result.Should().NotBeNull();
+ }
+
+ [Fact]
+ public async Task GetWeatherAsync_ReturnsCorrectWeatherData()
+ {
+ // Arrange
+ var request = new OpenWeatherRequest
+ {
+ ApiKey = _fixture.ApiKey,
+ Latitude = 40.712776,
+ Longitude = -74.005974,
+ Language = Constants.Languages.English,
+ Unit = Constants.Units.Metric,
+ Excludes = [Constants.Excludes.Minutely]
+ };
+
+ // Act
+ var result = await _fixture.OpenWeatherClient.GetWeatherAsync(request);
+
+ // Assert
+ result.Should().NotBeNull();
+ }
+}
\ No newline at end of file