From ac5f2d97477cb272c4afa27176c2048cab677852 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 14 Nov 2021 19:47:41 +0100 Subject: [PATCH 01/10] Add ApiLibs as example --- .gitmodules | 3 +++ ApiLibs | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 ApiLibs diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..647a5d7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ApiLibs"] + path = ApiLibs + url = git@github.com:mjwsteenbergen/ApiLibs.git diff --git a/ApiLibs b/ApiLibs new file mode 160000 index 0000000..60b6d27 --- /dev/null +++ b/ApiLibs @@ -0,0 +1 @@ +Subproject commit 60b6d2728dc65e44de99d58b8f693e3a126e8447 From 76bbf4391dda040a631eedb38366a312b684b169 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 14 Nov 2021 19:47:54 +0100 Subject: [PATCH 02/10] Update everything --- TradFri.sln | 11 ++ Tradfri/Controllers/DeviceController.cs | 31 ++-- Tradfri/Controllers/GatewayController.cs | 5 +- Tradfri/Controllers/GroupController.cs | 23 +-- Tradfri/Controllers/SmartTaskController.cs | 2 +- Tradfri/Extensions/CoapService.cs | 133 ++++++++++++++ Tradfri/Tradfri.csproj | 3 +- Tradfri/TradfriController.cs | 191 +++++++-------------- TradfriTest/BaseTradfriTest.cs | 5 +- 9 files changed, 247 insertions(+), 157 deletions(-) create mode 100644 Tradfri/Extensions/CoapService.cs diff --git a/TradFri.sln b/TradFri.sln index 1b26595..42b57e9 100644 --- a/TradFri.sln +++ b/TradFri.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TradfriTest", "TradfriTest\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TradfriUI", "TradfriUI\TradfriUI.csproj", "{E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiLibs", "ApiLibs", "{F471E280-2363-4ABE-AD09-563873FAB754}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiLibs", "ApiLibs\ApiLibs\ApiLibs.csproj", "{DBBEC452-B161-4CC5-85DC-5E7492BE02C7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,10 @@ Global {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.Build.0 = Release|Any CPU + {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -34,4 +42,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4981C63-C624-40AA-BF7C-68D88FD08E92} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {DBBEC452-B161-4CC5-85DC-5E7492BE02C7} = {F471E280-2363-4ABE-AD09-563873FAB754} + EndGlobalSection EndGlobal diff --git a/Tradfri/Controllers/DeviceController.cs b/Tradfri/Controllers/DeviceController.cs index b2bc10e..5f4ffd6 100644 --- a/Tradfri/Controllers/DeviceController.cs +++ b/Tradfri/Controllers/DeviceController.cs @@ -1,4 +1,5 @@ using ApiLibs.General; +using ApiLibs; using Com.AugustCellars.CoAP; using Newtonsoft.Json; using System; @@ -9,7 +10,7 @@ namespace Tomidix.NetStandard.Tradfri.Controllers { - public class DeviceController : SubService + public class DeviceController : SubService { public DeviceController(TradfriController controller) : base(controller) { @@ -30,9 +31,9 @@ public Task GetTradfriDevice(long id) /// /// /// - public async Task RenameTradfriDevice(TradfriDevice device) + public Task RenameTradfriDevice(TradfriDevice device) { - RenameTradfriDevice(device.ID, device.Name); + return RenameTradfriDevice(device.ID, device.Name); } /// @@ -41,7 +42,7 @@ public async Task RenameTradfriDevice(TradfriDevice device) /// /// /// - public async Task RenameTradfriDevice(long id, string newName) + public Task RenameTradfriDevice(long id, string newName) { if (!string.IsNullOrWhiteSpace(newName)) { @@ -49,7 +50,7 @@ public async Task RenameTradfriDevice(long id, string newName) { Name = newName }; - HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + return MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); } else { @@ -108,7 +109,7 @@ public async Task SetColor(long id, string value, int? transition = null) } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); } /// @@ -156,7 +157,7 @@ public async Task SetColor(long id, int x, int y, int? intensity = null, int? tr } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); } /// @@ -203,7 +204,7 @@ public async Task SetColorHSV(long id, int hue, int saturation, int? value = nul } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); } /// @@ -238,7 +239,7 @@ public async Task SetDimmer(long id, int value, int? transition = null) } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); } /// @@ -274,7 +275,7 @@ public async Task SetLight(long id, bool state) } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -310,7 +311,7 @@ public async Task SetOutlet(long id, bool state) } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -346,7 +347,7 @@ public async Task SetBlind(long id, int position) } } }; - await HandleRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); } /// @@ -365,8 +366,10 @@ public void ObserveDevice(TradfriDevice device, Action callback) } }; - // this specific combination of parameter values is handled in TradfriController's HandleRequest for Observable - HandleRequest($"/{(int)TradfriConstRoot.Devices}/{device.ID}", Call.GET, null, null, update, HttpStatusCode.Continue); + _ = MakeRequest(new WatchRequest($"/{(int)TradfriConstRoot.Devices}/{device.ID}") + { + EventHandler = update, + }); } } internal class RenameRequest diff --git a/Tradfri/Controllers/GatewayController.cs b/Tradfri/Controllers/GatewayController.cs index b1bc00d..0134b90 100644 --- a/Tradfri/Controllers/GatewayController.cs +++ b/Tradfri/Controllers/GatewayController.cs @@ -1,4 +1,5 @@ -using ApiLibs.General; +using ApiLibs; +using ApiLibs.General; using Newtonsoft.Json; using Org.BouncyCastle.Asn1.Cms; using System.Collections.Generic; @@ -7,7 +8,7 @@ namespace Tomidix.NetStandard.Tradfri.Controllers { - public class GatewayController : SubService + public class GatewayController : SubService { private TradfriController mainController; diff --git a/Tradfri/Controllers/GroupController.cs b/Tradfri/Controllers/GroupController.cs index 380b53b..62ebbda 100644 --- a/Tradfri/Controllers/GroupController.cs +++ b/Tradfri/Controllers/GroupController.cs @@ -1,4 +1,5 @@ -using ApiLibs.General; +using ApiLibs; +using ApiLibs.General; using System; using System.Net; using System.Threading.Tasks; @@ -6,7 +7,7 @@ namespace Tomidix.NetStandard.Tradfri.Controllers { - public class GroupController : SubService + public class GroupController : SubService { //private readonly CoapClient cc; //private long id { get; } @@ -34,9 +35,9 @@ public async Task GetTradfriGroup(long id) /// /// /// - public async Task RenameTradfriGroup(TradfriGroup group) + public Task RenameTradfriGroup(TradfriGroup group) { - RenameTradfriGroup(group.ID, group.Name); + return RenameTradfriGroup(group.ID, group.Name); } /// @@ -45,7 +46,7 @@ public async Task RenameTradfriGroup(TradfriGroup group) /// /// /// - public async Task RenameTradfriGroup(long id, string newName) + public Task RenameTradfriGroup(long id, string newName) { if (!string.IsNullOrWhiteSpace(newName)) { @@ -53,7 +54,7 @@ public async Task RenameTradfriGroup(long id, string newName) { Name = newName }; - HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + return MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); } else { @@ -98,7 +99,7 @@ public async Task SetMood(long id, long moodId) IsOn = 1, Mood = moodId }; - await HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); } /// @@ -114,12 +115,12 @@ public async Task SetMood(long id, TradfriMoodProperties moodProperties) IsOn = 1, Mood = 1 //hardcoded non-existing moodId }; - await HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: moodProperties, statusCode: System.Net.HttpStatusCode.NoContent); - await HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); } /// @@ -134,7 +135,7 @@ public async Task SetDimmer(long id, int value) { LightIntensity = value }; - await HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); } /// @@ -162,7 +163,7 @@ public async Task SetLight(long id, bool state) IsOn = state ? 1 : 0 }; - await HandleRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); } } } diff --git a/Tradfri/Controllers/SmartTaskController.cs b/Tradfri/Controllers/SmartTaskController.cs index dd0fc7a..3c04762 100644 --- a/Tradfri/Controllers/SmartTaskController.cs +++ b/Tradfri/Controllers/SmartTaskController.cs @@ -6,7 +6,7 @@ namespace Tomidix.NetStandard.Tradfri.Controllers { - public class SmartTaskController : SubService + public class SmartTaskController : SubService { public SmartTaskController(TradfriController controller) : base(controller) { diff --git a/Tradfri/Extensions/CoapService.cs b/Tradfri/Extensions/CoapService.cs new file mode 100644 index 0000000..0e837fd --- /dev/null +++ b/Tradfri/Extensions/CoapService.cs @@ -0,0 +1,133 @@ +using System; +using System.Threading.Tasks; +using ApiLibs; +using Com.AugustCellars.CoAP; +using Com.AugustCellars.CoAP.DTLS; +using Newtonsoft.Json; + +namespace Tomidix.NetStandard.Tradfri.Extensions +{ + public class CoapImplementation : ICallImplementation + { + internal CoapClient _coapClient; + + + public override async Task ExecuteRequest(Service service2, ApiLibs.Request request) + { + var coapRequest = new Com.AugustCellars.CoAP.Request(ConvertToMethod(request.Method)) + { + UriPath = request.EndPoint + }; + + if (request.Content != null) + { + coapRequest.SetPayload(AddBody(request.Content)); + } + + // this is done on purpose to handle ObserveDevice from DeviceController + if (request is WatchRequest watch) + { + coapRequest.MarkObserve(); + coapRequest.Respond += (object sender, ResponseEventArgs e) => + { + watch.EventHandler?.Invoke(coapRequest.Response); + }; + } + + if(request is EndPointRequest a) + { + coapRequest.EndPoint = a.DTLSEndPoint; + } + + Task requestTask = new Task(() => + { + return _coapClient.Send(coapRequest); + }); + + requestTask.Start(); + + Response resp = await requestTask; + + var responseApiLibs = new RequestResponse((System.Net.HttpStatusCode)MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp, request, service2); + + if (resp.IsTimedOut) + { + throw new RequestTimeoutResponse(responseApiLibs).ToException(); + } + + return responseApiLibs; + } + + private static string AddBody(object content) + { + return content switch + { + string text => text, + var randomObject => JsonConvert.SerializeObject(randomObject, new JsonSerializerSettings() + { + NullValueHandling = NullValueHandling.Ignore + }) + }; + } + + private int MapToHttpStatusCode(StatusCode statusCode) => + statusCode switch + { + StatusCode.Created => 201, + StatusCode.Deleted => 204, + StatusCode.Valid => 200, + StatusCode.Changed => 200, + StatusCode.Content => 200, + StatusCode.BadRequest => 400, + StatusCode.BadOption => 400, + StatusCode.Forbidden => 403, + StatusCode.NotFound => 404, + StatusCode.MethodNotAllowed => 405, + StatusCode.NotAcceptable => 406, + StatusCode.PreconditionFailed => 412, + StatusCode.RequestEntityTooLarge => 413, + StatusCode.UnsupportedMediaType => 414, + StatusCode.InternalServerError => 500, + StatusCode.NotImplemented => 501, + StatusCode.ServiceUnavailable => 503, + StatusCode.GatewayTimeout => 504, + StatusCode.ProxyingNotSupported => 500, + _ => 400, + }; + + private Method ConvertToMethod(Call call) + { + switch (call) + { + case Call.GET: + return Method.GET; + case Call.POST: + return Method.POST; + case Call.DELETE: + return Method.DELETE; + case Call.PUT: + return Method.PUT; + default: + throw new System.NotSupportedException("This is not supported for coap"); + } + } + } + + public class WatchRequest : ApiLibs.Request + { + public WatchRequest(string endPoint) : base(endPoint) + { + } + + public Action EventHandler { get; internal set; } + } + + public class EndPointRequest : Request + { + public EndPointRequest(string endPoint) : base(endPoint) + { + } + + public DTLSClientEndPoint DTLSEndPoint { get; set; } + } +} \ No newline at end of file diff --git a/Tradfri/Tradfri.csproj b/Tradfri/Tradfri.csproj index 70826eb..48f1d19 100644 --- a/Tradfri/Tradfri.csproj +++ b/Tradfri/Tradfri.csproj @@ -3,6 +3,7 @@ netstandard2.0 CSharpTradfriLibrary + 8.0 Tomidix.CSharpTradFriLibrary https://github.com/tomidix/CSharpTradFriLibrary https://github.com/tomidix/CSharpTradFriLibrary/blob/master/LICENSE @@ -21,7 +22,7 @@ - + diff --git a/Tradfri/TradfriController.cs b/Tradfri/TradfriController.cs index 8ce979e..61c7d68 100644 --- a/Tradfri/TradfriController.cs +++ b/Tradfri/TradfriController.cs @@ -12,6 +12,7 @@ using System.Text; using System.Threading.Tasks; using Tomidix.NetStandard.Tradfri.Controllers; +using Tomidix.NetStandard.Tradfri.Extensions; using Tomidix.NetStandard.Tradfri.Models; using Method = Com.AugustCellars.CoAP.Method; @@ -29,7 +30,7 @@ public class TradfriController : Service public string GateWayName { get; } - public TradfriController(string gatewayName, string gatewayIp) : base("https://www.ikea.com/") //Ignore this + public TradfriController(string gatewayName, string gatewayIp) : base(new CoapImplementation()) //Implementation of the COAP model { this.GateWayName = gatewayName; _gatewayIp = gatewayIp; @@ -53,14 +54,13 @@ public void ConnectPSK(string GatewaySecret) authKey.Add(CoseKeyParameterKeys.Octet_k, CBORObject.FromObject(Encoding.UTF8.GetBytes(GatewaySecret))); DTLSClientEndPoint ep = new DTLSClientEndPoint(authKey); - CoapClient cc = new CoapClient(new Uri($"coaps://{_gatewayIp}")) + + (Implementation as CoapImplementation)._coapClient = new CoapClient(new Uri($"coaps://{_gatewayIp}")) { EndPoint = ep }; ep.Start(); - - _coapClient = cc; } public void ConnectAppKey(string appKey, string applicationName) @@ -71,58 +71,56 @@ public void ConnectAppKey(string appKey, string applicationName) authKey.Add(CoseKeyKeys.KeyIdentifier, CBORObject.FromObject(Encoding.UTF8.GetBytes(applicationName))); DTLSClientEndPoint ep = new DTLSClientEndPoint(authKey); - CoapClient cc = new CoapClient(new Uri($"coaps://{_gatewayIp}")) + (Implementation as CoapImplementation)._coapClient = new CoapClient(new Uri($"coaps://{_gatewayIp}")) { EndPoint = ep }; ep.Start(); - - _coapClient = cc; } //This is the interface of the entire library. Every request that is made outside of this class will use this method to communicate. - protected override async Task HandleRequest(string url, Call call = Call.GET, List parameters = null, List headers = null, object content = null, HttpStatusCode statusCode = HttpStatusCode.OK) - { - Request request = new Request(ConvertToMethod(call)); - request.UriPath = url; - - // this is done on purpose to handle ObserveDevice from DeviceController - if (statusCode.Equals(HttpStatusCode.Continue)) - { - request.MarkObserve(); - request.Respond += (Object sender, ResponseEventArgs e) => - { - ((Action)content).Invoke(request.Response); - }; - } - - if (content != null && !statusCode.Equals(HttpStatusCode.Continue)) - { - JsonSerializerSettings settings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore - }; - - request.SetPayload(JsonConvert.SerializeObject(content, settings)); - } - - Task t = new Task(() => - { - return _coapClient.Send(request); - }); - - t.Start(); - - Response resp = await t; - - if (MapToHttpStatusCode(resp.StatusCode) != (int)statusCode) - { - RequestException.ConvertToException(MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp); - } - - return resp.ResponseText; - } + // protected override async Task HandleRequest(string url, Call call = Call.GET, List parameters = null, List headers = null, object content = null, HttpStatusCode statusCode = HttpStatusCode.OK) + // { + // Request request = new Request(ConvertToMethod(call)); + // request.UriPath = url; + + // // this is done on purpose to handle ObserveDevice from DeviceController + // if (statusCode.Equals(HttpStatusCode.Continue)) + // { + // request.MarkObserve(); + // request.Respond += (Object sender, ResponseEventArgs e) => + // { + // ((Action)content).Invoke(request.Response); + // }; + // } + + // if (content != null && !statusCode.Equals(HttpStatusCode.Continue)) + // { + // JsonSerializerSettings settings = new JsonSerializerSettings + // { + // NullValueHandling = NullValueHandling.Ignore + // }; + + // request.SetPayload(JsonConvert.SerializeObject(content, settings)); + // } + + // Task t = new Task(() => + // { + // return _coapClient.Send(request); + // }); + + // t.Start(); + + // Response resp = await t; + + // if (MapToHttpStatusCode(resp.StatusCode) != (int)statusCode) + // { + // RequestException.ConvertToException(MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp); + // } + + // return resp.ResponseText; + // } private Method ConvertToMethod(Call call) { @@ -141,92 +139,33 @@ private Method ConvertToMethod(Call call) } } - /// - /// Acquire All Resources - /// - /// - public List GetResources() - { - return _coapClient.Discover().ToList(); - } + // /// + // /// Acquire All Resources + // /// + // /// + // public List GetResources() + // { + // return _coapClient.Discover().ToList(); + // } - public TradfriAuth GenerateAppSecret(string GatewaySecret, string applicationName) + public Task GenerateAppSecret(string GatewaySecret, string applicationName) { - Response resp = new Response(StatusCode.Valid); - OneKey authKey = new OneKey(); authKey.Add(CoseKeyKeys.KeyType, GeneralValues.KeyType_Octet); authKey.Add(CoseKeyParameterKeys.Octet_k, CBORObject.FromObject(Encoding.UTF8.GetBytes(GatewaySecret))); authKey.Add(CoseKeyKeys.KeyIdentifier, CBORObject.FromObject(Encoding.UTF8.GetBytes("Client_identity"))); - DTLSClientEndPoint ep = new DTLSClientEndPoint(authKey); - ep.Start(); - Request r = new Request(Method.POST); - r.SetUri($"coaps://{_gatewayIp}" + $"/{(int)TradfriConstRoot.Gateway}/{(int)TradfriConstAttr.Auth}/"); - r.EndPoint = ep; - r.AckTimeout = 333; // 10sec - r.SetPayload($@"{{""{(int)TradfriConstAttr.Identity}"":""{applicationName}""}}"); - r.Send(); - resp = r.WaitForResponse(); - if (r.IsTimedOut) - throw new Exception("Timeout Generating App Secret."); - - if ((int)resp.StatusCode != 201) + return MakeRequest(new EndPointRequest($"/{(int)TradfriConstRoot.Gateway}/{(int)TradfriConstAttr.Auth}/") { - RequestException.ConvertToException(MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp); - } - - return Convert(resp.PayloadString); - } - - private int MapToHttpStatusCode(StatusCode statusCode) - { - switch (statusCode) - { - case StatusCode.Created: - return 201; - case StatusCode.Deleted: - return 204; - case StatusCode.Valid: - return 200; - case StatusCode.Changed: - return 200; - case StatusCode.Content: - return 200; - - case StatusCode.BadRequest: - return 400; - case StatusCode.BadOption: - return 400; - case StatusCode.Forbidden: - return 403; - case StatusCode.NotFound: - return 404; - case StatusCode.MethodNotAllowed: - return 405; - case StatusCode.NotAcceptable: - return 406; - case StatusCode.PreconditionFailed: - return 412; - case StatusCode.RequestEntityTooLarge: - return 413; - case StatusCode.UnsupportedMediaType: - return 414; - - case StatusCode.InternalServerError: - return 500; - case StatusCode.NotImplemented: - return 501; - case StatusCode.ServiceUnavailable: - return 503; - case StatusCode.GatewayTimeout: - return 504; - case StatusCode.ProxyingNotSupported: - return 500; - - default: - return 400; - } + Content = $"{{\"{(int)TradfriConstAttr.Identity}\":\"{applicationName}\"}}", + Method = Call.POST, + DTLSEndPoint = new DTLSClientEndPoint(authKey), + RequestHandler = (resp) => resp switch + { + CreatedResponse crea => crea.Convert(), + var other => throw other.ToException() + } + }); } } } diff --git a/TradfriTest/BaseTradfriTest.cs b/TradfriTest/BaseTradfriTest.cs index dbe3fc1..f0ab495 100644 --- a/TradfriTest/BaseTradfriTest.cs +++ b/TradfriTest/BaseTradfriTest.cs @@ -1,3 +1,4 @@ +using System.Threading.Tasks; using NUnit.Framework; using Tomidix.NetStandard.Tradfri; using Tomidix.NetStandard.Tradfri.Models; @@ -17,7 +18,7 @@ public virtual void BaseSetup() } // Real usage example - public virtual void BaseSetupRecommendation() + public virtual async Task BaseSetupRecommendation() { // Unique name for the application which communicates with Tradfri gateway string applicationName = "UnitTestApp"; @@ -25,7 +26,7 @@ public virtual void BaseSetupRecommendation() // This line should only be called once per applicationName // Gateway generates one appSecret key per applicationName - TradfriAuth appSecret = controller.GenerateAppSecret("PSK", applicationName); + TradfriAuth appSecret = await controller.GenerateAppSecret("PSK", applicationName); // You should now save programatically appSecret.PSK value and use it // when connection to your gateway every other time From 4ff4e5d496f7a32c0166a8f2a5493a66e7db43d9 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Wed, 12 Jan 2022 21:26:25 +0100 Subject: [PATCH 03/10] Purge NoContent. Move to OK --- ApiLibs | 2 +- Tradfri/Controllers/DeviceController.cs | 22 +++++++++++---------- Tradfri/Controllers/GroupController.cs | 11 +++++------ Tradfri/Extensions/CoapService.cs | 26 ++++++++++++------------- 4 files changed, 31 insertions(+), 30 deletions(-) diff --git a/ApiLibs b/ApiLibs index 60b6d27..c675837 160000 --- a/ApiLibs +++ b/ApiLibs @@ -1 +1 @@ -Subproject commit 60b6d2728dc65e44de99d58b8f693e3a126e8447 +Subproject commit c675837dbf1d3aba93758483099e062931ba926f diff --git a/Tradfri/Controllers/DeviceController.cs b/Tradfri/Controllers/DeviceController.cs index 5f4ffd6..b0d7df6 100644 --- a/Tradfri/Controllers/DeviceController.cs +++ b/Tradfri/Controllers/DeviceController.cs @@ -50,7 +50,7 @@ public Task RenameTradfriDevice(long id, string newName) { Name = newName }; - return MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + return MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } else { @@ -109,7 +109,7 @@ public async Task SetColor(long id, string value, int? transition = null) } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -157,7 +157,7 @@ public async Task SetColor(long id, int x, int y, int? intensity = null, int? tr } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -204,7 +204,7 @@ public async Task SetColorHSV(long id, int hue, int saturation, int? value = nul } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -239,7 +239,7 @@ public async Task SetDimmer(long id, int value, int? transition = null) } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -263,7 +263,7 @@ public async Task SetLight(TradfriDevice device, bool state) /// Id of the device /// On (True) or Off(false) /// - public async Task SetLight(long id, bool state) + public Task SetLight(long id, bool state) { SwitchStateLightRequest set = new SwitchStateLightRequest() { @@ -275,7 +275,7 @@ public async Task SetLight(long id, bool state) } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); + return MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -299,7 +299,7 @@ public async Task SetOutlet(TradfriDevice device, bool state) /// Id of the device /// On (True) or Off (false) /// - public async Task SetOutlet(long id, bool state) + public Task SetOutlet(long id, bool state) { SwitchStateOutletRequest set = new SwitchStateOutletRequest() { @@ -311,7 +311,7 @@ public async Task SetOutlet(long id, bool state) } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); + return MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -347,7 +347,7 @@ public async Task SetBlind(long id, int position) } } }; - await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Devices}/{id}", Call.PUT, content: set); } /// @@ -369,6 +369,8 @@ public void ObserveDevice(TradfriDevice device, Action callback) _ = MakeRequest(new WatchRequest($"/{(int)TradfriConstRoot.Devices}/{device.ID}") { EventHandler = update, + RequestHandler = (resp) => { }, + ExpectedStatusCode = System.Net.HttpStatusCode.OK }); } } diff --git a/Tradfri/Controllers/GroupController.cs b/Tradfri/Controllers/GroupController.cs index 62ebbda..dc39c21 100644 --- a/Tradfri/Controllers/GroupController.cs +++ b/Tradfri/Controllers/GroupController.cs @@ -54,7 +54,7 @@ public Task RenameTradfriGroup(long id, string newName) { Name = newName }; - return MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: HttpStatusCode.NoContent); + return MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); } else { @@ -99,7 +99,7 @@ public async Task SetMood(long id, long moodId) IsOn = 1, Mood = moodId }; - await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); } /// @@ -117,10 +117,9 @@ public async Task SetMood(long id, TradfriMoodProperties moodProperties) }; await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, - content: moodProperties, - statusCode: System.Net.HttpStatusCode.NoContent); + content: moodProperties); - await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); } /// @@ -135,7 +134,7 @@ public async Task SetDimmer(long id, int value) { LightIntensity = value }; - await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set, statusCode: System.Net.HttpStatusCode.NoContent); + await MakeRequest($"/{(int)TradfriConstRoot.Groups}/{id}", Call.PUT, content: set); } /// diff --git a/Tradfri/Extensions/CoapService.cs b/Tradfri/Extensions/CoapService.cs index 0e837fd..d513474 100644 --- a/Tradfri/Extensions/CoapService.cs +++ b/Tradfri/Extensions/CoapService.cs @@ -48,6 +48,11 @@ public override async Task ExecuteRequest(Service service2, Api Response resp = await requestTask; + if(resp == null) + { + throw new Exception("Request timed out"); + } + var responseApiLibs = new RequestResponse((System.Net.HttpStatusCode)MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp, request, service2); if (resp.IsTimedOut) @@ -74,7 +79,7 @@ private int MapToHttpStatusCode(StatusCode statusCode) => statusCode switch { StatusCode.Created => 201, - StatusCode.Deleted => 204, + StatusCode.Deleted => 200, StatusCode.Valid => 200, StatusCode.Changed => 200, StatusCode.Content => 200, @@ -97,19 +102,14 @@ private int MapToHttpStatusCode(StatusCode statusCode) => private Method ConvertToMethod(Call call) { - switch (call) + return call switch { - case Call.GET: - return Method.GET; - case Call.POST: - return Method.POST; - case Call.DELETE: - return Method.DELETE; - case Call.PUT: - return Method.PUT; - default: - throw new System.NotSupportedException("This is not supported for coap"); - } + Call.GET => Method.GET, + Call.POST => Method.POST, + Call.DELETE => Method.DELETE, + Call.PUT => Method.PUT, + _ => throw new System.NotSupportedException("This is not supported for coap"), + }; } } From 63edc2cdf2ced6d59575670b2e2a789b72d9ba58 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 6 Feb 2022 12:44:17 +0100 Subject: [PATCH 04/10] Cleaning up --- Tradfri/TradfriController.cs | 52 ------------------------------------ 1 file changed, 52 deletions(-) diff --git a/Tradfri/TradfriController.cs b/Tradfri/TradfriController.cs index 61c7d68..00e2bb9 100644 --- a/Tradfri/TradfriController.cs +++ b/Tradfri/TradfriController.cs @@ -79,49 +79,6 @@ public void ConnectAppKey(string appKey, string applicationName) ep.Start(); } - //This is the interface of the entire library. Every request that is made outside of this class will use this method to communicate. - // protected override async Task HandleRequest(string url, Call call = Call.GET, List parameters = null, List headers = null, object content = null, HttpStatusCode statusCode = HttpStatusCode.OK) - // { - // Request request = new Request(ConvertToMethod(call)); - // request.UriPath = url; - - // // this is done on purpose to handle ObserveDevice from DeviceController - // if (statusCode.Equals(HttpStatusCode.Continue)) - // { - // request.MarkObserve(); - // request.Respond += (Object sender, ResponseEventArgs e) => - // { - // ((Action)content).Invoke(request.Response); - // }; - // } - - // if (content != null && !statusCode.Equals(HttpStatusCode.Continue)) - // { - // JsonSerializerSettings settings = new JsonSerializerSettings - // { - // NullValueHandling = NullValueHandling.Ignore - // }; - - // request.SetPayload(JsonConvert.SerializeObject(content, settings)); - // } - - // Task t = new Task(() => - // { - // return _coapClient.Send(request); - // }); - - // t.Start(); - - // Response resp = await t; - - // if (MapToHttpStatusCode(resp.StatusCode) != (int)statusCode) - // { - // RequestException.ConvertToException(MapToHttpStatusCode(resp.StatusCode), resp.StatusCode.ToString(), resp.UriQuery, "", resp.ResponseText, resp); - // } - - // return resp.ResponseText; - // } - private Method ConvertToMethod(Call call) { switch (call) @@ -139,15 +96,6 @@ private Method ConvertToMethod(Call call) } } - // /// - // /// Acquire All Resources - // /// - // /// - // public List GetResources() - // { - // return _coapClient.Discover().ToList(); - // } - public Task GenerateAppSecret(string GatewaySecret, string applicationName) { OneKey authKey = new OneKey(); From 4d462301c16e194205e8005c3804772af687ed88 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 6 Feb 2022 12:44:31 +0100 Subject: [PATCH 05/10] Remove submodule --- ApiLibs | 1 - Tradfri/Tradfri.csproj | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 160000 ApiLibs diff --git a/ApiLibs b/ApiLibs deleted file mode 160000 index c675837..0000000 --- a/ApiLibs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c675837dbf1d3aba93758483099e062931ba926f diff --git a/Tradfri/Tradfri.csproj b/Tradfri/Tradfri.csproj index 861d256..f66e144 100644 --- a/Tradfri/Tradfri.csproj +++ b/Tradfri/Tradfri.csproj @@ -22,10 +22,8 @@ - + - - From d2cb463d5e8e45646b6fc5e997e1607982b3d067 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 6 Feb 2022 12:45:57 +0100 Subject: [PATCH 06/10] Remove submodule file --- .gitmodules | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .gitmodules diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 647a5d7..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "ApiLibs"] - path = ApiLibs - url = git@github.com:mjwsteenbergen/ApiLibs.git From 19a66dfaa96a77792dcb5bc70f2306944e4724b2 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sun, 6 Feb 2022 12:47:28 +0100 Subject: [PATCH 07/10] Remove from sln --- TradFri.sln | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/TradFri.sln b/TradFri.sln index 42b57e9..1b26595 100644 --- a/TradFri.sln +++ b/TradFri.sln @@ -9,10 +9,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TradfriTest", "TradfriTest\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TradfriUI", "TradfriUI\TradfriUI.csproj", "{E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiLibs", "ApiLibs", "{F471E280-2363-4ABE-AD09-563873FAB754}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiLibs", "ApiLibs\ApiLibs\ApiLibs.csproj", "{DBBEC452-B161-4CC5-85DC-5E7492BE02C7}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -31,10 +27,6 @@ Global {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.Build.0 = Release|Any CPU - {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DBBEC452-B161-4CC5-85DC-5E7492BE02C7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -42,7 +34,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4981C63-C624-40AA-BF7C-68D88FD08E92} EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {DBBEC452-B161-4CC5-85DC-5E7492BE02C7} = {F471E280-2363-4ABE-AD09-563873FAB754} - EndGlobalSection EndGlobal From b12e0b1924d115b95bf2a10e22f21711afd9bb7e Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Fri, 30 Dec 2022 16:06:59 +0100 Subject: [PATCH 08/10] Start `DTLSClientEndPoint` --- Tradfri/TradfriController.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tradfri/TradfriController.cs b/Tradfri/TradfriController.cs index 00e2bb9..34b2737 100644 --- a/Tradfri/TradfriController.cs +++ b/Tradfri/TradfriController.cs @@ -103,11 +103,14 @@ public Task GenerateAppSecret(string GatewaySecret, string applicat authKey.Add(CoseKeyParameterKeys.Octet_k, CBORObject.FromObject(Encoding.UTF8.GetBytes(GatewaySecret))); authKey.Add(CoseKeyKeys.KeyIdentifier, CBORObject.FromObject(Encoding.UTF8.GetBytes("Client_identity"))); + var ep = new DTLSClientEndPoint(authKey); + ep.Start(); + return MakeRequest(new EndPointRequest($"/{(int)TradfriConstRoot.Gateway}/{(int)TradfriConstAttr.Auth}/") { Content = $"{{\"{(int)TradfriConstAttr.Identity}\":\"{applicationName}\"}}", Method = Call.POST, - DTLSEndPoint = new DTLSClientEndPoint(authKey), + DTLSEndPoint = ep, RequestHandler = (resp) => resp switch { CreatedResponse crea => crea.Convert(), From 7be08fe76c3a251c08b29ace3151885adcab82c5 Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Wed, 19 Jun 2024 10:09:07 +0200 Subject: [PATCH 09/10] add cancelling to observation --- Tradfri/Controllers/DeviceController.cs | 8 ++++---- Tradfri/Extensions/CoapService.cs | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Tradfri/Controllers/DeviceController.cs b/Tradfri/Controllers/DeviceController.cs index b0d7df6..baa7b31 100644 --- a/Tradfri/Controllers/DeviceController.cs +++ b/Tradfri/Controllers/DeviceController.cs @@ -355,18 +355,18 @@ public async Task SetBlind(long id, int position) /// /// Device on which you want to be notified /// Action to take for each device update - public void ObserveDevice(TradfriDevice device, Action callback) + public void ObserveDevice(TradfriDevice device, Action callback) { - Action update = (Response response) => + Action update = (Response response, Action cancel) => { if (!string.IsNullOrWhiteSpace(response?.PayloadString)) { device = JsonConvert.DeserializeObject(response.PayloadString); - callback.Invoke(device); + callback.Invoke(device, cancel); } }; - _ = MakeRequest(new WatchRequest($"/{(int)TradfriConstRoot.Devices}/{device.ID}") + MakeRequest(new WatchRequest($"/{(int)TradfriConstRoot.Devices}/{device.ID}") { EventHandler = update, RequestHandler = (resp) => { }, diff --git a/Tradfri/Extensions/CoapService.cs b/Tradfri/Extensions/CoapService.cs index d513474..608b261 100644 --- a/Tradfri/Extensions/CoapService.cs +++ b/Tradfri/Extensions/CoapService.cs @@ -28,9 +28,10 @@ public override async Task ExecuteRequest(Service service2, Api if (request is WatchRequest watch) { coapRequest.MarkObserve(); + Action cancelWatch = () => coapRequest.MarkObserveCancel(); coapRequest.Respond += (object sender, ResponseEventArgs e) => { - watch.EventHandler?.Invoke(coapRequest.Response); + watch.EventHandler?.Invoke(coapRequest.Response, cancelWatch); }; } @@ -119,7 +120,7 @@ public WatchRequest(string endPoint) : base(endPoint) { } - public Action EventHandler { get; internal set; } + public Action EventHandler { get; internal set; } } public class EndPointRequest : Request From f1a585411df01985ede29186c32262f92e9cc3ca Mon Sep 17 00:00:00 2001 From: Martijn Steenbergen Date: Sat, 6 Jul 2024 20:57:27 +0200 Subject: [PATCH 10/10] Add Dirigera --- Dirigera/DeviceController.cs | 101 +++++++++++ Dirigera/Devices/Device.cs | 154 +++++++++++++++++ Dirigera/Devices/EnvironmentSensor.cs | 58 +++++++ Dirigera/Devices/Gateway.cs | 80 +++++++++ Dirigera/Devices/Light.cs | 66 +++++++ Dirigera/Dirigera.csproj | 12 ++ Dirigera/DirigeraController.cs | 87 ++++++++++ Dirigera/UserController.cs | 47 +++++ TradFri.sln | 12 ++ TradfriTerminalUI/MainLoop.cs | 143 +++++++++++++++ TradfriTerminalUI/Program.cs | 192 +++++++++++++++++++++ TradfriTerminalUI/TradfriTerminalUI.csproj | 22 +++ 12 files changed, 974 insertions(+) create mode 100644 Dirigera/DeviceController.cs create mode 100644 Dirigera/Devices/Device.cs create mode 100644 Dirigera/Devices/EnvironmentSensor.cs create mode 100644 Dirigera/Devices/Gateway.cs create mode 100644 Dirigera/Devices/Light.cs create mode 100644 Dirigera/Dirigera.csproj create mode 100644 Dirigera/DirigeraController.cs create mode 100644 Dirigera/UserController.cs create mode 100644 TradfriTerminalUI/MainLoop.cs create mode 100644 TradfriTerminalUI/Program.cs create mode 100644 TradfriTerminalUI/TradfriTerminalUI.csproj diff --git a/Dirigera/DeviceController.cs b/Dirigera/DeviceController.cs new file mode 100644 index 0000000..fe97b92 --- /dev/null +++ b/Dirigera/DeviceController.cs @@ -0,0 +1,101 @@ +using ApiLibs.General; +using Tomidix.NetStandard.Dirigera.Devices; +using Newtonsoft.Json; +using ApiLibs; + +namespace Tomidix.NetStandard.Dirigera; + +public class DeviceController : SubService +{ + private Service service; + public DeviceController(DirigeraController controller) : base(controller) + { + service = controller; + } + + public Task> GetDevices() => MakeRequest>("devices/").ContinueWith((item) => item.Result.Select(i => + { + i.service = service; + return i; + }).ToList()); + public Task GetDevicesJson() => MakeRequest("devices/"); + + public Task ChangeAttributes(string deviceId, PostingAttributes attributes) where T : PostingAttributesProperties => MakeRequest("devices/" + deviceId, Call.PATCH, content: new object[] { attributes }, statusCode: System.Net.HttpStatusCode.Accepted); + + public Task Toggle(Light l) => Toggle(l.Id, !l.Attributes.IsOn).ContinueWith((a) => + { + l.Attributes.IsOn = !l.Attributes.IsOn; + return a.Result; + }); + + public Task Toggle(string id, bool isOn) => ChangeAttributes(id, new PostingAttributes(new ToggleProperty + { + IsOn = isOn + })); + + public Task SetLightLevel(Light l, int level) => SetLightLevel(l.Id, level).ContinueWith((a) => + { + l.Attributes.LightLevel = level; + return a.Result; + }); + + public Task SetLightLevel(string id, int level) => ChangeAttributes(id, new PostingAttributes(new LightLevelProperty + { + LightLevel = level + })); + + + public Task SetLightTemperature(Light l, int temperature) => Toggle(l.Id, !l.Attributes.IsOn).ContinueWith((a) => + { + l.Attributes.ColorTemperature = temperature; + return a.Result; + }); + + public Task SetLightTemperature(string id, int temperature) => ChangeAttributes(id, new PostingAttributes(new LightTemperatureProperty + { + ColorTemperature = temperature + })); + +} + +public class PostingAttributes where T : PostingAttributesProperties +{ + public PostingAttributes(T properties, int? transitionTime = null) + { + // TransitionTime = transitionTime; + Attributes = properties; + } + + [JsonProperty("attributes")] + public T Attributes { get; set; } + + + [JsonProperty("transitionTime")] + public int? TransitionTime { get; set; } +} + +public interface PostingAttributesProperties +{ + +} + +public class ToggleProperty : PostingAttributesProperties +{ + [JsonProperty("isOn")] + public required bool IsOn { get; set; } +} + + +public class LightLevelProperty : PostingAttributesProperties +{ + [JsonProperty("lightLevel")] + public required int LightLevel { get; set; } +} + +public class LightTemperatureProperty : PostingAttributesProperties +{ + [JsonProperty("colorTemperature")] + public required int ColorTemperature { get; set; } +} + + diff --git a/Dirigera/Devices/Device.cs b/Dirigera/Devices/Device.cs new file mode 100644 index 0000000..8b0a293 --- /dev/null +++ b/Dirigera/Devices/Device.cs @@ -0,0 +1,154 @@ +using System.Reflection; +using ApiLibs.General; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Tomidix.NetStandard.Dirigera.Devices; + +public class DeviceConverter : JsonConverter + +{ + public override Device ReadJson(JsonReader reader, Type objectType, Device existingValue, bool hasExistingValue, JsonSerializer serializer) + { + JToken jObject = JToken.ReadFrom(reader); + + string type = null; + + try + { + if (jObject.Type is not JTokenType.None and not JTokenType.Null) + { + type = jObject["deviceType"].ToObject(); + } + } + catch { } + + Device result = type switch + { + "environmentSensor" => new EnvironmentSensor(), + "gateway" => new Gateway(), + "light" => new Light(), + _ => throw new ArgumentOutOfRangeException("Cannot convert type " + type + jObject.ToString()) + }; + + + serializer.Populate(jObject.CreateReader(), result); + return result; + } + + public override bool CanWrite => true; + + public void Serialize(PropertyInfo info, Device value, JsonWriter writer) + { + var val = info.GetValue(value); + + if (val != null) + { + var customAttributes = (JsonPropertyAttribute[])info.GetCustomAttributes(typeof(JsonPropertyAttribute), true); + if (customAttributes.Length > 0) + { + var myAttribute = customAttributes[0]; + string propName = myAttribute.PropertyName; + + if (!string.IsNullOrEmpty(propName)) + { + writer.WritePropertyName(propName); + } + else + { + writer.WritePropertyName(info.Name); + } + // TODO: Do something with the value + } + else + { + writer.WritePropertyName(info.Name); + } + + writer.WriteRawValue(JsonConvert.SerializeObject(val, Formatting.None, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + })); + } + } + + public override void WriteJson(JsonWriter writer, Device value, JsonSerializer serializer) + { + throw new System.NotImplementedException(); + } +} + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + +[JsonConverter(typeof(DeviceConverter))] +public class Device : ObjectSearcher +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("type")] + public string Type { get; set; } + + [JsonProperty("deviceType")] + public string DeviceType { get; set; } + + [JsonProperty("createdAt")] + public string CreatedAt { get; set; } + + [JsonProperty("isReachable")] + public bool IsReachable { get; set; } + + [JsonProperty("lastSeen")] + public string LastSeen { get; set; } +} + +public partial class Attributes +{ + [JsonProperty("customName")] + public string CustomName { get; set; } + + [JsonProperty("model")] + public string Model { get; set; } + + [JsonProperty("manufacturer")] + public string Manufacturer { get; set; } + + [JsonProperty("firmwareVersion")] + public string FirmwareVersion { get; set; } + + [JsonProperty("hardwareVersion")] + public string HardwareVersion { get; set; } + + [JsonProperty("serialNumber")] + public string SerialNumber { get; set; } + + [JsonProperty("productCode")] + public string ProductCode { get; set; } + + [JsonProperty("identifyPeriod")] + public long IdentifyPeriod { get; set; } + + [JsonProperty("identifyStarted")] + public DateTimeOffset IdentifyStarted { get; set; } + + [JsonProperty("permittingJoin")] + public bool PermittingJoin { get; set; } + + [JsonProperty("otaPolicy")] + public string OtaPolicy { get; set; } + + [JsonProperty("otaProgress")] + public long OtaProgress { get; set; } + + [JsonProperty("otaScheduleEnd")] + public string OtaScheduleEnd { get; set; } + + [JsonProperty("otaScheduleStart")] + public string OtaScheduleStart { get; set; } + + [JsonProperty("otaState")] + public string OtaState { get; set; } + + [JsonProperty("otaStatus")] + public string OtaStatus { get; set; } +} \ No newline at end of file diff --git a/Dirigera/Devices/EnvironmentSensor.cs b/Dirigera/Devices/EnvironmentSensor.cs new file mode 100644 index 0000000..a3566ab --- /dev/null +++ b/Dirigera/Devices/EnvironmentSensor.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using ApiLibs.General; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Tomidix.NetStandard.Dirigera.Devices; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + +public class EnvironmentSensor : Device +{ + [JsonProperty("attributes")] + public EnvironmentSensorAttributes Attributes { get; set; } + + [JsonProperty("room")] + public Room Room { get; set; } + + public override string ToString() + { + return Attributes.CustomName; + } +} + +public partial class Room +{ + [JsonProperty("id")] + public string Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("color")] + public string Color { get; set; } + + [JsonProperty("icon")] + public string Icon { get; set; } +} + +public class EnvironmentSensorAttributes : Attributes +{ + [JsonProperty("currentTemperature")] + public int CurrentTemperature { get; set; } + + [JsonProperty("currentRH")] + public int CurrentRH { get; set; } + + [JsonProperty("currentPM25")] + public int CurrentPM25 { get; set; } + + [JsonProperty("maxMeasuredPM25")] + public int MaxMeasuredPM25 { get; set; } + + [JsonProperty("minMeasuredPM25")] + public int MinMeasuredPM25 { get; set; } + + [JsonProperty("vocIndex")] + public int VocIndex { get; set; } +} \ No newline at end of file diff --git a/Dirigera/Devices/Gateway.cs b/Dirigera/Devices/Gateway.cs new file mode 100644 index 0000000..396c324 --- /dev/null +++ b/Dirigera/Devices/Gateway.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; + +namespace Tomidix.NetStandard.Dirigera.Devices; +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + +public class Gateway : Device +{ + [JsonProperty("relationId")] + public string RelationId { get; set; } + + [JsonProperty("deviceSet")] + public object[] DeviceSet { get; set; } + + [JsonProperty("remoteLinks")] + public object[] RemoteLinks { get; set; } + + [JsonProperty("attributes")] + public GatewayAttributes Attributes { get; set; } + + public override string ToString() + { + return Attributes.CustomName; + } +} + +public partial class GatewayAttributes : Attributes +{ + [JsonProperty("backendConnected")] + public bool BackendConnected { get; set; } + + [JsonProperty("backendConnectionPersistent")] + public bool BackendConnectionPersistent { get; set; } + + [JsonProperty("backendOnboardingComplete")] + public bool BackendOnboardingComplete { get; set; } + + [JsonProperty("backendRegion")] + public string BackendRegion { get; set; } + + [JsonProperty("backendCountryCode")] + public string BackendCountryCode { get; set; } + + [JsonProperty("userConsents")] + public UserConsent[] UserConsents { get; set; } + + [JsonProperty("logLevel")] + public long LogLevel { get; set; } + + [JsonProperty("coredump")] + public bool Coredump { get; set; } + + [JsonProperty("timezone")] + public string Timezone { get; set; } + + [JsonProperty("nextSunSet")] + public object NextSunSet { get; set; } + + [JsonProperty("nextSunRise")] + public object NextSunRise { get; set; } + + [JsonProperty("homestate")] + public string Homestate { get; set; } + + [JsonProperty("countryCode")] + public string CountryCode { get; set; } + + [JsonProperty("isOn")] + public bool IsOn { get; set; } +} + +public partial class UserConsent +{ + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("value")] + public string Value { get; set; } +} + +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. diff --git a/Dirigera/Devices/Light.cs b/Dirigera/Devices/Light.cs new file mode 100644 index 0000000..1554413 --- /dev/null +++ b/Dirigera/Devices/Light.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json; + +namespace Tomidix.NetStandard.Dirigera.Devices; + +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + +public class Light : Device +{ + [JsonProperty("attributes")] + public LightAttributes Attributes { get; set; } + + [JsonProperty("room")] + public Room Room { get; set; } + + public override string ToString() + { + var state = Attributes.IsOn ? Attributes.LightLevel.ToString() : "Off"; + return Attributes.CustomName + $"[{state}]"; + } + + public Task Toggle() + { + return (service as DirigeraController)?.DeviceController.Toggle(this) ?? Task.CompletedTask; + } + + public Task SetLightLevel(int lightLevel) + { + return (service as DirigeraController)?.DeviceController.SetLightLevel(this, lightLevel) ?? Task.CompletedTask; + } + + public Task SetLightTemperature(int lightTemperature) + { + return (service as DirigeraController)?.DeviceController.SetLightTemperature(this, lightTemperature) ?? Task.CompletedTask; + } +} + +public class LightAttributes : Attributes +{ + + [JsonProperty("isOn")] + public bool IsOn { get; set; } + + [JsonProperty("startupOnOff")] + public string StartupOnOff { get; set; } + + [JsonProperty("lightLevel")] + public long LightLevel { get; set; } + + [JsonProperty("startUpCurrentLevel")] + public long StartUpCurrentLevel { get; set; } + + [JsonProperty("colorMode")] + public string ColorMode { get; set; } + + [JsonProperty("startupTemperature")] + public long StartupTemperature { get; set; } + + [JsonProperty("colorTemperature")] + public long ColorTemperature { get; set; } + + [JsonProperty("colorTemperatureMax")] + public long ColorTemperatureMax { get; set; } + + [JsonProperty("colorTemperatureMin")] + public long ColorTemperatureMin { get; set; } +} \ No newline at end of file diff --git a/Dirigera/Dirigera.csproj b/Dirigera/Dirigera.csproj new file mode 100644 index 0000000..1746cc1 --- /dev/null +++ b/Dirigera/Dirigera.csproj @@ -0,0 +1,12 @@ + + + + net8.0 + enable + enable + + + + + + diff --git a/Dirigera/DirigeraController.cs b/Dirigera/DirigeraController.cs new file mode 100644 index 0000000..3c9d7b0 --- /dev/null +++ b/Dirigera/DirigeraController.cs @@ -0,0 +1,87 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using System.Text; +using ApiLibs; +using Newtonsoft.Json; + +namespace Tomidix.NetStandard.Dirigera; + +public class DirigeraController : Service +{ + public DirigeraController(string hostUrl) : base(hostUrl + ":8443/v1") + { + UserController = new UserController(this); + DeviceController = new DeviceController(this); + } + + public DirigeraController(string hostUrl, string token) : this(hostUrl) + { + AddStandardHeader(new Param("Authorization", "Bearer " + token)); + } + + public UserController UserController { get; set; } + public DeviceController DeviceController { get; set; } + + public static readonly string CODE_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; + public static readonly int CODE_LENGTH = 128; + public static readonly string AUDIENCE = "homesmart.local"; + public static readonly string CHALLENGE_METHOD = "S256"; + + public static string generateCodeVerifier() + { + StringBuilder res = new(); + Random random = new(); + + for (int i = 0; i < CODE_LENGTH; i++) + { + res.Append(CODE_ALPHABET.ElementAt(random.Next(0, CODE_ALPHABET.Length))); + } + return res.ToString(); + + } + + public static string calculateCodeChallenge(string code) + { + SHA256 mySHA256 = SHA256.Create(); + byte[] byteHash = mySHA256.ComputeHash(Encoding.ASCII.GetBytes(code)); + var base64 = Base64SafeEncode(byteHash); + return base64.Substring(0, base64.Length - 1); + } + + public Task Authorize(string challenge) + { + return MakeRequest("oauth/authorize", Call.GET, new List { + new("audience", AUDIENCE), + new("response_type", "code"), + new("code_challenge", challenge), + new("code_challenge_method", CHALLENGE_METHOD) + }); + } + + public Task Pair(string dirigeraCode, string generatedCode, string name) + { + return MakeRequest("oauth/token", Call.POST, new List { + new("code", dirigeraCode), + new("name", name), + new("grant_type", "authorization_code"), + new("code_verifier", generatedCode), + }); + } + + public static string Base64SafeEncode(byte[] encbuff) + { + return System.Convert.ToBase64String(encbuff).Replace("=", ",").Replace("+", "-").Replace("/", "_"); + } +} + +public class Authorize +{ + [JsonProperty("code")] + public required string Code { get; set; } +} + +public class TokenObject +{ + [JsonProperty("access_token")] + public required string AccessToken { get; set; } +} diff --git a/Dirigera/UserController.cs b/Dirigera/UserController.cs new file mode 100644 index 0000000..3e23c41 --- /dev/null +++ b/Dirigera/UserController.cs @@ -0,0 +1,47 @@ +using System.Buffers.Text; +using System.Security.Cryptography; +using System.Text; +using ApiLibs; +using ApiLibs.General; +using ApiLibs.GitHub; +using Newtonsoft.Json; + +namespace Tomidix.NetStandard.Dirigera; + +public class UserController : SubService +{ + public UserController(DirigeraController controller) : base(controller) + { + } + + public Task> GetUsers() => MakeRequest>("users/"); + public Task GetMe() => MakeRequest("users/me/"); + +} + +public class User +{ + [JsonProperty("uid")] + public required string UID { get; set; } + + [JsonProperty("name")] + public required string Name { get; set; } + + [JsonProperty("audience")] + public required string Audience { get; set; } + + [JsonProperty("email")] + public required string Email { get; set; } + + // [JsonProperty("createdTimestamp")] + // public string CreatedTimestamp { get; set; } + + [JsonProperty("verifiedUid")] + public required string VerifiedUid { get; set; } + + [JsonProperty("role")] + public required string Role { get; set; } + +} + + diff --git a/TradFri.sln b/TradFri.sln index 1b26595..467e40b 100644 --- a/TradFri.sln +++ b/TradFri.sln @@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TradfriTest", "TradfriTest\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TradfriUI", "TradfriUI\TradfriUI.csproj", "{E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TradfriTerminalUI", "TradfriTerminalUI\TradfriTerminalUI.csproj", "{873FD76F-F540-47B5-90EC-9722E1D2D191}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dirigera", "Dirigera\Dirigera.csproj", "{71F17A36-2B01-40FE-9861-CFFEF562A5D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +31,14 @@ Global {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {E74F7B9B-80B3-443B-A34F-EB4A859DD0FF}.Release|Any CPU.Build.0 = Release|Any CPU + {873FD76F-F540-47B5-90EC-9722E1D2D191}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {873FD76F-F540-47B5-90EC-9722E1D2D191}.Debug|Any CPU.Build.0 = Debug|Any CPU + {873FD76F-F540-47B5-90EC-9722E1D2D191}.Release|Any CPU.ActiveCfg = Release|Any CPU + {873FD76F-F540-47B5-90EC-9722E1D2D191}.Release|Any CPU.Build.0 = Release|Any CPU + {71F17A36-2B01-40FE-9861-CFFEF562A5D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71F17A36-2B01-40FE-9861-CFFEF562A5D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71F17A36-2B01-40FE-9861-CFFEF562A5D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71F17A36-2B01-40FE-9861-CFFEF562A5D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TradfriTerminalUI/MainLoop.cs b/TradfriTerminalUI/MainLoop.cs new file mode 100644 index 0000000..0d126b9 --- /dev/null +++ b/TradfriTerminalUI/MainLoop.cs @@ -0,0 +1,143 @@ +using Martijn.Extensions.Linq; +using Spectre.Console; +using Spectre.Console.Json; +using Tomidix.NetStandard.Dirigera.Devices; + +namespace TradfriTerminalUI +{ + public class Details + { + public static async Task DetailView(Device? d) + { + AnsiConsole.Clear(); + + return d switch + { + EnvironmentSensor environmentSensor => EnvironmentDetailView(environmentSensor), + Light light => await LightDetailView(light), + _ => DetailViewFallback(d) + }; + } + + public static int DetailViewFallback(Device? device) + { + AnsiConsole.Confirm("The following value does not have a detail view: " + device?.Type, true); + return 1; + } + + public static int EnvironmentDetailView(EnvironmentSensor sensor) + { + var table = new Table(); + table.AddColumns(new TableColumn("key"), new TableColumn("value")); + + var dict = new Dictionary { + { "Temperature", sensor.Attributes.CurrentTemperature.ToString() }, + { "PM25", sensor.Attributes.CurrentPM25.ToString() }, + { "VOCIndex", sensor.Attributes.VocIndex.ToString() }, + { "Humidity", sensor.Attributes.CurrentRH.ToString() }, + }; + + dict.Foreach(item => + { + table.AddRow(item.Key, item.Value); + }); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(""); + + while (true) + { + string choice = AnsiConsole.Prompt( + new SelectionPrompt() + .MoreChoicesText("[grey](Move up and down to reveal more devices)[/]") + .AddChoices([ + "Exit" + ])); + + + if (choice == "Exit") + { + break; + } + } + + return 1; + + } + + public static async Task LightDetailView(Light light) + { + var table = new Table(); + table.AddColumns(new TableColumn("key"), new TableColumn("value")); + + var dict = new Dictionary { + { "Lightlevel", light.Attributes.LightLevel.ToString() }, + { "IsOn", light.Attributes.IsOn.ToString() }, + { "ColorTemperature", light.Attributes.ColorTemperature.ToString() }, + { "ColorTemperatureMax", light.Attributes.ColorTemperatureMax.ToString() }, + { "ColorTemperatureMin", light.Attributes.ColorTemperatureMin.ToString() }, + }; + + dict.Foreach(item => + { + table.AddRow(item.Key, item.Value); + }); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(""); + + while (true) + { + string choice = AnsiConsole.Prompt( + new SelectionPrompt() + .MoreChoicesText("[grey](Move up and down to reveal more devices)[/]") + .AddChoices([ + "Set Temperature", + "Set Lightlevel", + "Toggle", + "Exit" + ])); + if (choice == "Set Temperature") + { + var response = AnsiConsole.Ask("What should the temperature be?"); + await light.SetLightTemperature(response); + } + + if (choice == "Set Lightlevel") + { + var response = AnsiConsole.Ask("What should the lightlevel be?"); + await light.SetLightLevel(response); + } + + if (choice == "Toggle") + { + await light.Toggle(); + } + + if (choice == "Exit") + { + break; + } + } + + return 1; + + } + + public static int JsonView(string input, string? name = null) + { + var json = new JsonText(input); + + AnsiConsole.Clear(); + AnsiConsole.Write( + new Panel(json) + .Header(name) + .RoundedBorder() + .BorderColor(Color.Yellow)); + + AnsiConsole.Confirm("Press enter to go back", true); + + return 0; + } + } +} \ No newline at end of file diff --git a/TradfriTerminalUI/Program.cs b/TradfriTerminalUI/Program.cs new file mode 100644 index 0000000..72a73e3 --- /dev/null +++ b/TradfriTerminalUI/Program.cs @@ -0,0 +1,192 @@ + +using Newtonsoft.Json; +using Spectre.Console; +using Tomidix.NetStandard.Dirigera; +using Martijn.Extensions.Memory; +using Martijn.Extensions.Time; +using System.Net; +using Tomidix.NetStandard.Dirigera.Devices; + +namespace TradfriTerminalUI +{ + public class UserData + { + [JsonProperty("ip")] + public string? IPAddress { get; set; } + + [JsonProperty("access_token")] + public string? AccessToken { get; set; } + + [JsonProperty("hub_name")] + public string? HubName { get; set; } + } + + public partial class Program + { + private static readonly string SettingsFile = "userdata.json"; + + public static async Task Main(string[] args) + { + // We need to disable some server certification for these calls + ServicePointManager.ServerCertificateValidationCallback += (o, c, ch, er) => true; + + Memory memory = new Memory(AppContext.BaseDirectory); + var userData = await memory.ReadOrCalculate(SettingsFile, () => new UserData()); + try + { + if (string.IsNullOrEmpty(userData.AccessToken)) + { + await Authenticate(userData); + } + else + { + await MainMenu(userData); + } + } + catch (Exception e) + { + AnsiConsole.MarkupLine("[red]Encountered fatal error. Exiting...[/]"); + AnsiConsole.WriteException(e); + } + await memory.Write(SettingsFile, userData); + } + + public static async Task MainMenu(UserData data) + { + var controller = new DirigeraController(data.IPAddress!, data.AccessToken!); + + var me = await controller.UserController.GetMe(); + + AnsiConsole.MarkupLine("Hi, " + me.Name); + + // var devices = await controller.DeviceController.GetDevices(); + await MainLoop(controller, new List()); + + // AnsiConsole.MarkupLine(Markup.Escape(devices)); + + + + } + + public static async Task MainLoop(DirigeraController controller, List devices) + { + while (true) + { + AnsiConsole.Clear(); + var mapDeviceToString = (Device i) => + { + var text = i switch + { + Gateway g => g.ToString(), + EnvironmentSensor environmentSensor => environmentSensor.ToString(), + Light light => light.ToString(), + Device d => "Unknown device:" + d.Type, + _ => "Unknown value" + }; + return Markup.Escape(text); + }; + + string chosenDevice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Which device would you like to look into?") + .EnableSearch() + .MoreChoicesText("[grey](Move up and down to reveal more devices)[/]") + .AddChoiceGroup("Devices", devices.Select(i => mapDeviceToString(i))) + .AddChoiceGroup("System", ["Update devices", "Json Dump of devices", "Exit"])); + if (chosenDevice == "Update devices") + { + devices = await controller.DeviceController.GetDevices(); + continue; + } + + if (chosenDevice == "Json Dump of devices") + { + Details.JsonView(await controller.DeviceController.GetDevicesJson(), "All devices"); + continue; + } + + if (chosenDevice == "Exit") + { + break; + } + + await Details.DetailView(devices.FirstOrDefault(i => mapDeviceToString(i) == chosenDevice)); + } + } + + public static async Task Authenticate(UserData userData) + { + AnsiConsole.MarkupLine("Did not find a connection token. Starting authorization flow"); + + string code = DirigeraController.generateCodeVerifier(); + // string code = "Hello"; + AnsiConsole.MarkupLine("Generated code: [bold]{0}[/]", Markup.Escape(code)); + + string challenge = DirigeraController.calculateCodeChallenge(code); + Console.WriteLine(challenge); + AnsiConsole.MarkupLine("Using challenge: [bold]{0}[/]", Markup.Escape(challenge)); + + /// IP Address + if (!string.IsNullOrEmpty(userData.IPAddress) && !AnsiConsole.Confirm($"Use {userData.IPAddress} to connect?")) + { + userData.IPAddress = null; + } + if (string.IsNullOrEmpty(userData.IPAddress)) + { + userData.IPAddress = AnsiConsole.Ask("What's the ip of the app? (it needs to be https://)"); + } + + + /// Hub Name + if (!string.IsNullOrEmpty(userData.HubName) && !AnsiConsole.Confirm($"Use {userData.HubName} to connect?")) + { + userData.HubName = null; + } + if (string.IsNullOrEmpty(userData.HubName)) + { + userData.HubName = AnsiConsole.Ask("How would you like to name the hub?", "Debug Application"); + } + + /// Connecting + + DirigeraController controller = new DirigeraController(userData.IPAddress); + string? dirigeraCode = null; + + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .StartAsync("Connecting to Dirigera Hub", async ctx => + { + dirigeraCode = (await controller.Authorize(challenge)).Code; + }); + + + + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .StartAsync("Trying to get an access token", async ctx => + { + AnsiConsole.MarkupLine("Press the Action Button on the bottom of your Dirigera Hub within 60 seconds."); + + + for (int i = 0; i < 30; i++) + { + try + { + Thread.Sleep(TimeSpan.FromSeconds(3)); + userData.AccessToken = (await controller.Pair(dirigeraCode!, code, userData.HubName)).AccessToken; + break; + } + catch (Exception e) + { + AnsiConsole.MarkupLine("Got an exception: " + e.Message); + ctx.Status = $"Trying to get an access token [[{i}/30]]"; + } + } + + + }); + + AnsiConsole.MarkupLine("🎉 Successfully got the code 🎉"); + } + } +} diff --git a/TradfriTerminalUI/TradfriTerminalUI.csproj b/TradfriTerminalUI/TradfriTerminalUI.csproj new file mode 100644 index 0000000..f9432f6 --- /dev/null +++ b/TradfriTerminalUI/TradfriTerminalUI.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + {6b39a6d0-adf6-4c55-9aef-00efe63141c0} + Tradfri + + + +