diff --git a/EgwProxy.Shelly.Test/App.config b/EgwProxy.Shelly.Test/App.config new file mode 100644 index 0000000..0b589db --- /dev/null +++ b/EgwProxy.Shelly.Test/App.config @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly.Test/EgwProxy.Shelly.Test.csproj b/EgwProxy.Shelly.Test/EgwProxy.Shelly.Test.csproj new file mode 100644 index 0000000..dce3fb9 --- /dev/null +++ b/EgwProxy.Shelly.Test/EgwProxy.Shelly.Test.csproj @@ -0,0 +1,69 @@ + + + + + Debug + AnyCPU + {081AB7E7-4C36-49D7-9343-2C567F324B74} + Exe + EgwProxy.Shelly.Test + EgwProxy.Shelly.Test + v4.6.2 + 512 + true + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + Always + + + + + + {69b1068b-e53f-4e54-9a3d-1031f84889ba} + EgwProxy.Shelly + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly.Test/Program.cs b/EgwProxy.Shelly.Test/Program.cs new file mode 100644 index 0000000..a087a9c --- /dev/null +++ b/EgwProxy.Shelly.Test/Program.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Newtonsoft.Json; +using EgwProxy.Shelly.Clients; +using System.Net.Http; +using EgwProxy.Shelly.Options; +using System.Threading; + +namespace EgwProxy.Shelly.Test +{ + internal class Program + { + + /// + /// Helper separatore dash + /// + private const string separator = "------------------------"; + + private static Stopwatch sw = new Stopwatch(); + /// + /// legge conf in formato stringa + /// + /// + /// + protected static string ReadSetting(string key) + { + string answ = ""; + try + { + answ = $"{ConfigurationManager.AppSettings[key]}" ?? ""; + } + catch (Exception exc) + { + Console.Write("Eccezione in ReadSettings"); + Console.Write(exc.Message); + } + return answ; + } + + //// caffè + //private string shellyAddr = "10.74.81.72"; + //APC + private string shellyAddr = "10.74.81.71"; + + /// + /// Programma principale + /// + /// + private static void Main(string[] args) + { + Console.WriteLine(separator); + Console.WriteLine("Test Shelly Client"); + Console.WriteLine(separator); + Console.WriteLine(); + string exePath = System.Reflection.Assembly.GetExecutingAssembly().Location; + string BaseDirectory = System.IO.Path.GetDirectoryName(exePath); + string testFile = Path.Combine(BaseDirectory, "conf", ReadSetting("testFile")); + if (!string.IsNullOrEmpty(testFile)) + { + Console.WriteLine(separator); + Console.WriteLine($"Mode json ({testFile})"); + Console.WriteLine(separator); + Console.WriteLine(); + if (File.Exists(testFile)) + { + var rawData = File.ReadAllText(testFile); + if (!string.IsNullOrEmpty(rawData)) + { + TestSetup testConf = new TestSetup(); + try + { + testConf = JsonConvert.DeserializeObject(rawData); + } + catch + { } + // setup devAddr + Shelly1PmOptions options = new Shelly1PmOptions() + { + DefaultTimeout = TimeSpan.FromSeconds(testConf.tOutSec), + ServerUri = new Uri($"http://{testConf.devAddr}/rpc") + }; + Shelly1PmClient shelly = new Shelly1PmClient(new HttpClient(), options); + // test chiamata completa + serverTest(shelly); + bool doRepeat = true; + while (doRepeat) + { + // eseguo per ogni step + foreach (var item in testConf.steps) + { + // ripeto per il num di volte richieste... + for (int i = 0; i < item.numRep; i++) + { + Console.WriteLine(separator); + Console.WriteLine($"Rep: {i + 1}"); + Console.WriteLine(separator); + string esitoStep = ""; + sw.Restart(); + switch (item.action) + { + case stepType.getFullStatus: + var respFull = Task.Run(() => shelly.GetStatus(CancellationToken.None)).Result; + if (respFull.IsSuccess) + { + esitoStep = JsonConvert.SerializeObject(respFull.Value, Formatting.Indented); + } + else + { + esitoStep = "Errore in GetStatus"; + } + break; + case stepType.getSwitchStatus: + var respSwitch = Task.Run(() => shelly.GetSwitchStatus(CancellationToken.None, 0)).Result; + if (respSwitch.IsSuccess) + { + esitoStep = JsonConvert.SerializeObject(respSwitch.Value, Formatting.Indented); + } + else + { + esitoStep = "Errore in GetSwitchStatus"; + } + break; + default: + esitoStep = "Action not managed: skipping"; + break; + } + sw.Stop(); + Console.WriteLine(esitoStep); + Console.WriteLine(separator); + Console.WriteLine($"Elapsed: {sw.Elapsed.TotalMilliseconds:N3}ms"); + Console.WriteLine(separator); + Console.WriteLine(); + Thread.Sleep(item.waitTime); + } + Console.WriteLine($"------ Done Step {item.id} ------"); + } + + Console.WriteLine("Do you want to repeat from the beginnning? esc to close"); + ConsoleKeyInfo answ = Console.ReadKey(); + doRepeat = answ.Key != ConsoleKey.Escape; + } + } + } + } + } + + private static void serverTest(Shelly1PmClient shellyClient) + { + Console.WriteLine(separator); + // registro ed eseguo chiamata in modalità sincrona + sw.Restart(); + var response = Task.Run(() => shellyClient.GetStatus(CancellationToken.None)).Result; + sw.Stop(); + Console.WriteLine($"CallSuccess: {response.IsSuccess}"); + if (response.Value != null) + { + Console.WriteLine(separator); + Console.WriteLine(JsonConvert.SerializeObject(response.Value, Formatting.Indented)); + Console.WriteLine(separator); + Console.WriteLine($"Elapsed: {sw.Elapsed.TotalMilliseconds:N3}ms"); + Console.WriteLine(separator); + Console.WriteLine(); + } + } + } +} diff --git a/EgwProxy.Shelly.Test/Properties/AssemblyInfo.cs b/EgwProxy.Shelly.Test/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..eeaef9d --- /dev/null +++ b/EgwProxy.Shelly.Test/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EgwProxy.Shelly.Test")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EgwProxy.Shelly.Test")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("081ab7e7-4c36-49d7-9343-2c567f324b74")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/EgwProxy.Shelly.Test/TestSetup.cs b/EgwProxy.Shelly.Test/TestSetup.cs new file mode 100644 index 0000000..b617b7c --- /dev/null +++ b/EgwProxy.Shelly.Test/TestSetup.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Test +{ + public class TestSetup + { + public string devAddr { get; set; } = ""; + public int tOutSec{ get; set; } = 5; + public List steps { get; set; } + } + + public class singleStep + { + public string id { get; set; } = "00"; + public string description { get; set; } = "00"; + public stepType action { get; set; } = stepType.none; + + public int numRep { get; set; } = 1; + + public int waitTime { get; set; } = 1000; + + public List paramList { get; set; } = new List(); + } + + public enum stepType + { + none, + getFullStatus, + getSwitchStatus + } + +} diff --git a/EgwProxy.Shelly.Test/conf/.placeholder b/EgwProxy.Shelly.Test/conf/.placeholder new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/EgwProxy.Shelly.Test/conf/.placeholder @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/EgwProxy.Shelly.Test/conf/testSetup.json b/EgwProxy.Shelly.Test/conf/testSetup.json new file mode 100644 index 0000000..f8be26c --- /dev/null +++ b/EgwProxy.Shelly.Test/conf/testSetup.json @@ -0,0 +1,13 @@ +{ + "devAddr": "10.74.81.71", + "steps": [ + { + "id": "01", + "description": "Test SwitchStatus", + "action": "getSwitchStatus", + "numRep": 5, + "waitTime": 500, + "paramList": [] + } + ] +} \ No newline at end of file diff --git a/EgwProxy.Shelly.Test/packages.config b/EgwProxy.Shelly.Test/packages.config new file mode 100644 index 0000000..6f243a9 --- /dev/null +++ b/EgwProxy.Shelly.Test/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly/Clients/IShelly1.cs b/EgwProxy.Shelly/Clients/IShelly1.cs new file mode 100644 index 0000000..516da82 --- /dev/null +++ b/EgwProxy.Shelly/Clients/IShelly1.cs @@ -0,0 +1,12 @@ +using EgwProxy.Shelly.DTO; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Clients +{ + public interface IShelly1 + { + Task> GetStatus(CancellationToken cancellationToken, TimeSpan? timeout = null); + } +} diff --git a/EgwProxy.Shelly/Clients/IShelly1Pm.cs b/EgwProxy.Shelly/Clients/IShelly1Pm.cs new file mode 100644 index 0000000..3913879 --- /dev/null +++ b/EgwProxy.Shelly/Clients/IShelly1Pm.cs @@ -0,0 +1,12 @@ +using EgwProxy.Shelly.DTO; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Clients +{ + public interface IShelly1Pm + { + Task> GetStatus(CancellationToken cancellationToken, TimeSpan? timeout = null); + } +} diff --git a/EgwProxy.Shelly/Clients/Shelly1Client.cs b/EgwProxy.Shelly/Clients/Shelly1Client.cs new file mode 100644 index 0000000..598363c --- /dev/null +++ b/EgwProxy.Shelly/Clients/Shelly1Client.cs @@ -0,0 +1,24 @@ +using EgwProxy.Shelly.DTO; +using EgwProxy.Shelly.Options; +using Flurl; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Clients +{ + public class Shelly1Client : ShellyClientBase, IShelly1 + { + public Shelly1Client(HttpClient httpClient, Shelly1Options shelly1Options) : base(httpClient, shelly1Options) + { + } + + public async Task> GetStatus(CancellationToken cancellationToken, TimeSpan? timeout = null) + { + var endpoint = ServerUri.AppendPathSegment("status"); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, endpoint); + return await ExecuteRequestAsync(requestMessage, cancellationToken, timeout); + } + } +} diff --git a/EgwProxy.Shelly/Clients/Shelly1PmClient.cs b/EgwProxy.Shelly/Clients/Shelly1PmClient.cs new file mode 100644 index 0000000..269c4e8 --- /dev/null +++ b/EgwProxy.Shelly/Clients/Shelly1PmClient.cs @@ -0,0 +1,31 @@ +using EgwProxy.Shelly.DTO; +using EgwProxy.Shelly.Options; +using Flurl; +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Clients +{ + public class Shelly1PmClient : ShellyClientBase, IShelly1Pm + { + public Shelly1PmClient(HttpClient httpClient, Shelly1PmOptions shellyOptions) : base(httpClient, shellyOptions) + { + } + + public async Task> GetStatus(CancellationToken cancellationToken, TimeSpan? timeout = null) + { + var endpoint = ServerUri.AppendPathSegment("Shelly.GetStatus"); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, endpoint); + return await ExecuteRequestAsync(requestMessage, cancellationToken, timeout); + } + + public async Task> GetSwitchStatus(CancellationToken cancellationToken, int id, TimeSpan? timeout = null) + { + var endpoint = ServerUri.AppendPathSegment("Switch.GetStatus").AppendQueryParam("id",id); + var requestMessage = new HttpRequestMessage(HttpMethod.Get, endpoint); + return await ExecuteRequestAsync(requestMessage, cancellationToken, timeout); + } + } +} diff --git a/EgwProxy.Shelly/Clients/ShellyClientBase.cs b/EgwProxy.Shelly/Clients/ShellyClientBase.cs new file mode 100644 index 0000000..f1e56ae --- /dev/null +++ b/EgwProxy.Shelly/Clients/ShellyClientBase.cs @@ -0,0 +1,84 @@ +using EgwProxy.Shelly.Options; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Clients +{ + public abstract class ShellyClientBase + { + protected readonly HttpClient ShellyHttpClient; + private readonly IShellyCommonOptions _shellyCommonOptions; + protected readonly Uri ServerUri; + + protected TimeSpan DefaultTimeout = TimeSpan.FromSeconds(5); + + public ShellyClientBase(HttpClient httpClient, IShellyCommonOptions shellyCommonOptions) + { + if (httpClient == null) throw new ArgumentNullException(nameof(httpClient)); + if (shellyCommonOptions == null) throw new ArgumentNullException(nameof(shellyCommonOptions)); + + + ShellyHttpClient = httpClient; + _shellyCommonOptions = shellyCommonOptions; + ServerUri = shellyCommonOptions.ServerUri; + + if (shellyCommonOptions.DefaultTimeout.HasValue) + { + DefaultTimeout = shellyCommonOptions.DefaultTimeout.Value; + } + } + + protected async Task> ExecuteRequestAsync(HttpRequestMessage httpRequestMessage, + CancellationToken cancellationToken, TimeSpan? timeout = null) + { + timeout = timeout ?? DefaultTimeout; + + using (var timeoutTokenSource = new CancellationTokenSource(timeout.Value)) + { + var authenticationString = $"{_shellyCommonOptions.UserName}:{_shellyCommonOptions.Password}"; + var base64EncodedAuthenticationString = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString)); + + httpRequestMessage.Headers.Authorization = new AuthenticationHeaderValue("Basic", base64EncodedAuthenticationString); + + var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutTokenSource.Token, cancellationToken); + var response = await ShellyHttpClient.SendAsync(httpRequestMessage, linkedTokenSource.Token); + + if (response.StatusCode == 0) + { + // Status code of 0 means timeout reached + return ShellyResult.TransientFailure("Device did not respond within timeout period"); + } + + if (response.StatusCode == HttpStatusCode.ServiceUnavailable) + { + return ShellyResult.TransientFailure("Device responded with ServiceUnavailable"); + } + + if (response.StatusCode == HttpStatusCode.NotFound) + { + return ShellyResult.Success(default, "Device responded with NotFound"); + } + + if (response.StatusCode != HttpStatusCode.OK) + { + return ShellyResult.Failure($"Device responded with http status code {response.StatusCode}"); + } + + return await HandleOkResponse(response); + } + } + + protected virtual async Task> HandleOkResponse(HttpResponseMessage response) + { + var readAsStringAsync = await response.Content.ReadAsStringAsync(); + var shelly1Status = JsonConvert.DeserializeObject(readAsStringAsync); + return ShellyResult.Success(shelly1Status); + } + } +} diff --git a/EgwProxy.Shelly/DTO/BaseServiceDto.cs b/EgwProxy.Shelly/DTO/BaseServiceDto.cs new file mode 100644 index 0000000..bb026c7 --- /dev/null +++ b/EgwProxy.Shelly/DTO/BaseServiceDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO.Shelly1PM +{ + public class BaseServiceDto + { + /// + /// Connection State + /// + [JsonProperty("connected")] + public bool Connected { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/CloudDto.cs b/EgwProxy.Shelly/DTO/CloudDto.cs new file mode 100644 index 0000000..85d285f --- /dev/null +++ b/EgwProxy.Shelly/DTO/CloudDto.cs @@ -0,0 +1,14 @@ +using EgwProxy.Shelly.DTO.Shelly1PM; +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + public class CloudDto :BaseServiceDto + { + /// + /// Define if service is enabled + /// + [JsonProperty("enabled")] + public bool Enabled { get; set; } = true; + } +} diff --git a/EgwProxy.Shelly/DTO/Shelly1PM/EnergyDto.cs b/EgwProxy.Shelly/DTO/Shelly1PM/EnergyDto.cs new file mode 100644 index 0000000..ecd64a0 --- /dev/null +++ b/EgwProxy.Shelly/DTO/Shelly1PM/EnergyDto.cs @@ -0,0 +1,28 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO.Shelly1PM +{ + /// + /// Energy Info + /// + public class EnergyDto + { + /// + /// Total returned energy consumed in Watt-hours + /// + [JsonProperty("total")] + public double Total { get; set; } + + /// + /// Returned energy in Milliwatt-hours for the last three complete minutes. The 0-th element indicates the counts accumulated during the minute preceding minute_ts. Present only if the device clock is synced. + /// + [JsonProperty("by_minute")] + public double[] LastByMinute { get; set; } + + /// + /// Unix timestamp marking the start of the current minute (in UTC). + /// + [JsonProperty("minute_ts")] + public long TimeStamp { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/Shelly1PM/InputDto.cs b/EgwProxy.Shelly/DTO/Shelly1PM/InputDto.cs new file mode 100644 index 0000000..2290d87 --- /dev/null +++ b/EgwProxy.Shelly/DTO/Shelly1PM/InputDto.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO.Shelly1PM +{ + /// + /// Input info + /// + public class InputDto + { + /// + /// Id of the Input component instance + /// + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// Current State(only for type switch, button) State of the input (null if the input instance is stateless, i.e. for type button) + /// + [JsonProperty("state")] + public bool State { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/Shelly1PM/SwitchDto.cs b/EgwProxy.Shelly/DTO/Shelly1PM/SwitchDto.cs new file mode 100644 index 0000000..b230e3c --- /dev/null +++ b/EgwProxy.Shelly/DTO/Shelly1PM/SwitchDto.cs @@ -0,0 +1,67 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO.Shelly1PM +{ + public class SwitchDto + { + /// + /// Id of the Switch component instance + /// + [JsonProperty("id")] + public int Id { get; set; } = 0; + + /// + /// Source of the last command, for example: init, WS_in, http, ... + /// + [JsonProperty("source")] + public string Source { get; set; } = ""; + + /// + /// Output status: rue if the output channel is currently on, false otherwise + /// + [JsonProperty("output")] + public bool Output { get; set; } + + /// + /// Last measured instantaneous active power(in Watts) delivered to the attached load(shown if applicable) + /// + [JsonProperty("apower")] + public double Power { get; set; } + + /// + /// Last measured voltage in Volts (shown if applicable) + /// + [JsonProperty("voltage")] + public double Voltage { get; set; } + + /// + /// Last measured current in Amperes (shown if applicable) + /// + [JsonProperty("current")] + public double Current { get; set; } + + /// + /// Information about the active energy counter (shown if applicable) + /// + [JsonProperty("aenergy")] + public EnergyDto ActEnergy { get; set; } + + /// + /// Information about the returned active energy counter * (shown if applicable) + /// + [JsonProperty("ret_aenergy")] + public EnergyDto RetEnergy { get; set; } + + /// + /// Information about the temperature (shown if applicable) + /// + [JsonProperty("temperature")] + public TemperatureDto Temperature { get; set; } + + /// + /// Error conditions occurred. May contain overtemp, overpower, overvoltage, undervoltage, (shown if at least one error is present) + /// + [JsonProperty("errors")] + public string[] Errors { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/Shelly1PmStatusDto.cs b/EgwProxy.Shelly/DTO/Shelly1PmStatusDto.cs new file mode 100644 index 0000000..9ae4b6a --- /dev/null +++ b/EgwProxy.Shelly/DTO/Shelly1PmStatusDto.cs @@ -0,0 +1,25 @@ +using EgwProxy.Shelly.DTO.Shelly1PM; +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + /// + /// Extended information for the Shelly1PM + /// https://shelly-api-docs.shelly.cloud/gen2/Devices/Gen3/Shelly1PMG3 + /// + public class Shelly1PmStatusDto : ShellyGen2StatusDto + { + /// + /// Input channel + /// + [JsonProperty("input:0")] + public InputDto Input { get; set; } + + /// + /// Switch 0 data + /// + [JsonProperty("switch:0")] + public SwitchDto Switch { get; set; } + + } +} diff --git a/EgwProxy.Shelly/DTO/ShellyGen2StatusDto.cs b/EgwProxy.Shelly/DTO/ShellyGen2StatusDto.cs new file mode 100644 index 0000000..30bf2a1 --- /dev/null +++ b/EgwProxy.Shelly/DTO/ShellyGen2StatusDto.cs @@ -0,0 +1,44 @@ +using EgwProxy.Shelly.DTO.Shelly1PM; +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + /// + /// Shelly Gen2 device status data + /// https://shelly-api-docs.shelly.cloud/gen2/#status + /// + public class ShellyGen2StatusDto + { + + /// + /// WiFi data + /// + [JsonProperty("wifi")] + public WifiDto WiFiStatus { get; set; } + + /// + /// Shelly Cloud state + /// + [JsonProperty("cloud")] + public CloudDto ShellyCloud { get; set; } + + /// + /// MQTT queue state + /// + [JsonProperty("mqtt")] + public BaseServiceDto MQTT { get; set; } + + /// + /// Sys Info data + /// + [JsonProperty("sys")] + public SysDto SysInfo { get; set; } + + /// + /// WS state + /// + [JsonProperty("ws")] + public BaseServiceDto WebSocket { get; set; } + + } +} diff --git a/EgwProxy.Shelly/DTO/SysDto.cs b/EgwProxy.Shelly/DTO/SysDto.cs new file mode 100644 index 0000000..75b4302 --- /dev/null +++ b/EgwProxy.Shelly/DTO/SysDto.cs @@ -0,0 +1,101 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + /// + /// System Info + /// + public class SysDto + { + /// + /// Mac address of the device + /// + [JsonProperty("mac")] + public string Mac { get; set; } = ""; + + /// + /// True if restart is required, false otherwise + /// + [JsonProperty("restart_required")] + public bool RestartRequired { get; set; } = false; + + /// + /// Current time in the format HH:MM (24-hour time format in the current timezone with leading zero). null when time is not synced from NTP server. + /// + [JsonProperty("time")] + public string Time { get; set; } = ""; + + /// + /// Unix timestamp (in UTC), null when time is not synced from NTP server. + /// + [JsonProperty("unixtime")] + public int UnixTime { get; set; } = 0; + + /// + /// Time in seconds since last reboot + /// + [JsonProperty("uptime")] + public int Uptime { get; set; } = 0; + + /// + /// Total size of the RAM in the system in Bytes + /// + [JsonProperty("ram_size")] + public int RamSize { get; set; } = 0; + + /// + /// Size of the free RAM in the system in Bytes + /// + [JsonProperty("ram_free")] + public int RamFree { get; set; } = 0; + + /// + /// Total size of the file system in Bytes + /// + [JsonProperty("fs_size")] + public int FSSize { get; set; } = 0; + + /// + /// Size of the free file system in Bytes + /// + [JsonProperty("fs_free")] + public int FSFree { get; set; } = 0; + + /// + /// Configuration revision number + /// + [JsonProperty("cfg_rev")] + public int CfgRev { get; set; } = 0; + + /// + /// KVS (Key-Value Store) revision number + /// + [JsonProperty("kvs_rev")] + public int KvsRev { get; set; } = 0; + + /// + /// Schedules revision number, present if schedules are enabled + /// + [JsonProperty("schedule_rev")] + public int ScheduleRev { get; set; } = 0; + + /// + /// Webhooks revision number, present if webhooks are enabled + /// + [JsonProperty("webhook_rev")] + public int WebhookRev { get; set; } = 0; + + /// + /// Webhooks revision number, present if webhooks are enabled + /// + [JsonProperty("available_updates")] + public UpdateDto AvailUpdates { get; set; } + + + /// + /// Information about reset type and cause + /// + [JsonProperty("reset_reason")] + public int ResetReason { get; set; } = 0; + } +} diff --git a/EgwProxy.Shelly/DTO/TemperatureDto.cs b/EgwProxy.Shelly/DTO/TemperatureDto.cs new file mode 100644 index 0000000..48401f8 --- /dev/null +++ b/EgwProxy.Shelly/DTO/TemperatureDto.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + public class TemperatureDto + { + /// + /// Temperature in Celsius (null if the temperature is out of the measurement range) + /// + [JsonProperty("tC")] + public double? Celsius { get; set; } + + /// + /// Temperature in Fahrenheit (null if the temperature is out of the measurement range) + /// + [JsonProperty("tF")] + public double? Fahrenheit { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/UpdateDto.cs b/EgwProxy.Shelly/DTO/UpdateDto.cs new file mode 100644 index 0000000..aa6066a --- /dev/null +++ b/EgwProxy.Shelly/DTO/UpdateDto.cs @@ -0,0 +1,22 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + /// + /// Info about sw update + /// + public class UpdateDto + { + /// + /// Shown only if beta update is available + /// + [JsonProperty("beta")] + public VersDto Beta { get; set; } + + /// + /// Shown only if beta update is available + /// + [JsonProperty("stable")] + public VersDto Stable { get; set; } + } +} diff --git a/EgwProxy.Shelly/DTO/VersDto.cs b/EgwProxy.Shelly/DTO/VersDto.cs new file mode 100644 index 0000000..a93ff5f --- /dev/null +++ b/EgwProxy.Shelly/DTO/VersDto.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + public class VersDto + { + /// + /// Version of the new firmware + /// + [JsonProperty("version")] + public string Version { get; set; } = ""; + } +} diff --git a/EgwProxy.Shelly/DTO/WifiDto.cs b/EgwProxy.Shelly/DTO/WifiDto.cs new file mode 100644 index 0000000..1ced5ca --- /dev/null +++ b/EgwProxy.Shelly/DTO/WifiDto.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; + +namespace EgwProxy.Shelly.DTO +{ + /// + /// WiFi status info + /// + public class WifiDto + { + /// + /// Ip of the device in the network (null if disconnected) + /// + [JsonProperty("sta_ip")] + public string StaIp { get; set; } = ""; + + /// + /// Status of the connection. Range of values: disconnected, connecting, connected, got ip + /// + [JsonProperty("status")] + public string Status { get; set; } = ""; + + /// + /// SSID of the network (null in case of hidden network) + /// + [JsonProperty("ssid")] + public string SSID { get; set; } = ""; + + /// + /// Strength of the signal in dBms + /// + [JsonProperty("rssi")] + public int RSSI { get; set; } = 0; + + /// + /// Number of clients connected to the access point. Present only when AP is enabled and range extender functionality is present and enabled. + /// + [JsonProperty("ap_client_count")] + public int NumClients { get; set; } = 0; + } +} diff --git a/EgwProxy.Shelly/EgwProxy.Shelly.csproj b/EgwProxy.Shelly/EgwProxy.Shelly.csproj new file mode 100644 index 0000000..9b14bd4 --- /dev/null +++ b/EgwProxy.Shelly/EgwProxy.Shelly.csproj @@ -0,0 +1,119 @@ + + + + + Debug + AnyCPU + {69B1068B-E53F-4E54-9A3D-1031F84889BA} + Library + Properties + EgwProxy.Shelly + EgwProxy.Shelly + v4.6.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Flurl.4.0.0\lib\net461\Flurl.dll + + + ..\packages\Flurl.Http.4.0.2\lib\net461\Flurl.Http.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll + + + ..\packages\System.Text.Json.6.0.4\lib\net461\System.Text.Json.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + ..\packages\System.ValueTuple.4.5.0\lib\net461\System.ValueTuple.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly/Options/IShellyAuthOptions.cs b/EgwProxy.Shelly/Options/IShellyAuthOptions.cs new file mode 100644 index 0000000..1ac3d3e --- /dev/null +++ b/EgwProxy.Shelly/Options/IShellyAuthOptions.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Options +{ + /// + /// Common authentication options across all shelly devices + /// + public interface IShellyAuthOptions + { + string UserName { get; } + string Password { get; } + } +} diff --git a/EgwProxy.Shelly/Options/IShellyCommonOptions.cs b/EgwProxy.Shelly/Options/IShellyCommonOptions.cs new file mode 100644 index 0000000..9fb112a --- /dev/null +++ b/EgwProxy.Shelly/Options/IShellyCommonOptions.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Options +{ + /// + /// Common options across all shelly devices + /// + public interface IShellyCommonOptions : IShellyAuthOptions + { + /// + /// URI for the Shelly device + /// + Uri ServerUri { get; } + /// + /// Default timeout for HTTP requests if a specific timeout is not supplied + /// + TimeSpan? DefaultTimeout { get; } + } +} diff --git a/EgwProxy.Shelly/Options/Shelly1Options.cs b/EgwProxy.Shelly/Options/Shelly1Options.cs new file mode 100644 index 0000000..fe63756 --- /dev/null +++ b/EgwProxy.Shelly/Options/Shelly1Options.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Options +{ + public class Shelly1Options : IShellyCommonOptions + { + public Uri ServerUri { get; set; } + public TimeSpan? DefaultTimeout { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + + public Shelly1Options() + { + DefaultTimeout = null; + } + } +} diff --git a/EgwProxy.Shelly/Options/Shelly1PmOptions.cs b/EgwProxy.Shelly/Options/Shelly1PmOptions.cs new file mode 100644 index 0000000..41c0141 --- /dev/null +++ b/EgwProxy.Shelly/Options/Shelly1PmOptions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly.Options +{ + public class Shelly1PmOptions : IShellyCommonOptions + { + public Uri ServerUri { get; set; } + public TimeSpan? DefaultTimeout { get; set; } + public string UserName { get; set; } + public string Password { get; set; } + + public Shelly1PmOptions() + { + DefaultTimeout = null; + } + } +} diff --git a/EgwProxy.Shelly/Properties/AssemblyInfo.cs b/EgwProxy.Shelly/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ef7c2f9 --- /dev/null +++ b/EgwProxy.Shelly/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("EgwProxy.Shelly")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("EgwProxy.Shelly")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("69b1068b-e53f-4e54-9a3d-1031f84889ba")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/EgwProxy.Shelly/ShellyResult.cs b/EgwProxy.Shelly/ShellyResult.cs new file mode 100644 index 0000000..91c543f --- /dev/null +++ b/EgwProxy.Shelly/ShellyResult.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EgwProxy.Shelly +{ + public class ShellyResult + { + public T Value + { + get + { + if (!_successChecked) + { + throw new InvalidOperationException("Cannot access value of result without checking success first"); + } + + return _value; + } + } + + /// + /// Indicates the client request has completed successfully + /// + public bool IsSuccess + { + get + { + _successChecked = true; + return _success; + } + } + + /// + /// Indicates the client request has failed + /// + public bool IsFailure => !IsSuccess; + + /// + /// Indicates if the reason for failure is transient + /// + public bool IsTransient { get; private set; } + + /// + /// Any message that accompanies this result + /// + public string Message { get; } + + private T _value; + private bool _successChecked = false; + private bool _success = false; + + private ShellyResult(T value, bool success, bool isTransient, string message = null) + { + _value = value; + _success = success; + IsTransient = isTransient; + Message = message; + } + + public static ShellyResult Success(T value, string message = null) + { + return new ShellyResult(value, true, false, message); + } + + public static ShellyResult Failure(string message = null) + { + return new ShellyResult(default, success: false, isTransient: false, message); + } + + public static ShellyResult TransientFailure(string message = null) + { + return new ShellyResult(default, success: false, isTransient: true, message); + } + } +} diff --git a/EgwProxy.Shelly/app.config b/EgwProxy.Shelly/app.config new file mode 100644 index 0000000..1696df6 --- /dev/null +++ b/EgwProxy.Shelly/app.config @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly/docfx.json b/EgwProxy.Shelly/docfx.json new file mode 100644 index 0000000..435e349 --- /dev/null +++ b/EgwProxy.Shelly/docfx.json @@ -0,0 +1,69 @@ +{ + "metadata": [ + { + "src": [ + { + "files": [ + "*.csproj", + "*.vbproj" + ], + "cwd": ".", + "exclude": [ + "**/obj/**", + "**/bin/**", + "_site/**" + ] + } + ], + "dest": "obj/api" + } + ], + "build": { + "content": [ + { + "files": [ + "api/**.yml" + ], + "cwd": "obj" + }, + { + "files": [ + "api/*.md", + "articles/**.md", + "toc.yml", + "*.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "overwrite": [ + { + "files": [ + "apidoc/**.md" + ], + "exclude": [ + "obj/**", + "_site/**" + ] + } + ], + "dest": "_site", + "template": [ + "default" + ] + } +} \ No newline at end of file diff --git a/EgwProxy.Shelly/index.md b/EgwProxy.Shelly/index.md new file mode 100644 index 0000000..63cb151 --- /dev/null +++ b/EgwProxy.Shelly/index.md @@ -0,0 +1,19 @@ +# EgwProxy.Shelly Library + +Documentazione relativa alla libreria di interfaccia con sistemi Shelly x controllo domotico e del consumo energetico. + +Disponibile in forma di pacchetto nuget sul repo aziendale nexus.steamware.net: i pacchetti sono disponibili all'indirizzo + +https://nexus.steamware.net/#browse/browse:nuget-hosted + +Vedere la sezione Articles per maggiori informazioni sulle definizioni, l'impiego ed esempi. + +L'oggetot id base è **EgwProxy.Shelly.Clients.* ** + +## Articles + +Per maggiori dettagli, definizioni e demo funzionamento si rimanda alla sezione Articles + +## Api + +Per ogni dettaglio e riferimento alla libreria si rimanda alla sezione Api Documentation \ No newline at end of file diff --git a/EgwProxy.Shelly/packages.config b/EgwProxy.Shelly/packages.config new file mode 100644 index 0000000..45a386e --- /dev/null +++ b/EgwProxy.Shelly/packages.config @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/EgwProxy.Shelly/toc.yml b/EgwProxy.Shelly/toc.yml new file mode 100644 index 0000000..7eee9ff --- /dev/null +++ b/EgwProxy.Shelly/toc.yml @@ -0,0 +1,6 @@ + +- name: Articles + href: articles/ +- name: API Documentation + href: obj/api/ + homepage: api/index.md