Skip to content

Commit

Permalink
Merge pull request #16 from qJake/http-fix
Browse files Browse the repository at this point in the history
Fix HTTP bug, add Logbook client
  • Loading branch information
qJake authored Jan 23, 2021
2 parents 28ac5cc + 9669d82 commit 5da45cd
Show file tree
Hide file tree
Showing 8 changed files with 349 additions and 68 deletions.
6 changes: 3 additions & 3 deletions HADotNet.Core.Tests/HADotNet.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="nunit" Version="3.10.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="nunit" Version="3.13.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
</ItemGroup>

<ItemGroup>
Expand Down
97 changes: 97 additions & 0 deletions HADotNet.Core.Tests/LogbookTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using HADotNet.Core;
using HADotNet.Core.Clients;
using NUnit.Framework;
using System;
using System.Threading.Tasks;

namespace Tests
{
public class LogbookTests
{
private Uri Instance { get; set; }
private string ApiKey { get; set; }

[SetUp]
public void Setup()
{
Instance = new Uri(Environment.GetEnvironmentVariable("HADotNet:Tests:Instance"));
ApiKey = Environment.GetEnvironmentVariable("HADotNet:Tests:ApiKey");

ClientFactory.Initialize(Instance, ApiKey);
}

[Test]
public async Task ShouldRetrieveAllLogbookEntries()
{
var client = ClientFactory.GetClient<LogbookClient>();

var logEntries = await client.GetLogbookEntries();

Assert.IsNotNull(logEntries);
Assert.IsNotEmpty(logEntries[0].EntityId);
Assert.AreNotEqual(0, logEntries.Count);
Assert.AreNotEqual(string.Empty, logEntries[0].EntityId);
}

[Test]
public async Task ShouldRetrieveLogbookEntriesByEntityId()
{
var client = ClientFactory.GetClient<LogbookClient>();

var history = await client.GetLogbookEntries("light.jakes_office");

Assert.IsNotNull(history);
Assert.IsNotEmpty(history.EntityId);
}

[Test]
public async Task ShouldRetrieveLogbookEntriesByStartDate()
{
var client = ClientFactory.GetClient<LogbookClient>();

var logEntries = await client.GetLogbookEntries(DateTimeOffset.Now.Subtract(TimeSpan.FromDays(2)));

Assert.IsNotNull(logEntries);
Assert.IsNotEmpty(logEntries[0].EntityId);
Assert.AreNotEqual(0, logEntries.Count);
Assert.AreNotEqual(string.Empty, logEntries[0].EntityId);
}

[Test]
public async Task ShouldRetrieveLogbookEntriesByStartAndEndDate()
{
var client = ClientFactory.GetClient<LogbookClient>();

var logEntries = await client.GetLogbookEntries(DateTimeOffset.Now.Subtract(TimeSpan.FromDays(2)), DateTimeOffset.Now.Subtract(new TimeSpan(1, 12, 0, 0)));

Assert.IsNotNull(logEntries);
Assert.IsNotEmpty(logEntries[0].EntityId);
Assert.AreNotEqual(0, logEntries.Count);
Assert.AreNotEqual(string.Empty, logEntries[0].EntityId);
}

[Test]
public async Task ShouldRetrieveLogbookEntriesByStartDateAndDuration()
{
var client = ClientFactory.GetClient<LogbookClient>();

var logEntries = await client.GetLogbookEntries(DateTimeOffset.Now.Subtract(TimeSpan.FromDays(2)), TimeSpan.FromHours(18));

Assert.IsNotNull(logEntries);
Assert.IsNotEmpty(logEntries[0].EntityId);
Assert.AreNotEqual(0, logEntries.Count);
Assert.AreNotEqual(string.Empty, logEntries[0].EntityId);
}

[Test]
public async Task ShouldRetrieveLogbookEntriesByStartAndEndDateAndEntityId()
{
var client = ClientFactory.GetClient<LogbookClient>();

var history = await client.GetLogbookEntries("group.family_room_lights", DateTimeOffset.Now.Subtract(TimeSpan.FromDays(2)), DateTimeOffset.Now.Subtract(new TimeSpan(1, 12, 0, 0)));

Assert.IsNotNull(history);
Assert.IsNotEmpty(history.EntityId);
}
}
}
19 changes: 19 additions & 0 deletions HADotNet.Core/BaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public abstract class BaseClient
protected BaseClient(Uri instance, string apiKey)
{
Client = new RestClient(instance);
Client.AutomaticDecompression = false;
Client.AddDefaultHeader("Authorization", $"Bearer {apiKey}");
}

Expand All @@ -36,6 +37,11 @@ protected BaseClient(Uri instance, string apiKey)
protected async Task<T> Get<T>(string path) where T : class
{
var req = new RestRequest(path);

// Bug in HA or RestSharp if Gzip is enabled, so disable it for now
req.AddDecompressionMethod(DecompressionMethods.None);
req.AddHeader("Accept-Encoding", "identity");

var resp = await Client.ExecuteGetAsync(req);

if (!string.IsNullOrWhiteSpace(resp.Content) && (resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.Created))
Expand Down Expand Up @@ -63,6 +69,11 @@ protected async Task<T> Get<T>(string path) where T : class
protected async Task<T> Post<T>(string path, object body, bool isRawBody = false) where T : class
{
var req = new RestRequest(path);

// Bug in HA or RestSharp if Gzip is enabled, so disable it for now
req.AddDecompressionMethod(DecompressionMethods.None);
req.AddHeader("Accept-Encoding", "identity");

if (body != null)
{
if (isRawBody)
Expand Down Expand Up @@ -100,6 +111,10 @@ protected async Task<T> Delete<T>(string path) where T : class
{
var req = new RestRequest(path, Method.DELETE);

// Bug in HA or RestSharp if Gzip is enabled, so disable it for now
req.AddDecompressionMethod(DecompressionMethods.None);
req.AddHeader("Accept-Encoding", "identity");

var resp = await Client.ExecuteAsync(req);

if (!string.IsNullOrWhiteSpace(resp.Content) && (resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.NoContent))
Expand All @@ -124,6 +139,10 @@ protected async Task Delete(string path)
{
var req = new RestRequest(path, Method.DELETE);

// Bug in HA or RestSharp if Gzip is enabled, so disable it for now
req.AddDecompressionMethod(DecompressionMethods.None);
req.AddHeader("Accept-Encoding", "identity");

var resp = await Client.ExecuteAsync(req);

if (!(resp.StatusCode == HttpStatusCode.OK || resp.StatusCode == HttpStatusCode.NoContent))
Expand Down
61 changes: 61 additions & 0 deletions HADotNet.Core/Clients/LogbookClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using HADotNet.Core.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace HADotNet.Core.Clients
{
/// <summary>
/// Provides access to the Logbook API for retrieving and querying for change events.
/// </summary>
public sealed class LogbookClient : BaseClient
{
/// <summary>
/// Initializes a new instance of the <see cref="LogbookClient" />.
/// </summary>
/// <param name="instance">The Home Assistant base instance URL.</param>
/// <param name="apiKey">The Home Assistant long-lived access token.</param>
public LogbookClient(Uri instance, string apiKey) : base(instance, apiKey) { }

/// <summary>
/// Retrieves a list of ALL logbook states for all entities for the past 1 day.
/// </summary>
/// <returns>A <see cref="List{LogbookObject}"/> representing a 24-hour history snapshot for all entities.</returns>
public async Task<List<LogbookObject>> GetLogbookEntries() => await Get<List<LogbookObject>>("/api/logbook");

/// <summary>
/// Retrieves a list of ALL historical states for all entities for the specified day (<paramref name="startDate" /> + 24 hours). WARNING: On larger HA installs, this can return 300+ entities, over 4 MB of data, and take 20+ seconds.
/// </summary>
/// <returns>A <see cref="List{LogbookObject}"/> representing a 24-hour history snapshot starting from <paramref name="startDate" /> for all entities.</returns>
public async Task<List<LogbookObject>> GetLogbookEntries(DateTimeOffset startDate) => await Get<List<LogbookObject>>($"/api/logbook/{startDate.UtcDateTime:yyyy-MM-dd\\THH:mm:ss\\+00\\:00}");

/// <summary>
/// Retrieves a list of ALL historical states for all entities for the specified time range, from <paramref name="startDate" /> to <paramref name="endDate" />. WARNING: On larger HA installs, for multiple days, this can return A LOT of data and potentially take a LONG time to return. Use with caution!
/// </summary>
/// <returns>A <see cref="List{LogbookObject}"/> representing a 24-hour history snapshot, from <paramref name="startDate" /> to <paramref name="endDate" />, for all entities.</returns>
public async Task<List<LogbookObject>> GetLogbookEntries(DateTimeOffset startDate, DateTimeOffset endDate) => await Get<List<LogbookObject>>($"/api/logbook/{startDate.UtcDateTime:yyyy-MM-dd\\THH:mm:ss\\+00\\:00}?end_time={Uri.EscapeDataString(endDate.UtcDateTime.ToString("yyyy-MM-dd\\THH:mm:ss\\+00\\:00"))}");

/// <summary>
/// Retrieves a list of ALL historical states for all entities for the specified time range, from <paramref name="startDate" />, for the specified <paramref name="duration" />. WARNING: On larger HA installs, for multiple days, this can return A LOT of data and potentially take a LONG time to return. Use with caution!
/// </summary>
/// <returns>A <see cref="List{LogbookObject}"/> representing a 24-hour history snapshot, from <paramref name="startDate" />, for the specified <paramref name="duration" />, for all entities.</returns>
public async Task<List<LogbookObject>> GetLogbookEntries(DateTimeOffset startDate, TimeSpan duration) => await GetLogbookEntries(startDate, startDate.Add(duration));

/// <summary>
/// Retrieves a list of historical states for the specified <paramref name="entityId" /> for the specified time range, from <paramref name="startDate" /> to <paramref name="endDate" />.
/// </summary>
/// <param name="entityId">The entity ID to filter on.</param>
/// <param name="startDate">The earliest history entry to retrieve.</param>
/// <param name="endDate">The most recent history entry to retrieve.</param>
/// <returns>A <see cref="LogbookObject"/> of history snapshots for the specified <paramref name="entityId" />, from <paramref name="startDate" /> to <paramref name="endDate" />.</returns>
public async Task<LogbookObject> GetLogbookEntries(string entityId, DateTimeOffset startDate, DateTimeOffset endDate) => (await Get<List<LogbookObject>>($"/api/logbook/{startDate.UtcDateTime:yyyy-MM-dd\\THH:mm:ss\\+00\\:00}?entity={entityId}&end_time={Uri.EscapeDataString(endDate.UtcDateTime.ToString("yyyy-MM-dd\\THH:mm:ss\\+00\\:00"))}")).First();

/// <summary>
/// Retrieves a list of historical states for the specified <paramref name="entityId" /> for the past 1 day.
/// </summary>
/// <param name="entityId">The entity ID to retrieve state history for.</param>
/// <returns>A <see cref="LogbookObject"/> representing a 24-hour history snapshot for the specified <paramref name="entityId" />.</returns>
public async Task<LogbookObject> GetLogbookEntries(string entityId) => (await Get<List<LogbookObject>>($"/api/logbook?entity={entityId}")).First();
}
}
8 changes: 4 additions & 4 deletions HADotNet.Core/HADotNet.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
<PackageTags>home-assistant external api library</PackageTags>
<Title>HADotNet Core Library</Title>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>1.3.2</Version>
<Version>1.4.0</Version>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/qJake/HADotNet</PackageProjectUrl>
<RepositoryUrl>https://github.com/qJake/HADotNet.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReleaseNotes></PackageReleaseNotes>
<LangVersion>7.3</LangVersion>
<AssemblyVersion>1.3.2.0</AssemblyVersion>
<FileVersion>1.3.2.0</FileVersion>
<AssemblyVersion>1.4.0.0</AssemblyVersion>
<FileVersion>1.4.0.0</FileVersion>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand All @@ -32,7 +32,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RestSharp" Version="106.11.4" />
<PackageReference Include="RestSharp" Version="106.11.7" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup>
</Project>
89 changes: 89 additions & 0 deletions HADotNet.Core/Models/LogbookObject.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using Newtonsoft.Json;
using System;

namespace HADotNet.Core.Models
{
/// <summary>
/// Represents a logbook entry object. The only consistently available properties are <see cref="Timestamp" />, <see cref="Name" />, and <see cref="EntityId" />.
/// </summary>
public class LogbookObject
{
/// <summary>
/// Gets or sets the entity ID for this entry.
/// </summary>
[JsonProperty("entity_id")]
public string EntityId { get; set; }

/// <summary>
/// Gets or sets the domain.
/// </summary>
[JsonProperty("domain")]
public string Domain { get; set; }

/// <summary>
/// Gets or sets the context user ID associated with this entry.
/// </summary>
[JsonProperty("context_user_id")]
public string ContextUserId { get; set; }

/// <summary>
/// Gets or sets the context entity ID associated with this entry. (For example, which automation triggered this change?)
/// </summary>
[JsonProperty("context_entity_id")]
public string ContextEntityId { get; set; }

/// <summary>
/// Gets or sets the friendly name for the <see cref="ContextEntityId" />. This is sometimes the same as <see cref="ContextName" />.
/// </summary>
[JsonProperty("context_entity_id_name")]
public string ContextEntityIdName { get; set; }

/// <summary>
/// Gets or sets the type of associated change, for example, if an automation triggered this event, the value is 'automation_triggered'.
/// </summary>
[JsonProperty("context_event_type")]
public string ContextEventType { get; set; }

/// <summary>
/// Gets or sets the domain associated with the context entity. For example, if an automation triggered this change, the value is 'automation'.
/// </summary>
[JsonProperty("context_domain")]
public string ContextDomain { get; set; }

/// <summary>
/// Gets or sets the name associated with the context entity (e.g. the name of the automation). This is sometimes the same as <see cref="ContextEntityIdName" />.
/// </summary>
[JsonProperty("context_name")]
public string ContextName { get; set; }

/// <summary>
/// Gets or sets the new state that the entity transitioned to.
/// </summary>
[JsonProperty("state")]
public string State { get; set; }

/// <summary>
/// Gets or sets the source for this logbook entry (i.e. what triggered this change).
/// </summary>
[JsonProperty("source")]
public string Source { get; set; }

/// <summary>
/// Gets or sets the message, a brief description of what happened.
/// </summary>
[JsonProperty("message")]
public string Message { get; set; }

/// <summary>
/// Gets or sets the category name for this entry.
/// </summary>
[JsonProperty("name")]
public string Name { get; set; }

/// <summary>
/// Gets or sets the timestmap when this entry occurred..
/// </summary>
[JsonProperty("when")]
public DateTimeOffset Timestamp { get; set; }
}
}
Loading

0 comments on commit 5da45cd

Please sign in to comment.