Skip to content

Commit

Permalink
chore: Misc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dr1rrb committed Jul 30, 2023
1 parent 50c3486 commit 99002e4
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 120 deletions.
1 change: 1 addition & 0 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<RepositoryUrl>https://github.com/dr1rrb/smarthome.net</RepositoryUrl>
<PackageTags>iot home-assistant hass mqtt zigbee</PackageTags>
<NeutralLanguage>en-US</NeutralLanguage>
<LangVersion>latest</LangVersion>
</PropertyGroup>

</Project>
2 changes: 1 addition & 1 deletion src/SmartHomeDotNet.Package/Mqtt/MqttClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ public async Task Subscribe(CancellationToken ct, string topic)

public Task Publish(CancellationToken ct, string topic, string value, QualityOfService qos, bool retain)
{
this.Log().Info($"Publishing ({(retain?"retained": "volatile")}) message to topic '{topic}': {value}");
this.Log().Debug($"Publishing ({(retain?"retained": "volatile")}) message to topic '{topic}': {value}");

var message = new MqttApplicationMessage(topic, Encoding.UTF8.GetBytes(value), retain);
Task Send(IMqttClient client) => client.PublishAsync(message, (MqttQualityOfService)qos, retain);
Expand Down
10 changes: 7 additions & 3 deletions src/SmartHomeDotNet.Package/Mqtt/MqttSceneHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,20 @@ public async Task Initialized(CancellationToken ct, Scene scene)
AvailabilityTopic = _mqtt.AvailabilityTopic,
Icon = "mdi:script-text-outline"
};
var settings = new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore,
Formatting = Formatting.None
};

// With first publish the scene using the "id" as "name" so Home assistant
// will use it as "entity_id" (which cannot be configured from discovery component)
// Then we publish it a second time using the right name.
// Note: HA will not generate 2 different devices as we are providing device "unique_id" which stays the same.
// Note: This is a patch which works only if HA is up when this config is published, if not, you can still change the entity_id from the UI

await _mqtt.Publish(ct, $"homeassistant/switch/{id}/config", JsonConvert.SerializeObject(config), retain: !_mqtt.IsTestEnvironment);
await _mqtt.Publish(ct, $"homeassistant/switch/{id}/config", JsonConvert.SerializeObject(config, settings), retain: !_mqtt.IsTestEnvironment);
config.Name = scene.Name;
await _mqtt.Publish(ct, $"homeassistant/switch/{id}/config", JsonConvert.SerializeObject(config), retain: !_mqtt.IsTestEnvironment);
await _mqtt.Publish(ct, $"homeassistant/switch/{id}/config", JsonConvert.SerializeObject(config, settings), retain: !_mqtt.IsTestEnvironment);
}

/// <inheritdoc />
Expand Down
21 changes: 20 additions & 1 deletion src/SmartHomeDotNet.Package/SmartHome/Devices/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Dynamic;
using System.Globalization;
using System.Linq;
using System.Threading;

Expand Down Expand Up @@ -49,13 +50,31 @@ void IDeviceAdapter.Init(DeviceState state, IDeviceHost host)
protected virtual void OnInit() { }

/// <summary>
/// Try to get the value of a property, if value is missing returns null
/// Try to get the value of a property, if value is missing returns null.
/// </summary>
/// <param name="property">Name of the property</param>
/// <returns>The value of the property or `null` is the property was not set.</returns>
protected string GetValueOrDefault(string property)
=> GetState().Properties.GetValueOrDefault(property);

/// <summary>
/// Try to get the value of a property, if value is missing returns <paramref name="defaultValue"/>.
/// </summary>
/// <param name="property">Name of the property</param>
/// <param name="defaultValue">The default value to return if the property is not set.</param>
/// <returns>The value of the property or <paramref name="defaultValue"/> is the property was not set.</returns>
protected bool GetBoolOrDefault(string property, bool defaultValue = false)
=> TryGetValue(property, out var rawValue) && bool.TryParse(rawValue, out var value) ? value : defaultValue;

/// <summary>
/// Try to get the value of a property, if value is missing returns <paramref name="defaultValue"/>.
/// </summary>
/// <param name="property">Name of the property</param>
/// <param name="defaultValue">The default value to return if the property is not set.</param>
/// <returns>The value of the property or <paramref name="defaultValue"/> is the property was not set.</returns>
protected int GetInt32OrDefault(string property, int defaultValue = 0)
=> TryGetValue(property, out var rawValue) && int.TryParse(rawValue, NumberStyles.Number, CultureInfo.InvariantCulture, out var value) ? value : defaultValue;

/// <summary>
/// Try to get the value of a property
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class DeviceState
public DeviceState(object deviceId, ImmutableDictionary<string, string> properties, bool isPersistedState)
{
DeviceId = deviceId;
Properties = properties;
Properties = properties; // TODO .WithComparers(StringComparer.OrdinalIgnoreCase); => Actually we should even allow snake casing vs camel case
IsPersistedState = isPersistedState;
}

Expand Down
6 changes: 3 additions & 3 deletions src/SmartHomeDotNet.Package/SmartHome/Scenes/ISceneHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ public interface ISceneHost
/// <param name="scene">The target scene of the command</param>
/// <returns>An observable sequence of the <see cref="SceneCommand"/> sent to the scene</returns>
IObservable<SceneCommand> ObserveCommands(Scene scene);

/// <summary>
/// Notifies the host that a <see cref="Scene"/> has initialized and is now ready to handle commands
/// </summary>
/// <param name="ct">Cancellation to cancel the asynchronous action</param>
/// <param name="scene">The scene that has is now initialized</param>
/// <returns>An synchonous operation</returns>
/// <returns>An asynchronous operation</returns>
Task Initialized(CancellationToken ct, Scene scene);

/// <summary>
Expand All @@ -37,7 +37,7 @@ public interface ISceneHost
/// <param name="ct"></param>
/// <param name="scene">The scene that has started or completed</param>
/// <param name="isRunning">A boolean which indicates if the scene is now running or not</param>
/// <returns>An synchonous operation</returns>
/// <returns>An asynchronous operation</returns>
Task SetIsRunning(CancellationToken ct, Scene scene, bool isRunning);
}
}
6 changes: 4 additions & 2 deletions src/SmartHomeDotNet.Package/SmartHome/Scenes/Scene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public enum RunOption
/// </summary>
AbortPending,

// Queue
// Parallel
// AttachToPending
// AttachToPendingWithCancellationToken
}
Expand Down Expand Up @@ -158,7 +160,7 @@ public async Task Run(CancellationToken ct, RunOption options = RunOption.Ignore
/// Request to start the scene
/// </summary>
/// <remarks>This is a "fire and forget". If you need to track the execution, you should use <see cref="Run"/>.</remarks>
public void Start()
public void Start(RunOption options = RunOption.IgnoreIfRunning)
{
// This method is fire and forget, prevent flowing of the AsyncContext
using (AsyncContext.None())
Expand All @@ -167,7 +169,7 @@ public void Start()
{
try
{
await Run(ct);
await Run(ct, options);
}
catch (Exception e)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
#nullable enable

using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
Expand All @@ -8,64 +10,71 @@
using Newtonsoft.Json;
using SmartHomeDotNet.Utils;

namespace SmartHomeDotNet.Hass.Api
namespace SmartHomeDotNet.Hass.Api;

/// <summary>
/// Represent the REST API of an <see cref="HomeAssistantHub"/>. (cf. <seealso cref="https://developers.home-assistant.io/docs/en/external_api_rest.html"/>).
/// </summary>
public class HomeAssistantHttpApi
{
private readonly HttpClient _client;
private readonly string _host;

/// <summary>
/// Represent the REST API of an <see cref="HomeAssistantHub"/>. (cf. <seealso cref="https://developers.home-assistant.io/docs/en/external_api_rest.html"/>).
/// Creates a new instance given the uri and teh password of the REST API of a <see cref="HomeAssistantHub"/>.
/// </summary>
public class HomeAssistantHttpApi
/// <param name="host">The uri of the REST API, eg. IP_ADDRESS:8123</param>
/// <param name="apiToken">The API token of your home assistant hub</param>
public HomeAssistantHttpApi(string host, string apiToken)
{
private readonly HttpClient _client;
private readonly string _host;
_host = host;

/// <summary>
/// Creates a new instance given the uri and teh password of the REST API of a <see cref="HomeAssistantHub"/>.
/// </summary>
/// <param name="host">The uri of the REST API, eg. IP_ADDRESS:8123</param>
/// <param name="apiToken">The API token of your home assistant hub</param>
public HomeAssistantHttpApi(string host, string apiToken)
var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, certificate, chain, errors) => true;
_client = new HttpClient(handler)
{
_host = host;

var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = (message, certificate, chain, errors) => true;
_client = new HttpClient(handler)
DefaultRequestHeaders =
{
DefaultRequestHeaders =
{
Authorization = new AuthenticationHeaderValue("Bearer", apiToken)
}
};
}
Authorization = new AuthenticationHeaderValue("Bearer", apiToken)
}
};
}

/// <summary>
/// Call a service within a specific domain <seealso cref="https://developers.home-assistant.io/docs/en/external_api_rest.html#post-apiservicesltdomainltservice"/>.
/// </summary>
/// <param name="domain">The target domain of this call</param>
/// <param name="service">The service to invoke</param>
/// <param name="data">The parameters of the service (will be Json encoded), or `null` if no parameters</param>
/// <param name="transition">The expected duration of the effect of the commend (for instance the fade in/out of a light)</param>
/// <returns>An asynchronous <see cref="AsyncContextOperation"/>.</returns>
public AsyncContextOperation CallService(string domain, string service, object data, TimeSpan? transition = null)
/// <summary>
/// Call a service within a specific domain <seealso cref="https://developers.home-assistant.io/docs/en/external_api_rest.html#post-apiservicesltdomainltservice"/>.
/// </summary>
/// <param name="domain">The target domain of this call</param>
/// <param name="service">The service to invoke</param>
/// <param name="data">The parameters of the service (will be Json encoded), or `null` if no parameters</param>
/// <param name="transition">The expected duration of the effect of the commend (for instance the fade in/out of a light)</param>
/// <returns>An asynchronous <see cref="AsyncContextOperation"/>.</returns>
public AsyncContextOperation CallService(string domain, string service, object data, TimeSpan? transition = null)
{
if (transition.GetValueOrDefault() > TimeSpan.Zero)
{
if (transition.GetValueOrDefault() > TimeSpan.Zero)
{
return AsyncContextOperation.StartNew(Send, ct => Task.Delay(transition.Value, ct));
}
else
{
return AsyncContextOperation.StartNew(Send);
}
return AsyncContextOperation.StartNew(Send, ct => Task.Delay(transition.Value, ct));
}
else
{
return AsyncContextOperation.StartNew(Send);
}

async Task Send(CancellationToken ct)
{
var content = data == null
? null
: new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"https://{_host}/api/services/{domain}/{service}", content, ct);
async Task Send(CancellationToken ct)
{
var content = data == null
? null
: new StringContent(JsonConvert.SerializeObject(data), Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"https://{_host}/api/services/{domain}/{service}", content, ct);

response.EnsureSuccessStatusCode();
}
response.EnsureSuccessStatusCode();
}
}

/// <summary>
/// Send an **authenticated** raw request to home assistant
/// </summary>
/// <param name="request">The requests to send</param>
/// <returns>The asynchronous response from the server.</returns>
public Task<HttpResponseMessage> Send(HttpRequestMessage request)
=> _client.SendAsync(request);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.Linq;
using SmartHomeDotNet.SmartHome.Devices;

Expand All @@ -19,7 +20,7 @@ public class InputTimeSpan : Device, IInputTimeSpan
/// <summary>
/// Gets the defined VALUE
/// </summary>
public TimeSpan Time => TimeSpan.FromSeconds(double.Parse(Raw.timestamp));
public TimeSpan Time => TimeSpan.Parse(Raw.state, CultureInfo.InvariantCulture);

public static implicit operator TimeSpan(InputTimeSpan input)
=> input.Time;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

Expand Down Expand Up @@ -41,5 +42,31 @@ public class HomeAssistantConfig

[JsonProperty("retain")]
public bool IsRetained { get; set; }

[JsonProperty("device")]
public HomeAssistantConfigDevice Device { get; set; }
}

public class HomeAssistantConfigDevice
{
public HomeAssistantConfigDevice(string identifier)
{
Identifiers = new List<string> {identifier};
}

[JsonProperty("identifiers")]
public List<string> Identifiers { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("sw_version")]
public string Firmware { get; set; }

[JsonProperty("model")]
public string Model { get; set; }

[JsonProperty("manufacturer")]
public string Manufacturer { get; set; }
}
}
Loading

0 comments on commit 99002e4

Please sign in to comment.