Skip to content

Commit

Permalink
feat(station): Added Shelly channel
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandarDev committed Oct 20, 2023
1 parent 90e72f4 commit a49b989
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 2 deletions.
2 changes: 2 additions & 0 deletions station/Signal.Beacon.WorkerService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Signal.Beacon.Core.Helpers;
using Signal.Beacon.Voice;
using Signalco.Station.Channel.MiFlora;
using Signalco.Station.Channel.Shelly;

namespace Signal.Beacon;

Expand All @@ -41,6 +42,7 @@ private static IHostBuilder CreateHostBuilder(string[] args) =>
.AddSamsung()
.AddMiFlora()
.AddIRobot()
.AddShelly()
.AddVoice();

services.AddTransient(typeof(Lazy<>), typeof(Lazier<>));
Expand Down
1 change: 1 addition & 0 deletions station/Signal.Beacon.WorkerService/Signal.Beacon.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<ProjectReference Include="..\Signal.Beacon.Voice\Signal.Beacon.Voice.csproj" />
<ProjectReference Include="..\Signal.Beacon.Channel.Zigbee2Mqtt\Signal.Beacon.Channel.Zigbee2Mqtt.csproj" />
<ProjectReference Include="..\Signalco.Station.Channel.MiFlora\Signalco.Station.Channel.MiFlora.csproj" />
<ProjectReference Include="..\Signalco.Station.Channel.Shelly\Signalco.Station.Channel.Shelly.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
7 changes: 5 additions & 2 deletions station/Signal.Beacon.WorkerService/WorkerServiceManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ public async Task StartAllWorkerServicesAsync(CancellationToken cancellationToke
c.ContactName == KnownContacts.ChannelStationId &&
c.ValueSerialized == stationState.Id));

var applicableChannels = assignedChannels
.Where(c => (c.ContactOrDefault("signalco", "disabled").ValueSerialized?.ToLowerInvariant() ?? "false") != "true");

// Start all channels workers
await Task.WhenAll(assignedChannels.Select(assignedChannel =>
this.StartWorkerServiceAsync(assignedChannel.Id, cancellationToken)));
await Task.WhenAll(applicableChannels.Select(assignedChannel =>
this.StartWorkerServiceAsync(assignedChannel.Id, cancellationToken)));

this.logger.LogInformation("All worker services started.");
}
Expand Down
6 changes: 6 additions & 0 deletions station/Signal.Beacon.sln
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Signal.Beacon.Voice.Tests",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Signalco.Station.Channel.MiFlora", "Signalco.Station.Channel.MiFlora\Signalco.Station.Channel.MiFlora.csproj", "{CEB8F61F-E257-41B2-AC82-B2A93A35C2DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Signalco.Station.Channel.Shelly", "Signalco.Station.Channel.Shelly\Signalco.Station.Channel.Shelly.csproj", "{A07B250F-73E4-4F82-AC60-886A493F4420}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -108,6 +110,10 @@ Global
{CEB8F61F-E257-41B2-AC82-B2A93A35C2DA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CEB8F61F-E257-41B2-AC82-B2A93A35C2DA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CEB8F61F-E257-41B2-AC82-B2A93A35C2DA}.Release|Any CPU.Build.0 = Release|Any CPU
{A07B250F-73E4-4F82-AC60-886A493F4420}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A07B250F-73E4-4F82-AC60-886A493F4420}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A07B250F-73E4-4F82-AC60-886A493F4420}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A07B250F-73E4-4F82-AC60-886A493F4420}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
4 changes: 4 additions & 0 deletions station/Signal.Beacon.sln.DotSettings
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=MDNS/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mirek/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=mqtt/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=multicast/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=multicasting/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=philipshue/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Roomba/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Signalco/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Tasmota/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=unicast/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Upsert/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=zigbee/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
9 changes: 9 additions & 0 deletions station/Signalco.Station.Channel.Shelly/Shelly3emApiClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Refit;

namespace Signalco.Station.Channel.Shelly;

internal interface Shelly3emApiClient
{
[Get("/status")]
Task<Shelly3emStatusDto> GetStatusAsync();
}
50 changes: 50 additions & 0 deletions station/Signalco.Station.Channel.Shelly/Shelly3emStatusDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Text.Json.Serialization;

namespace Signalco.Station.Channel.Shelly;

internal class Shelly3emStatusDto
{
[JsonPropertyName("relays")] public List<Relay>? Relays { get; set; }

[JsonPropertyName("emeters")] public List<Emeter>? Emeters { get; set; }

[JsonPropertyName("total_power")] public double? TotalPower { get; set; }

[JsonPropertyName("fs_mounted")] public bool? FsMounted { get; set; }

public class Emeter
{
[JsonPropertyName("power")] public double Power { get; set; }

[JsonPropertyName("pf")] public double Pf { get; set; }

[JsonPropertyName("current")] public double Current { get; set; }

[JsonPropertyName("voltage")] public double Voltage { get; set; }

[JsonPropertyName("is_valid")] public bool IsValid { get; set; }

[JsonPropertyName("total")] public double Total { get; set; }

[JsonPropertyName("total_returned")] public double TotalReturned { get; set; }
}

public class Relay
{
[JsonPropertyName("ison")] public bool Ison { get; set; }

[JsonPropertyName("has_timer")] public bool HasTimer { get; set; }

[JsonPropertyName("timer_started")] public int TimerStarted { get; set; }

[JsonPropertyName("timer_duration")] public int TimerDuration { get; set; }

[JsonPropertyName("timer_remaining")] public int TimerRemaining { get; set; }

[JsonPropertyName("overpower")] public bool Overpower { get; set; }

[JsonPropertyName("is_valid")] public bool IsValid { get; set; }

[JsonPropertyName("source")] public string Source { get; set; }
}
}
162 changes: 162 additions & 0 deletions station/Signalco.Station.Channel.Shelly/ShellyChannels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Refit;
using Signal.Beacon.Core.Entity;
using Signal.Beacon.Core.Workers;

namespace Signalco.Station.Channel.Shelly;

internal static class ShellyChannels
{
public const string Shelly = "shelly";
}

public static class ShellyWorkerServiceCollectionExtensions
{
public static IServiceCollection AddShelly(this IServiceCollection services) =>
services
.AddTransient<IWorkerServiceRegistration, ShellyWorkerServiceRegistration >()
.AddTransient<ShellyWorkerService>();
}

internal sealed class ShellyWorkerServiceRegistration : IWorkerServiceRegistration
{
public string ChannelName => ShellyChannels.Shelly;

public Type WorkerServiceType => typeof(ShellyWorkerService);
}

internal class ShellyWorkerService : IWorkerService
{
private readonly IEntitiesDao entitiesDao;
private readonly IEntityService entityService;
private readonly ILogger<ShellyWorkerService> logger;
private readonly CancellationTokenSource cts = new();

public ShellyWorkerService(
IEntitiesDao entitiesDao,
IEntityService entityService,
ILogger<ShellyWorkerService> logger)
{
this.entitiesDao = entitiesDao;
this.entityService = entityService;
this.logger = logger;
}

public async Task StartAsync(string entityId, CancellationToken cancellationToken)
{
var devices = (await this.entitiesDao.AllAsync(cancellationToken))
.Where(e => e.Type == EntityType.Device && e.Contacts.Any(c => c.ChannelName == ShellyChannels.Shelly))
.ToList();

foreach (var device in devices)
_ = this.StartDeviceAsync(device, cancellationToken);
}

private async Task StartDeviceAsync(IEntityDetails entity, CancellationToken cancellationToken)
{
var configurationJson = entity.Contact(ShellyChannels.Shelly, "configuration")?.ValueSerialized;
if (string.IsNullOrWhiteSpace(configurationJson))
{
this.logger.LogWarning("Entity {EntityId} doesn't have valid configuration. Please finish discovery for device first.", entity.Id);
return;
}

var configuration = JsonSerializer.Deserialize<ShellyDeviceConfiguration>(configurationJson);
if (string.IsNullOrWhiteSpace(configuration.IpAddress))
{
this.logger.LogWarning("Entity {EntityId} has invalid configuration. Please re-configure the device first.", entity.Id);
return;
}

try
{
var entityApiAddress = $"http://{configuration.IpAddress}/";
var client = RestService.For<Shelly3emApiClient>(entityApiAddress);

while (!this.cts.Token.IsCancellationRequested)
{
var status = await client.GetStatusAsync();
for (var i = 0; i < status.Emeters?.Count; i++)
{
var meterStatus = status.Emeters[i];
await this.entityService.ContactSetAsync(
new ContactPointer(entity.Id, ShellyChannels.Shelly, $"meter-{i}-power"),
meterStatus.Power.ToString(), cancellationToken);
}

await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
}
}
catch (Exception ex)
{
this.logger.LogWarning(ex, "Failed to retrieve status for entity " + entity.Id);
}
}

public Task StopAsync()
{
this.cts.Cancel();
return Task.CompletedTask;
}
}

internal interface Shelly3emApiClient
{
[Get("/status")]
Task<Shelly3emStatusDto> GetStatusAsync();
}

internal class Shelly3emStatusDto
{
[JsonPropertyName("relays")] public List<Relay>? Relays { get; set; }

[JsonPropertyName("emeters")] public List<Emeter>? Emeters { get; set; }

[JsonPropertyName("total_power")] public double? TotalPower { get; set; }

[JsonPropertyName("fs_mounted")] public bool? FsMounted { get; set; }

public class Emeter
{
[JsonPropertyName("power")] public double Power { get; set; }

[JsonPropertyName("pf")] public double Pf { get; set; }

[JsonPropertyName("current")] public double Current { get; set; }

[JsonPropertyName("voltage")] public double Voltage { get; set; }

[JsonPropertyName("is_valid")] public bool IsValid { get; set; }

[JsonPropertyName("total")] public double Total { get; set; }

[JsonPropertyName("total_returned")] public double TotalReturned { get; set; }
}

public class Relay
{
[JsonPropertyName("ison")] public bool Ison { get; set; }

[JsonPropertyName("has_timer")] public bool HasTimer { get; set; }

[JsonPropertyName("timer_started")] public int TimerStarted { get; set; }

[JsonPropertyName("timer_duration")] public int TimerDuration { get; set; }

[JsonPropertyName("timer_remaining")] public int TimerRemaining { get; set; }

[JsonPropertyName("overpower")] public bool Overpower { get; set; }

[JsonPropertyName("is_valid")] public bool IsValid { get; set; }

[JsonPropertyName("source")] public string Source { get; set; }
}
}

internal class ShellyDeviceConfiguration
{
public string IpAddress { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Signalco.Station.Channel.Shelly;

internal class ShellyDeviceConfiguration
{
public string IpAddress { get; set; }
}
82 changes: 82 additions & 0 deletions station/Signalco.Station.Channel.Shelly/ShellyWorkerService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Refit;
using Signal.Beacon.Core.Entity;
using Signal.Beacon.Core.Workers;

namespace Signalco.Station.Channel.Shelly;

internal class ShellyWorkerService : IWorkerService
{
private readonly IEntitiesDao entitiesDao;
private readonly IEntityService entityService;
private readonly ILogger<ShellyWorkerService> logger;
private readonly CancellationTokenSource cts = new();

public ShellyWorkerService(
IEntitiesDao entitiesDao,
IEntityService entityService,
ILogger<ShellyWorkerService> logger)
{
this.entitiesDao = entitiesDao;
this.entityService = entityService;
this.logger = logger;
}

public async Task StartAsync(string entityId, CancellationToken cancellationToken)
{
var devices = (await this.entitiesDao.AllAsync(cancellationToken))
.Where(e => e.Type == EntityType.Device && e.Contacts.Any(c => c.ChannelName == ShellyChannels.Shelly))
.ToList();

foreach (var device in devices)
_ = this.StartDeviceAsync(device, cancellationToken);
}

private async Task StartDeviceAsync(IEntityDetails entity, CancellationToken cancellationToken)
{
var configurationJson = entity.Contact(ShellyChannels.Shelly, "configuration")?.ValueSerialized;
if (string.IsNullOrWhiteSpace(configurationJson))
{
this.logger.LogWarning("Entity {EntityId} doesn't have valid configuration. Please finish discovery for device first.", entity.Id);
return;
}

var configuration = JsonSerializer.Deserialize<ShellyDeviceConfiguration>(configurationJson);
if (string.IsNullOrWhiteSpace(configuration.IpAddress))
{
this.logger.LogWarning("Entity {EntityId} has invalid configuration. Please re-configure the device first.", entity.Id);
return;
}

try
{
var entityApiAddress = $"http://{configuration.IpAddress}/";
var client = RestService.For<Shelly3emApiClient>(entityApiAddress);

while (!this.cts.Token.IsCancellationRequested)
{
var status = await client.GetStatusAsync();
for (var i = 0; i < status.Emeters?.Count; i++)
{
var meterStatus = status.Emeters[i];
await this.entityService.ContactSetAsync(
new ContactPointer(entity.Id, ShellyChannels.Shelly, $"meter-{i}-power"),
meterStatus.Power.ToString(), cancellationToken);
}

await Task.Delay(TimeSpan.FromSeconds(60), cancellationToken);
}
}
catch (Exception ex)
{
this.logger.LogWarning(ex, "Failed to retrieve status for entity " + entity.Id);
}
}

public Task StopAsync()
{
this.cts.Cancel();
return Task.CompletedTask;
}
}
Loading

0 comments on commit a49b989

Please sign in to comment.