-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
90e72f4
commit a49b989
Showing
13 changed files
with
368 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
9
station/Signalco.Station.Channel.Shelly/Shelly3emApiClient.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
50
station/Signalco.Station.Channel.Shelly/Shelly3emStatusDto.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
162
station/Signalco.Station.Channel.Shelly/ShellyChannels.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} |
6 changes: 6 additions & 0 deletions
6
station/Signalco.Station.Channel.Shelly/ShellyDeviceConfiguration.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
82
station/Signalco.Station.Channel.Shelly/ShellyWorkerService.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.