diff --git a/.gitignore b/.gitignore
index 41ceba8..9904d9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,9 +16,6 @@ assets/
# App Settings
appsettings.json
-# Experimental
-Triggers/
-
# Private Keys
*.pfx
diff --git a/src/Converters/CoordinatesArrayJsonConverter.cs b/src/Converters/CoordinatesArrayJsonConverter.cs
new file mode 100644
index 0000000..5ade645
--- /dev/null
+++ b/src/Converters/CoordinatesArrayJsonConverter.cs
@@ -0,0 +1,25 @@
+namespace OpenWeatherMap.Converters
+{
+ using System;
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Triggers;
+
+ internal sealed class CoordinatesArrayJsonConverter : JsonConverter
+ {
+ ///
+ public override bool CanConvert(Type objectType) => true;
+
+ ///
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var coordinates = serializer.Deserialize(reader);
+ return new Coordinates(coordinates);
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ serializer.Serialize(writer, ((Coordinates)value).AsArray());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Converters/DefaultJsonConverter.cs b/src/Converters/DefaultJsonConverter.cs
new file mode 100644
index 0000000..42c4284
--- /dev/null
+++ b/src/Converters/DefaultJsonConverter.cs
@@ -0,0 +1,29 @@
+namespace OpenWeatherMap.Converters
+{
+ using System;
+ using Newtonsoft.Json;
+
+ internal sealed class DefaultJsonConverter : JsonConverter
+ {
+ ///
+ public override bool CanRead => false;
+
+ ///
+ public override bool CanWrite => false;
+
+ ///
+ public override bool CanConvert(Type objectType) => true;
+
+ ///
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Converters/GeoAreaJsonConverter.cs b/src/Converters/GeoAreaJsonConverter.cs
new file mode 100644
index 0000000..bdfce77
--- /dev/null
+++ b/src/Converters/GeoAreaJsonConverter.cs
@@ -0,0 +1,51 @@
+namespace OpenWeatherMap.Converters
+{
+ using System;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Linq;
+ using OpenWeatherMap.Triggers.Area;
+
+ internal sealed class GeoAreaJsonConverter : JsonConverter
+ {
+ ///
+ public override bool CanWrite => false;
+
+ ///
+ public override bool CanConvert(Type objectType) => true;
+
+ ///
+ public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
+ {
+ var data = JObject.Load(reader);
+ var type = data["type"].ToObject(serializer);
+ var area = FromType(type);
+
+ using (var tokenReader = data.CreateReader())
+ {
+ serializer.Populate(tokenReader, area);
+ }
+
+ return area;
+ }
+
+ ///
+ public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
+ {
+ throw new NotSupportedException();
+ }
+
+ private static IGeoArea FromType(GeoAreaType type)
+ {
+ switch (type)
+ {
+ case GeoAreaType.Point: return new Point();
+ case GeoAreaType.MultiPoint: return new MultiPoint();
+ case GeoAreaType.Polygon: return new Polygon();
+ case GeoAreaType.MultiPolygon: return new MultiPolygon();
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(type), type,
+ "The specified type is not defined in the GeoAreaType enumeration.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Entities/WeatherForecast.cs b/src/Entities/WeatherForecast.cs
index 2ca017c..dc4e3a9 100644
--- a/src/Entities/WeatherForecast.cs
+++ b/src/Entities/WeatherForecast.cs
@@ -2,7 +2,6 @@
{
using System.Collections.Generic;
using Newtonsoft.Json;
- using OpenWeatherMap.Entities;
///
/// Represents the response object of a weather forecast request.
diff --git a/src/OpenWeatherMap4NET.csproj b/src/OpenWeatherMap4NET.csproj
index 5383869..5fd494e 100644
--- a/src/OpenWeatherMap4NET.csproj
+++ b/src/OpenWeatherMap4NET.csproj
@@ -8,7 +8,7 @@
A wrapper for the OpenWeatherMap.org RESTful api service.
Angelo Breuer
- 1.0.4
+ 1.0.5
MIT
https://github.com/angelobreuer/OpenWeatherMap4NET
openweathermap, weather, wrapper, OpenWeatherMap4NET
@@ -26,17 +26,6 @@
OpenWeatherMap4NET.xml
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/OpenWeatherMap4NET.xml b/src/OpenWeatherMap4NET.xml
index 5d14e4a..5f9c43f 100644
--- a/src/OpenWeatherMap4NET.xml
+++ b/src/OpenWeatherMap4NET.xml
@@ -4,6 +4,42 @@
OpenWeatherMap4NET
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The strongly-typed representation of a JSON-object providing additional weather
@@ -227,50 +263,81 @@
The different weather condition types.
+ https://openweathermap.org/weather-conditions
-
+
- Thunderstorm weather condition
+ Denotes that the weather condition type is "Ash".
+
+
+
+
+ Denotes that the weather condition type is "Clear".
+
+
+
+
+ Denotes that the weather condition type is "Clouds".
- Drizzle weather condition
+ Denotes that the weather condition type is "Drizzle".
+
+
+
+
+ Denotes that the weather condition type is "Dust".
+
+
+
+
+ Denotes that the weather condition type is "Fog".
+
+
+
+
+ Denotes that the weather condition type is "Haze".
+
+
+
+
+ Denotes that the weather condition type is "Mist".
- Rain weather condition
+ Denotes that the weather condition type is "Rain".
-
+
- Snow weather condition
+ Denotes that the weather condition type is "Sand".
-
+
- Atmosphere weather condition
+ Denotes that the weather condition type is "Smoke".
-
+
- Clear weather condition
+ Denotes that the weather condition type is "Snow".
-
+
- Clouds weather condition
+ Denotes that the weather condition type is "Squall".
-
+
- Haze weather condition
+ Denotes that the weather condition type is "Thunderstorm".
-
+
- Mist weather condition
+ Denotes that the weather condition type is "Tornado".
@@ -660,6 +727,27 @@
a task that represents the asynchronous operation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A set of supported caching modes.
@@ -706,6 +794,60 @@
Daily weather forecast for 16 days.
+
+
+ Represents a HTTP content which holds JSON-encoded data.
+
+
+
+
+ The media type for JSON-encoded HTTP content ("application/json").
+
+
+
+
+ Initializes a new instance of the class.
+
+ the JSON object to write
+
+ the encoding to use when writing the JSON data; or to use
+
+
+
+ thrown if the specified is .
+
+
+
+
+ Initializes a new instance of the class.
+
+ the JSON token to write
+
+ the encoding to use when writing the JSON data; or to use
+
+
+
+ thrown if the specified is .
+
+
+
+
+ Gets the encoding to use when writing the JSON data.
+
+ the encoding to use when writing the JSON data
+
+
+
+ Gets the JSON token to write.
+
+ the JSON token to write
+
+
+
+
+
+
+
Request options for .
diff --git a/src/OpenWeatherMapOptions.cs b/src/OpenWeatherMapOptions.cs
index bf82995..06ed658 100644
--- a/src/OpenWeatherMapOptions.cs
+++ b/src/OpenWeatherMapOptions.cs
@@ -17,6 +17,8 @@ public class OpenWeatherMapOptions
///
public Uri BaseAddress { get; set; } = new Uri("https://api.openweathermap.org/data/");
+ public TimeSpan PollInterval { get; set; } = TimeSpan.FromSeconds(40);
+
///
/// Validates the open weather map options.
///
diff --git a/src/OpenWeatherMapService.cs b/src/OpenWeatherMapService.cs
index 9972a8a..4316614 100644
--- a/src/OpenWeatherMapService.cs
+++ b/src/OpenWeatherMapService.cs
@@ -14,6 +14,10 @@
using System.Text;
using System.Globalization;
using System.Net;
+ using OpenWeatherMap.Triggers;
+ using Newtonsoft.Json.Linq;
+ using System.IO;
+ using System.Threading;
///
/// The service for getting weather information.
@@ -26,6 +30,8 @@ private static readonly DateTimeOffset _uvHistoryStartTime
private readonly MemoryCache _cache;
private readonly HttpClient _httpClient;
private readonly OpenWeatherMapOptions _options;
+ private readonly ISet _triggerSubscriptions;
+ private Timer _pollTimer;
///
/// Initializes a new instance of the class.
@@ -38,6 +44,31 @@ public OpenWeatherMapService(OpenWeatherMapOptions options)
_options = options ?? throw new ArgumentNullException(nameof(options));
_options.Validate();
+
+ _triggerSubscriptions = new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ public event EventHandler AlertReceived;
+
+ public async Task CreateTriggerAsync(Trigger trigger, RequestOptions requestOptions = null)
+ {
+ requestOptions = requestOptions ?? RequestOptions.Default;
+ requestOptions.CancellationToken.ThrowIfCancellationRequested();
+
+ using (var content = new JsonContent(trigger))
+ {
+ return await RequestAsync(
+ "triggers", requestOptions: requestOptions, doCache: false, method: HttpMethod.Post,
+ httpContent: content);
+ }
+ }
+
+ public Task DeleteTriggerAsync(string id, RequestOptions requestOptions = null)
+ {
+ requestOptions = requestOptions ?? RequestOptions.Default;
+ requestOptions.CancellationToken.ThrowIfCancellationRequested();
+
+ return RequestAsync($"triggers/{id}", requestOptions: requestOptions, method: HttpMethod.Delete);
}
///
@@ -47,6 +78,9 @@ public virtual void Dispose()
{
_httpClient.Dispose();
_cache.Dispose();
+
+ _pollTimer?.Dispose();
+ _pollTimer = null;
}
///
@@ -228,6 +262,14 @@ public Task GetCurrentWeatherAsync(int zipCode, string country = "us",
return RequestAsync("weather", parameters, requestOptions);
}
+ public Task GetTriggerAsync(string id, RequestOptions requestOptions = null)
+ {
+ requestOptions = requestOptions ?? RequestOptions.Default;
+ requestOptions.CancellationToken.ThrowIfCancellationRequested();
+
+ return RequestAsync($"triggers/{id}", requestOptions: requestOptions);
+ }
+
///
/// Gets the forecast UV index for the specified coordinates asynchronously.
///
@@ -396,71 +438,53 @@ public Task GetWeatherForecastAsync(int zipCode, string country
return RequestAsync(GetEndpoint(forecastType), parameters, requestOptions);
}
- private string BuildCityName(string city, string countryCode = null)
- => countryCode == null ? city : city + "-" + countryCode;
-
- private string BuildRequestCacheKey(string uri, NameValueCollection parameters)
+ public async Task PollAsync(CancellationToken cancellationToken = default)
{
- var query = string.Join(";", parameters.Keys
- .Cast().Select(s => s + ":" + parameters[s]));
+ cancellationToken.ThrowIfCancellationRequested();
- return $"req-{uri}-{{{query}}}";
- }
-
- private Uri BuildRequestUri(string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default, string version = "2.5")
- {
- // create a new query string collection
- var parameters = HttpUtility.ParseQueryString(string.Empty);
+ var requestOptions = new RequestOptions { CancellationToken = cancellationToken };
- // add query parameters if specified
- if (queryParameters != null)
+ foreach (var subscription in _triggerSubscriptions)
{
- parameters.Add(queryParameters);
- }
+ var trigger = await GetTriggerAsync(subscription, requestOptions);
- // add the api key to the query parameter list
- parameters.Add("APPID", _options.ApiKey);
-
- // add the specific unit if it is not the default (Kelvin)
- if (requestOptions.Unit != UnitType.Default)
- {
- parameters.Add("units", requestOptions.Unit == UnitType.Imperial
- ? "imperial" : "metric");
+ if (trigger.Alerts.Count > 0)
+ {
+ var eventArgs = new AlertEventArgs(trigger, trigger.Alerts);
+ AlertReceived?.Invoke(this, eventArgs);
+ }
}
+ }
- // add the specific language when specified
- if (!string.IsNullOrWhiteSpace(requestOptions.Language))
+ public void Subscribe(string triggerId)
+ {
+ _triggerSubscriptions.Add(triggerId);
+
+ if (_pollTimer is null)
{
- parameters.Add("lang", requestOptions.Language);
+ _pollTimer = new Timer(PollCallback, null, _options.PollInterval, _options.PollInterval);
}
-
- // build request uri
- var query = parameters.ToString();
- var requestUri = new Uri(_options.BaseAddress, version + "/" + uri);
- var builder = new UriBuilder(requestUri) { Query = query };
- return builder.Uri;
}
- private string GetEndpoint(ForecastType forecastType)
+ public bool Unsubscribe(string triggerId)
{
- switch (forecastType)
+ if (_triggerSubscriptions.Remove(triggerId))
{
- case ForecastType.ThreeHour:
- return "forecast";
-
- case ForecastType.Hourly:
- return "forecast/hourly";
-
- case ForecastType.Daily:
- return "forecast/daily";
+ if (_triggerSubscriptions.Count == 0)
+ {
+ _pollTimer?.Dispose();
+ _pollTimer = null;
+ }
- default:
- throw new ArgumentException("Unknown forecast type.", nameof(forecastType));
+ return true;
}
+
+ return false;
}
- private async Task RequestAsync(string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default,
- string version = "2.5", bool doCache = true, HttpMethod method = null, HttpContent httpContent = null)
+ protected async Task RequestAsync(
+ string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default,
+ bool doCache = true, HttpMethod method = null, HttpContent httpContent = null)
{
method = method ?? HttpMethod.Get;
requestOptions = requestOptions ?? RequestOptions.Default;
@@ -488,47 +512,72 @@ private async Task RequestAsync(string uri, NameValueCollectio
throw new InvalidOperationException("The request was not retrieved from cache.");
}
- // send request
- var requestUri = BuildRequestUri(uri, queryParameters, requestOptions, version);
- var result = await SendAsync(method, requestUri, httpContent);
-
- // resource not found
- if (result == null)
+ using (var response = await SendRequestAsync(uri, queryParameters, requestOptions, method, httpContent))
{
- return result;
+ // resource not found
+ if (response.StatusCode == HttpStatusCode.NotFound)
+ {
+ return default;
+ }
+
+ using (var responseStream = await response.Content.ReadAsStreamAsync())
+ using (var streamReader = new StreamReader(responseStream))
+ using (var jsonTextReader = new JsonTextReader(streamReader))
+ {
+ var data = await JToken.LoadAsync(jsonTextReader, requestOptions.CancellationToken);
+ var result = data.ToObject();
+
+ // add the result to the cache for 10 minutes
+ if (doCache)
+ {
+ _cache.Add(cacheKey, result, DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10));
+ }
+
+ return result;
+ }
}
+ }
+
+ protected async Task RequestAsync(
+ string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default,
+ HttpMethod method = null, HttpContent httpContent = null)
+ {
+ method = method ?? HttpMethod.Get;
+ requestOptions = requestOptions ?? RequestOptions.Default;
+ requestOptions.CancellationToken.ThrowIfCancellationRequested();
- // add the result to the cache for 10 minutes
- if (doCache)
+ using (var response = await SendRequestAsync(uri, queryParameters, requestOptions, method, httpContent))
{
- _cache.Add(cacheKey, result, DateTimeOffset.UtcNow + TimeSpan.FromMinutes(10));
+ response.EnsureSuccessStatusCode();
}
-
- return result;
}
- private async Task SendAsync(HttpMethod httpMethod, Uri uri, HttpContent httpContent = null)
+ protected async Task SendRequestAsync(
+ string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default,
+ HttpMethod method = null, HttpContent httpContent = null)
{
- // send request to the api
- using (var request = new HttpRequestMessage(httpMethod, uri) { Content = httpContent })
- using (var response = await _httpClient.SendAsync(request))
- {
- // read the result as a string
- var content = await response.Content.ReadAsStringAsync();
+ method = method ?? HttpMethod.Get;
+ requestOptions = requestOptions ?? RequestOptions.Default;
+ requestOptions.CancellationToken.ThrowIfCancellationRequested();
- // the resource was not found
- if (response.StatusCode == HttpStatusCode.NotFound)
- {
- return default;
- }
+ // send request
+ var requestUri = BuildRequestUri(uri, queryParameters, requestOptions);
+
+ // send request to the API
+ using (var request = new HttpRequestMessage(method, requestUri) { Content = httpContent })
+ {
+ var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, requestOptions.CancellationToken);
// something went wrong
if (!response.IsSuccessStatusCode)
{
+ // read the result as a string
+ var content = await response.Content.ReadAsStringAsync();
+
var messageBuilder = new StringBuilder();
messageBuilder.Append("OpenWeatherMap Request Error:");
messageBuilder.AppendFormat("\n- Got status {0} ({1}), expected: 2xx.", response.StatusCode, (int)response.StatusCode);
- messageBuilder.AppendFormat("\n- Request Status Line: {0} {1}.", httpMethod.Method, uri);
+ messageBuilder.AppendFormat("\n- Request Status Line: {0} {1}.", method.Method, uri);
messageBuilder.Append("\n- Response Headers:");
foreach (var header in response.Headers)
@@ -540,8 +589,81 @@ private async Task SendAsync(HttpMethod httpMethod, Uri uri, H
throw new InvalidOperationException(messageBuilder.ToString());
}
- // deserialize the response
- return JsonConvert.DeserializeObject(content);
+ return response;
+ }
+ }
+
+ private string BuildCityName(string city, string countryCode = null)
+ => countryCode == null ? city : city + "-" + countryCode;
+
+ private string BuildRequestCacheKey(string uri, NameValueCollection parameters)
+ {
+ var query = string.Join(";", parameters.Keys
+ .Cast().Select(s => s + ":" + parameters[s]));
+
+ return $"req-{uri}-{{{query}}}";
+ }
+
+ private Uri BuildRequestUri(string uri, NameValueCollection queryParameters = null, RequestOptions requestOptions = default, string version = "3.0")
+ {
+ // create a new query string collection
+ var parameters = HttpUtility.ParseQueryString(string.Empty);
+
+ // add query parameters if specified
+ if (queryParameters != null)
+ {
+ parameters.Add(queryParameters);
+ }
+
+ // add the api key to the query parameter list
+ parameters.Add("APPID", _options.ApiKey);
+
+ // add the specific unit if it is not the default (Kelvin)
+ if (requestOptions.Unit != UnitType.Default)
+ {
+ parameters.Add("units", requestOptions.Unit == UnitType.Imperial ? "imperial" : "metric");
+ }
+
+ // add the specific language when specified
+ if (!string.IsNullOrWhiteSpace(requestOptions.Language))
+ {
+ parameters.Add("lang", requestOptions.Language);
+ }
+
+ // build request uri
+ var query = parameters.ToString();
+ var requestUri = new Uri(_options.BaseAddress, version + "/" + uri);
+ var builder = new UriBuilder(requestUri) { Query = query };
+ return builder.Uri;
+ }
+
+ private string GetEndpoint(ForecastType forecastType)
+ {
+ switch (forecastType)
+ {
+ case ForecastType.ThreeHour:
+ return "forecast";
+
+ case ForecastType.Hourly:
+ return "forecast/hourly";
+
+ case ForecastType.Daily:
+ return "forecast/daily";
+
+ default:
+ throw new ArgumentException("Unknown forecast type.", nameof(forecastType));
+ }
+ }
+
+ private void PollCallback(object state)
+ {
+ try
+ {
+ PollAsync().GetAwaiter().GetResult();
+ }
+ catch (Exception)
+ {
+ // ignore exceptions, we do not throw exceptions on timers..
}
}
}
diff --git a/src/OpenWeatherMapTriggerOptions.cs b/src/OpenWeatherMapTriggerOptions.cs
deleted file mode 100644
index 141a699..0000000
--- a/src/OpenWeatherMapTriggerOptions.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace OpenWeatherMap
-{
- using System;
-
- public class OpenWeatherMapTriggerOptions : OpenWeatherMapOptions
- {
- public TimeSpan PollInterval { get; internal set; } = TimeSpan.FromMinutes(2);
- }
-}
\ No newline at end of file
diff --git a/src/OpenWeatherMapTriggerService.cs b/src/OpenWeatherMapTriggerService.cs
deleted file mode 100644
index 32a4ed7..0000000
--- a/src/OpenWeatherMapTriggerService.cs
+++ /dev/null
@@ -1,134 +0,0 @@
-namespace OpenWeatherMap
-{
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Text;
- using System.Threading;
- using System.Threading.Tasks;
- using OpenWeatherMap.Triggers;
- using OpenWeatherMap.Util;
-
- public class OpenWeatherMapTriggerService : OpenWeatherMapService, IDisposable
- {
- private readonly OpenWeatherMapTriggerOptions _options;
- private readonly CancellationTokenSource _cancellationTokenSource;
-
- public event Func AlertReceived;
-
- public override void Dispose()
- {
- base.Dispose();
- _cancellationTokenSource.Cancel();
- }
-
- protected virtual Task OnAlertReceived(AlertTrigger trigger, Alert alert)
- => AlertReceived == null ? Task.CompletedTask : Task.WhenAll(AlertReceived.GetInvocationList()
- .Select(s => s.DynamicInvoke(trigger, alert)).Cast());
-
- public OpenWeatherMapTriggerService(OpenWeatherMapTriggerOptions options) : base(options)
- {
- _options = options ?? throw new ArgumentNullException(nameof(options));
- _cancellationTokenSource = new CancellationTokenSource();
- _ = GetUpdateTask();
- }
-
- public async Task ForcePullAlertsAsync(RequestOptions requestOptions = default)
- {
- Console.WriteLine("polling...");
- var triggers = await GetAllTriggersAsync(requestOptions);
- var tasks = triggers.SelectMany(s => s.Alerts.Select(j => OnAlertReceived(s, j.Value))).ToArray();
- await Task.WhenAll(tasks);
- Console.WriteLine("polling finished: " + tasks.Length);
- return tasks.Length;
- }
-
- public Task> GetAllTriggersAsync(RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync>("triggers", doCache: false, requestOptions: requestOptions, version: "3.0");
- }
-
- public async Task UpdateTriggerAsync(string id, Trigger trigger, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- using (var content = new JsonContent(JsonConvert.SerializeObject(trigger)))
- {
- return await RequestAsync($"trigger/{id}", null, requestOptions, "3.0", false, HttpMethod.Put, content);
- }
- }
-
- public Task GetHistoryAlertAsync(string triggerId, string alertId, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync($"triggers/{triggerId}/history/{alertId}", null, requestOptions, version: "3.0", doCache: false);
- }
-
- public Task GetTriggerAsync(string id, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync($"triggers/{id}", null, requestOptions, version: "3.0", doCache: false);
- }
-
- public Task> GetTriggerHistoryAsync(string id, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync>($"triggers/{id}/history", null, requestOptions, "3.0", false);
- }
-
- public async Task CreateTriggerAsync(Trigger trigger, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- using (var content = new JsonContent(JsonConvert.SerializeObject(trigger)))
- {
- return await RequestAsync("triggers", null, requestOptions, "3.0", false, HttpMethod.Post, content);
- }
- }
-
- public Task DeleteHistoryAsync(string triggerId, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync($"triggers/{triggerId}/history", null, requestOptions, version: "3.0", doCache: false, HttpMethod.Delete);
- }
-
- public Task DeleteTriggerAsync(string id, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync($"triggers/{id}", null, requestOptions,
- version: "3.0", doCache: false, method: HttpMethod.Delete);
- }
-
- public Task DeleteTriggerHistorysync(string triggerId, string alertId, RequestOptions requestOptions = default)
- {
- requestOptions = requestOptions ?? RequestOptions.Default;
- requestOptions.CancellationToken.ThrowIfCancellationRequested();
-
- return RequestAsync($"triggers/{triggerId}/history/{alertId}", null, requestOptions, version: "3.0", doCache: false, HttpMethod.Delete);
- }
-
- private async Task GetUpdateTask()
- {
- while (!_cancellationTokenSource.IsCancellationRequested)
- {
- var pullTask = ForcePullAlertsAsync();
- await Task.WhenAll(pullTask, Task.Delay(_options.PollInterval));
- }
- }
- }
-}
\ No newline at end of file
diff --git a/src/Triggers/Alert.cs b/src/Triggers/Alert.cs
new file mode 100644
index 0000000..92e972f
--- /dev/null
+++ b/src/Triggers/Alert.cs
@@ -0,0 +1,23 @@
+namespace OpenWeatherMap.Triggers
+{
+ using System;
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Converters;
+
+ public sealed class Alert
+ {
+ [JsonRequired, JsonProperty("conditions")]
+ public IReadOnlyCollection Conditions { get; internal set; }
+
+ [JsonRequired, JsonProperty("coordinates")]
+ [JsonConverter(typeof(DefaultJsonConverter))]
+ public Coordinates Coordinates { get; internal set; }
+
+ [JsonRequired, JsonProperty("data")]
+ public DateTimeOffset Date { get; internal set; }
+
+ [JsonRequired, JsonProperty("last_update")]
+ public DateTimeOffset LastUpdate { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/AlertEventArgs.cs b/src/Triggers/AlertEventArgs.cs
new file mode 100644
index 0000000..97fcf81
--- /dev/null
+++ b/src/Triggers/AlertEventArgs.cs
@@ -0,0 +1,17 @@
+namespace OpenWeatherMap.Triggers
+{
+ using System;
+ using System.Collections.Generic;
+
+ public sealed class AlertEventArgs : EventArgs
+ {
+ public AlertEventArgs(Trigger trigger, IReadOnlyDictionary alerts)
+ {
+ Trigger = trigger ?? throw new ArgumentNullException(nameof(trigger));
+ Alerts = alerts ?? throw new ArgumentNullException(nameof(alerts));
+ }
+
+ public IReadOnlyDictionary Alerts { get; }
+ public Trigger Trigger { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/GeoAreaType.cs b/src/Triggers/Area/GeoAreaType.cs
new file mode 100644
index 0000000..f7a3332
--- /dev/null
+++ b/src/Triggers/Area/GeoAreaType.cs
@@ -0,0 +1,14 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum GeoAreaType : byte
+ {
+ Point,
+ MultiPoint,
+ Polygon,
+ MultiPolygon
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/IGeoArea.cs b/src/Triggers/Area/IGeoArea.cs
new file mode 100644
index 0000000..396e440
--- /dev/null
+++ b/src/Triggers/Area/IGeoArea.cs
@@ -0,0 +1,11 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Converters;
+
+ [JsonConverter(typeof(GeoAreaJsonConverter))]
+ public interface IGeoArea
+ {
+ GeoAreaType Type { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/MultiPoint.cs b/src/Triggers/Area/MultiPoint.cs
new file mode 100644
index 0000000..2c5331b
--- /dev/null
+++ b/src/Triggers/Area/MultiPoint.cs
@@ -0,0 +1,33 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using System;
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Converters;
+
+ public sealed class MultiPoint : IGeoArea
+ {
+ public MultiPoint(IReadOnlyCollection coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ public MultiPoint(params Coordinates[] coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ [JsonConstructor]
+ internal MultiPoint()
+ {
+ }
+
+ ///
+ [JsonRequired, JsonProperty("coordinates")]
+ public IReadOnlyCollection Coordinates { get; internal set; }
+
+ ///
+ [JsonRequired, JsonProperty("type")]
+ public GeoAreaType Type => GeoAreaType.MultiPoint;
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/MultiPolygon.cs b/src/Triggers/Area/MultiPolygon.cs
new file mode 100644
index 0000000..87b9e08
--- /dev/null
+++ b/src/Triggers/Area/MultiPolygon.cs
@@ -0,0 +1,32 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using System;
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+
+ public sealed class MultiPolygon : IGeoArea
+ {
+ public MultiPolygon(IReadOnlyCollection>> coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ public MultiPolygon(params Coordinates[][][] coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ [JsonConstructor]
+ internal MultiPolygon()
+ {
+ }
+
+ ///
+ [JsonRequired, JsonProperty("coordinates")]
+ public IReadOnlyCollection>> Coordinates { get; internal set; }
+
+ ///
+ [JsonRequired, JsonProperty("type")]
+ public GeoAreaType Type => GeoAreaType.Polygon;
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/Point.cs b/src/Triggers/Area/Point.cs
new file mode 100644
index 0000000..8d60730
--- /dev/null
+++ b/src/Triggers/Area/Point.cs
@@ -0,0 +1,24 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using Newtonsoft.Json;
+
+ public sealed class Point : IGeoArea
+ {
+ public Point(Coordinates coordinates)
+ {
+ Coordinates = coordinates;
+ }
+
+ [JsonConstructor]
+ internal Point()
+ {
+ }
+
+ [JsonRequired, JsonProperty("coordinates")]
+ public Coordinates Coordinates { get; internal set; }
+
+ ///
+ [JsonRequired, JsonProperty("type")]
+ public GeoAreaType Type => GeoAreaType.Point;
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Area/Polygon.cs b/src/Triggers/Area/Polygon.cs
new file mode 100644
index 0000000..f567f5f
--- /dev/null
+++ b/src/Triggers/Area/Polygon.cs
@@ -0,0 +1,32 @@
+namespace OpenWeatherMap.Triggers.Area
+{
+ using System;
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+
+ public sealed class Polygon : IGeoArea
+ {
+ public Polygon(IReadOnlyCollection> coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ public Polygon(params Coordinates[][] coordinates)
+ {
+ Coordinates = coordinates ?? throw new ArgumentNullException(nameof(coordinates));
+ }
+
+ [JsonConstructor]
+ internal Polygon()
+ {
+ }
+
+ ///
+ [JsonRequired, JsonProperty("coordinates")]
+ public IReadOnlyCollection> Coordinates { get; internal set; }
+
+ ///
+ [JsonRequired, JsonProperty("type")]
+ public GeoAreaType Type => GeoAreaType.Polygon;
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Builders/AlertTriggerBuilder.cs b/src/Triggers/Builders/AlertTriggerBuilder.cs
new file mode 100644
index 0000000..8fb40f0
--- /dev/null
+++ b/src/Triggers/Builders/AlertTriggerBuilder.cs
@@ -0,0 +1,60 @@
+namespace OpenWeatherMap.Triggers.Builders
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq.Expressions;
+ using OpenWeatherMap.Triggers.Area;
+ using OpenWeatherMap.Triggers.Conditions;
+ using OpenWeatherMap.Triggers.Time;
+
+ public sealed class AlertTriggerBuilder
+ {
+ private readonly List _areas;
+ private readonly List _conditions;
+
+ private TimePeriod _period;
+
+ public AlertTriggerBuilder()
+ {
+ _areas = new List();
+ _conditions = new List();
+ }
+
+ public Trigger Build()
+ {
+ return new Trigger
+ {
+ Areas = _areas.ToArray(),
+ Conditions = _conditions.ToArray(),
+ Period = _period
+ };
+ }
+
+ public AlertTriggerBuilder WithArea(IGeoArea area)
+ {
+ _areas.Add(area);
+ return this;
+ }
+
+ public AlertTriggerBuilder WithCondition(TriggerCondition condition)
+ {
+ _conditions.Add(condition);
+ return this;
+ }
+
+ public AlertTriggerBuilder WithCondition(Expression> expression)
+ {
+ _conditions.AddRange(TriggerCondition.FromExpression(expression));
+ return this;
+ }
+
+ public AlertTriggerBuilder WithPeriod(TimePeriod period)
+ {
+ _period = period ?? throw new ArgumentNullException(nameof(period));
+ return this;
+ }
+
+ public AlertTriggerBuilder WithPeriod(DynamicTimestamp start, DynamicTimestamp end)
+ => WithPeriod(new TimePeriod { Start = start, End = end });
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Conditions/TriggerCondition.cs b/src/Triggers/Conditions/TriggerCondition.cs
new file mode 100644
index 0000000..795c385
--- /dev/null
+++ b/src/Triggers/Conditions/TriggerCondition.cs
@@ -0,0 +1,29 @@
+namespace OpenWeatherMap.Triggers.Conditions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq.Expressions;
+ using Newtonsoft.Json;
+
+ public sealed class TriggerCondition
+ {
+ public TriggerCondition(WeatherTriggerName name, object value, WeatherExpressionType expression = WeatherExpressionType.Equal)
+ {
+ Name = name;
+ Expression = expression;
+ Value = value;
+ }
+
+ [JsonRequired, JsonProperty("expression")]
+ public WeatherExpressionType Expression { get; }
+
+ [JsonRequired, JsonProperty("name")]
+ public WeatherTriggerName Name { get; }
+
+ [JsonRequired, JsonProperty("amount")]
+ public object Value { get; }
+
+ public static IEnumerable FromExpression(Expression> expression)
+ => TriggerConditionExpressionConverter.FromExpression(expression);
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Conditions/TriggerConditionExpressionConverter.cs b/src/Triggers/Conditions/TriggerConditionExpressionConverter.cs
new file mode 100644
index 0000000..cbae312
--- /dev/null
+++ b/src/Triggers/Conditions/TriggerConditionExpressionConverter.cs
@@ -0,0 +1,130 @@
+namespace OpenWeatherMap.Triggers.Conditions
+{
+ using System;
+ using System.Collections.Generic;
+ using System.Linq;
+ using System.Linq.Expressions;
+ using System.Reflection;
+
+ internal static class TriggerConditionExpressionConverter
+ {
+ public static IEnumerable FromExpression(Expression> expression)
+ {
+ if (expression.CanReduce)
+ {
+ expression = (Expression>)expression.ReduceAndCheck();
+ }
+
+ if (!(expression is LambdaExpression lambdaExpression))
+ {
+ throw new ArgumentException("Expected lambda expression: " + expression, nameof(expression));
+ }
+
+ if (!(lambdaExpression.Body is BinaryExpression binaryExpression))
+ {
+ throw new ArgumentException("Expected binary expression: " + lambdaExpression.Body, nameof(expression));
+ }
+
+ return VisitAndAlsoExpressionChain(binaryExpression).Select(CreateFromExpression);
+ }
+
+ private static TriggerCondition CreateFromExpression(BinaryExpression expression)
+ {
+ var expressionType = ResolveExpressionType(expression);
+
+ var leftConstantExpression = expression.Left as ConstantExpression;
+ var rightConstantExpression = expression.Right as ConstantExpression;
+
+ if ((leftConstantExpression is null) == (rightConstantExpression is null))
+ {
+ var failedExpression = leftConstantExpression is null ? expression.Right : expression.Left;
+ throw new InvalidOperationException("Could not convert expression: " + failedExpression);
+ }
+
+ var constantValue = leftConstantExpression is null ? rightConstantExpression.Value : leftConstantExpression.Value;
+ var propertyExpression = (leftConstantExpression is null ? expression.Left : expression.Right) as MemberExpression;
+
+ if (propertyExpression is null)
+ {
+ var failedExpression = leftConstantExpression is null ? expression.Left : expression.Right;
+ throw new InvalidOperationException("Could not convert expression: " + failedExpression);
+ }
+
+ if (propertyExpression.Member.DeclaringType != typeof(WeatherTriggerCondition))
+ {
+ throw new InvalidOperationException("Could not convert expression: " + propertyExpression);
+ }
+
+ if (((PropertyInfo)propertyExpression.Member).PropertyType != constantValue.GetType())
+ {
+ throw new InvalidOperationException("Could not convert expression: " + expression);
+ }
+
+ var triggerName = ResolveTriggerName(propertyExpression.Member.Name);
+ return new TriggerCondition(triggerName, constantValue, expressionType);
+ }
+
+ private static WeatherExpressionType ResolveExpressionType(BinaryExpression expression)
+ {
+ switch (expression.NodeType)
+ {
+ case ExpressionType.Equal: return WeatherExpressionType.Equal;
+ case ExpressionType.NotEqual: return WeatherExpressionType.NotEqual;
+ case ExpressionType.GreaterThan: return WeatherExpressionType.GreaterThan;
+ case ExpressionType.LessThan: return WeatherExpressionType.LessThan;
+ case ExpressionType.GreaterThanOrEqual: return WeatherExpressionType.GreaterThan;
+ case ExpressionType.LessThanOrEqual: return WeatherExpressionType.GreaterThanOrEqual;
+ }
+
+ throw new ArgumentOutOfRangeException(
+ nameof(expression.NodeType), expression.NodeType,
+ "Unsupported logical expression type: " + expression);
+ }
+
+ private static WeatherTriggerName ResolveTriggerName(string propertyName)
+ {
+ switch (propertyName)
+ {
+ case nameof(WeatherTriggerCondition.Clouds): return WeatherTriggerName.Clouds;
+ case nameof(WeatherTriggerCondition.Humidity): return WeatherTriggerName.Humidity;
+ case nameof(WeatherTriggerCondition.Pressure): return WeatherTriggerName.Pressure;
+ case nameof(WeatherTriggerCondition.Temperature): return WeatherTriggerName.Temperature;
+ case nameof(WeatherTriggerCondition.WindDirection): return WeatherTriggerName.WindDirection;
+ case nameof(WeatherTriggerCondition.WindSpeed): return WeatherTriggerName.WindSpeed;
+ }
+
+ throw new ArgumentException("Failed to resolve weather trigger type.", nameof(propertyName));
+ }
+
+ private static IEnumerable VisitAndAlsoExpressionChain(BinaryExpression expression)
+ {
+ var enumerable = Enumerable.Empty();
+
+ if (expression.NodeType == ExpressionType.AndAlso)
+ {
+ if (!(expression.Left is BinaryExpression leftBinaryExpression))
+ {
+ throw new InvalidOperationException("Could not convert expression: " + expression.Left);
+ }
+
+ // append left children
+ enumerable = enumerable.Concat(VisitAndAlsoExpressionChain(leftBinaryExpression));
+
+ if (!(expression.Right is BinaryExpression rightBinaryExpression))
+ {
+ throw new InvalidOperationException("Could not convert expression: " + expression.Right);
+ }
+
+ // append right children
+ enumerable = enumerable.Concat(VisitAndAlsoExpressionChain(rightBinaryExpression));
+ }
+ else
+ {
+ // append self
+ enumerable = enumerable.Append(expression);
+ }
+
+ return enumerable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Conditions/WeatherExpressionType.cs b/src/Triggers/Conditions/WeatherExpressionType.cs
new file mode 100644
index 0000000..a26e581
--- /dev/null
+++ b/src/Triggers/Conditions/WeatherExpressionType.cs
@@ -0,0 +1,28 @@
+namespace OpenWeatherMap.Triggers.Conditions
+{
+ using System.Runtime.Serialization;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum WeatherExpressionType : byte
+ {
+ [EnumMember(Value = "$gt")]
+ GreaterThan,
+
+ [EnumMember(Value = "$lt")]
+ LessThan,
+
+ [EnumMember(Value = "$gte")]
+ GreaterThanOrEqual,
+
+ [EnumMember(Value = "$lte")]
+ LessThanOrEqual,
+
+ [EnumMember(Value = "$eq")]
+ Equal,
+
+ [EnumMember(Value = "$ne")]
+ NotEqual
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Conditions/WeatherTriggerCondition.cs b/src/Triggers/Conditions/WeatherTriggerCondition.cs
new file mode 100644
index 0000000..63c31be
--- /dev/null
+++ b/src/Triggers/Conditions/WeatherTriggerCondition.cs
@@ -0,0 +1,12 @@
+namespace OpenWeatherMap.Triggers.Conditions
+{
+ public sealed class WeatherTriggerCondition
+ {
+ public float Clouds { get; }
+ public float Humidity { get; }
+ public float Pressure { get; }
+ public float Temperature { get; }
+ public float WindDirection { get; }
+ public float WindSpeed { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Conditions/WeatherTriggerType.cs b/src/Triggers/Conditions/WeatherTriggerType.cs
new file mode 100644
index 0000000..af61059
--- /dev/null
+++ b/src/Triggers/Conditions/WeatherTriggerType.cs
@@ -0,0 +1,28 @@
+namespace OpenWeatherMap.Triggers.Conditions
+{
+ using System.Runtime.Serialization;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum WeatherTriggerName : byte
+ {
+ [EnumMember(Value = "temp")]
+ Temperature,
+
+ [EnumMember(Value = "pressure")]
+ Pressure,
+
+ [EnumMember(Value = "humidity")]
+ Humidity,
+
+ [EnumMember(Value = "wind_speed")]
+ WindSpeed,
+
+ [EnumMember(Value = "wind_direction")]
+ WindDirection,
+
+ [EnumMember(Value = "clouds")]
+ Clouds
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Coordinates.cs b/src/Triggers/Coordinates.cs
new file mode 100644
index 0000000..bbc946b
--- /dev/null
+++ b/src/Triggers/Coordinates.cs
@@ -0,0 +1,39 @@
+namespace OpenWeatherMap.Triggers
+{
+ using System;
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Converters;
+
+ [JsonConverter(typeof(CoordinatesArrayJsonConverter))]
+ public readonly struct Coordinates
+ {
+ public Coordinates(int[] array)
+ {
+ if (array is null)
+ {
+ throw new ArgumentNullException(nameof(array));
+ }
+
+ if (array.Length != 2)
+ {
+ throw new InvalidOperationException("Expected exactly 2 coordinates in array.");
+ }
+
+ Longitude = array[0];
+ Latitude = array[1];
+ }
+
+ [JsonConstructor]
+ public Coordinates([JsonProperty("lon")] int longitude, [JsonProperty("lat")] int latitude)
+ {
+ Longitude = longitude;
+ Latitude = latitude;
+ }
+
+ public int Latitude { get; }
+
+ public int Longitude { get; }
+
+ public int[] AsArray() => new[] { Longitude, Latitude };
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Time/DynamicTimestamp.cs b/src/Triggers/Time/DynamicTimestamp.cs
new file mode 100644
index 0000000..a3d4a29
--- /dev/null
+++ b/src/Triggers/Time/DynamicTimestamp.cs
@@ -0,0 +1,38 @@
+namespace OpenWeatherMap.Triggers.Time
+{
+ using System;
+ using Newtonsoft.Json;
+
+ public sealed class DynamicTimestamp
+ {
+ public DynamicTimestamp(long offset, TimestampExpression expression)
+ {
+ Offset = offset;
+ Expression = expression;
+ }
+
+ [JsonRequired, JsonProperty("expression")]
+ public TimestampExpression Expression { get; internal set; }
+
+ [JsonRequired, JsonProperty("amount")]
+ public long Offset { get; internal set; }
+
+ public DateTimeOffset ToTimestamp()
+ {
+ switch (Expression)
+ {
+ case TimestampExpression.Exact:
+ return DateTimeOffset.FromUnixTimeMilliseconds(Offset);
+
+ case TimestampExpression.After:
+ return DateTimeOffset.UtcNow + TimeSpan.FromMilliseconds(Offset);
+
+ case TimestampExpression.Before:
+ return DateTimeOffset.UtcNow - TimeSpan.FromMilliseconds(Offset);
+ }
+
+ throw new ArgumentOutOfRangeException(nameof(Expression), Expression,
+ "The specified Expression is not defined in the TimestampExpression enumeration.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Time/TimePeriod.cs b/src/Triggers/Time/TimePeriod.cs
new file mode 100644
index 0000000..994cd32
--- /dev/null
+++ b/src/Triggers/Time/TimePeriod.cs
@@ -0,0 +1,13 @@
+namespace OpenWeatherMap.Triggers.Time
+{
+ using Newtonsoft.Json;
+
+ public sealed class TimePeriod
+ {
+ [JsonRequired, JsonProperty("end")]
+ public DynamicTimestamp End { get; internal set; }
+
+ [JsonRequired, JsonProperty("start")]
+ public DynamicTimestamp Start { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Time/TimestampExpression.cs b/src/Triggers/Time/TimestampExpression.cs
new file mode 100644
index 0000000..327e9ab
--- /dev/null
+++ b/src/Triggers/Time/TimestampExpression.cs
@@ -0,0 +1,19 @@
+namespace OpenWeatherMap.Triggers.Time
+{
+ using System.Runtime.Serialization;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Converters;
+
+ [JsonConverter(typeof(StringEnumConverter))]
+ public enum TimestampExpression : byte
+ {
+ [EnumMember(Value = "exact")]
+ Exact,
+
+ [EnumMember(Value = "after")]
+ After,
+
+ [EnumMember(Value = "before")]
+ Before
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/Trigger.cs b/src/Triggers/Trigger.cs
new file mode 100644
index 0000000..482cee3
--- /dev/null
+++ b/src/Triggers/Trigger.cs
@@ -0,0 +1,27 @@
+namespace OpenWeatherMap.Triggers
+{
+ using System;
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Triggers.Area;
+ using OpenWeatherMap.Triggers.Conditions;
+ using OpenWeatherMap.Triggers.Time;
+
+ public class Trigger
+ {
+ [JsonProperty("area")]
+ public IReadOnlyCollection Areas { get; internal set; }
+
+ [JsonProperty("conditions")]
+ public IReadOnlyCollection Conditions { get; internal set; }
+
+ [JsonProperty("_id", NullValueHandling = NullValueHandling.Ignore)]
+ public string Id { get; internal set; }
+
+ [JsonProperty("owner", NullValueHandling = NullValueHandling.Ignore)]
+ public string OwnerId { get; internal set; }
+
+ [JsonRequired, JsonProperty("time_period")]
+ public TimePeriod Period { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/TriggerAlert.cs b/src/Triggers/TriggerAlert.cs
new file mode 100644
index 0000000..3f0f70a
--- /dev/null
+++ b/src/Triggers/TriggerAlert.cs
@@ -0,0 +1,14 @@
+namespace OpenWeatherMap.Triggers
+{
+ using Newtonsoft.Json;
+ using OpenWeatherMap.Triggers.Conditions;
+
+ public sealed class TriggerAlert
+ {
+ [JsonRequired, JsonProperty("condition")]
+ public TriggerCondition Condition { get; internal set; }
+
+ [JsonRequired, JsonProperty("current_value")]
+ public TriggerValue Value { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/TriggerInformation.cs b/src/Triggers/TriggerInformation.cs
new file mode 100644
index 0000000..55054b7
--- /dev/null
+++ b/src/Triggers/TriggerInformation.cs
@@ -0,0 +1,11 @@
+namespace OpenWeatherMap.Triggers
+{
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+
+ public sealed class TriggerInformation : Trigger
+ {
+ [JsonRequired, JsonProperty("alerts")]
+ public IReadOnlyDictionary Alerts { get; internal set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Triggers/TriggerValue.cs b/src/Triggers/TriggerValue.cs
new file mode 100644
index 0000000..0e3de09
--- /dev/null
+++ b/src/Triggers/TriggerValue.cs
@@ -0,0 +1,17 @@
+namespace OpenWeatherMap.Triggers
+{
+ using Newtonsoft.Json;
+
+ public readonly struct TriggerValue
+ {
+ [JsonConstructor]
+ public TriggerValue([JsonProperty("min")] float min, [JsonProperty("max")] float max)
+ {
+ Min = min;
+ Max = max;
+ }
+
+ public float Min { get; }
+ public float Max { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Util/JsonContent.cs b/src/Util/JsonContent.cs
index 9b79127..775d560 100644
--- a/src/Util/JsonContent.cs
+++ b/src/Util/JsonContent.cs
@@ -1,18 +1,85 @@
namespace OpenWeatherMap.Util
{
+ using System;
+ using System.IO;
+ using System.Net;
using System.Net.Http;
+ using System.Net.Http.Headers;
using System.Text;
+ using System.Threading.Tasks;
+ using Newtonsoft.Json;
+ using Newtonsoft.Json.Linq;
- public sealed class JsonContent : StringContent
+ ///
+ /// Represents a HTTP content which holds JSON-encoded data.
+ ///
+ public sealed class JsonContent : HttpContent
{
+ ///
+ /// The media type for JSON-encoded HTTP content ("application/json").
+ ///
public const string MediaType = "application/json";
- public JsonContent(string content) : this(content, Encoding.UTF8)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// the JSON object to write
+ ///
+ /// the encoding to use when writing the JSON data; or to use
+ ///
+ ///
+ ///
+ /// thrown if the specified is .
+ ///
+ public JsonContent(object value, Encoding encoding = null) : this(JToken.FromObject(value), encoding)
{
}
- public JsonContent(string content, Encoding encoding) : base(content, encoding, MediaType)
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// the JSON token to write
+ ///
+ /// the encoding to use when writing the JSON data; or to use
+ ///
+ ///
+ ///
+ /// thrown if the specified is .
+ ///
+ public JsonContent(JToken token, Encoding encoding = null)
{
+ Token = token ?? throw new ArgumentNullException(nameof(token));
+ Encoding = encoding ?? Encoding.UTF8;
+ Headers.ContentType = new MediaTypeHeaderValue(MediaType);
+ }
+
+ ///
+ /// Gets the encoding to use when writing the JSON data.
+ ///
+ /// the encoding to use when writing the JSON data
+ public Encoding Encoding { get; }
+
+ ///
+ /// Gets the JSON token to write.
+ ///
+ /// the JSON token to write
+ public JToken Token { get; }
+
+ ///
+ protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
+ {
+ using (var streamWriter = new StreamWriter(stream, Encoding, 4096, leaveOpen: true))
+ using (var jsonTextWriter = new JsonTextWriter(streamWriter))
+ {
+ await Token.WriteToAsync(jsonTextWriter);
+ }
+ }
+
+ ///
+ protected override bool TryComputeLength(out long length)
+ {
+ length = default;
+ return false;
}
}
}
\ No newline at end of file