diff --git a/AppBroker.susch/AppBroker.susch.csproj b/AppBroker.App/AppBroker.App.csproj similarity index 55% rename from AppBroker.susch/AppBroker.susch.csproj rename to AppBroker.App/AppBroker.App.csproj index 0b7db5e..2e9122f 100644 --- a/AppBroker.susch/AppBroker.susch.csproj +++ b/AppBroker.App/AppBroker.App.csproj @@ -1,20 +1,24 @@ - + - net8.0 + net9.0 enable enable + - + + + + diff --git a/AppBroker.susch/AssemblyAttributes.cs b/AppBroker.App/AssemblyAttributes.cs similarity index 100% rename from AppBroker.susch/AssemblyAttributes.cs rename to AppBroker.App/AssemblyAttributes.cs diff --git a/AppBroker.App/Controller/AppController.cs b/AppBroker.App/Controller/AppController.cs new file mode 100644 index 0000000..033da28 --- /dev/null +++ b/AppBroker.App/Controller/AppController.cs @@ -0,0 +1,62 @@ +using AppBroker.Core.Database; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.App.Controller; +[Route("app")] +internal class AppController : ControllerBase +{ + [HttpGet] + public Guid? GetIdByName(string name) + { + using var ctx = DbProvider.AppDbContext; + return ctx.Apps.FirstOrDefault(x => EF.Functions.Like(x.Name, name))?.Id; + } + + [HttpPatch] + public async Task UpdateName(Guid id, string newName) + { + using var ctx = DbProvider.AppDbContext; + + var app = ctx.Apps.FirstOrDefault(x => x.Id == id); + if (app is null) + { + ctx.Apps.Add(new Core.Database.Model.AppModel { Id = id, Name = newName }); + } + else + { + app.Name = newName; + } + await ctx.SaveChangesAsync(); + } + + [HttpGet("settings")] + public string? GetByKey(Guid id, string key) + { + using var ctx = DbProvider.AppDbContext; + return ctx.AppConfigs.FirstOrDefault(x => x.AppId == id && x.Key == key)?.Value; + } + + [HttpPost("settings")] + public string? SetValue(Guid id, string key, string value) + { + using var ctx = DbProvider.AppDbContext; + var setting = ctx.AppConfigs.FirstOrDefault(x => x.AppId == id && x.Key == key); + if (setting is null) + { + ctx.AppConfigs.Add(new Core.Database.Model.AppConfigModel {Key = key, Value = value , AppId = id }); + } + else + { + setting.Value = value; + } + return value; + } +} diff --git a/AppBroker.App/Controller/DeviceController.cs b/AppBroker.App/Controller/DeviceController.cs new file mode 100644 index 0000000..0d9a4ba --- /dev/null +++ b/AppBroker.App/Controller/DeviceController.cs @@ -0,0 +1,61 @@ +using AppBroker.Core.Devices; +using AppBroker.Core; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AppBroker.App.Model; +using AppBroker.Core.Database; +using AppBroker.Core.Managers; +using Azure.Core; + +namespace AppBroker.App.Controller; + +public record struct DeviceRenameRequest(long Id, string NewName); + +[Route("app/device")] +public class DeviceController : ControllerBase +{ + private readonly IDeviceManager deviceManager; + + public DeviceController(IDeviceManager deviceManager) + { + this.deviceManager = deviceManager; + } + + [HttpGet] + public List GetAllAppDevices() + { + var devices = deviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).ToList(); + var dev = JsonConvert.SerializeObject(devices); + + return devices; + } + + [HttpGet("overview")] + public List GetDeviceOverview(bool onlyShowInApp = true) => deviceManager + .Devices + .Select(x => x.Value) + .Where(x => !onlyShowInApp || x.ShowInApp) + .Select(x => new DeviceOverview(x.Id, x.FriendlyName, x.TypeName, x.TypeNames)) + .ToList(); + + [HttpPatch] + public void UpdateDevice([FromBody] DeviceRenameRequest request) + { + if (deviceManager.Devices.TryGetValue(request.Id, out Device? stored)) + { + stored.FriendlyName = request.NewName; + _ = DbProvider.UpdateDeviceInDb(stored); + stored.SendDataToAllSubscribers(); + } + } + +} diff --git a/AppBroker.App/Controller/HistoryController.cs b/AppBroker.App/Controller/HistoryController.cs new file mode 100644 index 0000000..addaecc --- /dev/null +++ b/AppBroker.App/Controller/HistoryController.cs @@ -0,0 +1,91 @@ +using AppBroker.Core.Devices; +using AppBroker.Core.Models; +using AppBroker.Core; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using AppBroker.Core.Managers; + +namespace AppBroker.App.Controller; + +public record struct SetHistoryRequest(bool Enable, List Ids, string Name); + +[Route("app/history")] +public class HistoryController : ControllerBase +{ + private readonly IHistoryManager historyManager; + private readonly IDeviceManager deviceManager; + + public HistoryController(IHistoryManager historyManager, IDeviceManager deviceManager) + { + this.historyManager = historyManager; + this.deviceManager = deviceManager; + } + + [HttpGet("settings")] + public List GetHistoryPropertySettings() => historyManager.GetHistoryProperties(); + + + [HttpPatch] + public void SetHistories([FromBody] SetHistoryRequest request) + { + if (request.Enable) + { + foreach (var id in request.Ids) + historyManager.EnableHistory(id, request.Name); + } + else + { + foreach (var id in request.Ids) + historyManager.DisableHistory(id, request.Name); + } + } + + [HttpGet] + public Task> GetIoBrokerHistories([FromQuery] long id, [FromQuery] DateTime dt) + { + if (deviceManager.Devices.TryGetValue(id, out Device? device)) + { + return device.GetHistory(dt.Date, dt.Date.AddDays(1).AddSeconds(-1)); + } + return Task.FromResult(new List()); + } + + //Currently not used on the app + //public Task GetIoBrokerHistory(long id, string dt, string propertyName) + //{ + // if (deviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // DateTime date = DateTime.Parse(dt).Date; + // return device.GetHistory(date, date.AddDays(1).AddSeconds(-1), propertyName); + // } + // return Task.FromResult(History.Empty); + //} + + + //Currently not used on the app + //public Task> GetIoBrokerHistoriesRange(long id, string dt, string dt2) + //{ + // if (deviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // return device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2)); + // } + + // return Task.FromResult(new List()); + //} + + [HttpGet("range")] + public async Task GetIoBrokerHistoryRange(long id, DateTime from, DateTime to, string propertyName) + { + if (deviceManager.Devices.TryGetValue(id, out Device? device)) + { + return await device.GetHistory(from, to, propertyName); + } + + return History.Empty; + } +} diff --git a/AppBroker.App/Controller/LayoutController.cs b/AppBroker.App/Controller/LayoutController.cs new file mode 100644 index 0000000..681631e --- /dev/null +++ b/AppBroker.App/Controller/LayoutController.cs @@ -0,0 +1,111 @@ +using AppBroker.Core.DynamicUI; +using AppBroker.Core; +using AppBrokerASP; + +using Microsoft.AspNetCore.Mvc; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AppBroker.App.Hubs; +using AppBroker.Core.Managers; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using AppBroker.Core.Devices; + +namespace AppBroker.App.Controller; + +public record LayoutRequest(string TypeName, string IconName, long DeviceId); +public record LayoutResponse(DeviceLayout? Layout, SvgIcon? Icon); + + +[Route("app/layout")] +public class LayoutController : ControllerBase +{ + private readonly IconService iconService; + private readonly IDeviceManager deviceManager; + + public LayoutController(IconService iconService, IDeviceManager deviceManager) + { + this.iconService = iconService; + this.deviceManager = deviceManager; + } + + [HttpGet("single")] + public LayoutResponse GetSingle([FromQuery] LayoutRequest request) + { + var layout = GetLayout(request); + var icon = GetIcon(request, layout?.IconName); + + return new LayoutResponse(layout, icon); + } + [HttpGet("multi")] + public List GetMultiple([FromQuery] List request) + { + var response = new List(); + + foreach (var req in request) + { + var layout = GetLayout(req); + var icon = GetIcon(req, layout?.IconName); + if (icon is not null || layout is not null) + response.Add(new LayoutResponse(layout, icon)); + } + return response; + } + + [HttpGet("all")] + public List GetAll() + { + return DeviceLayoutService + .GetAllLayouts() + .Select(x => new LayoutResponse(x, GetIcon(null, x.IconName))) + .ToList(); + } + + private DeviceLayout? GetLayout(LayoutRequest request) + { + DeviceLayout? layout = null; + if (request.DeviceId != 0) + layout = DeviceLayoutService.GetDeviceLayout(request.DeviceId)?.layout; + if (layout is null && !string.IsNullOrWhiteSpace(request.TypeName)) + layout = DeviceLayoutService.GetDeviceLayout(request.TypeName)?.layout; + if (layout is null && deviceManager.Devices.TryGetValue(request.DeviceId, out var device)) + { + foreach (var item in device.TypeNames) + { + if (DeviceLayoutService.GetDeviceLayout(item) is { } res && res.layout is { } resLayout) + { + layout = resLayout; + break; + } + } + } + return layout; + } + + private SvgIcon? GetIcon(LayoutRequest? request, string? iconName) + { + SvgIcon? icon = null; + if (!string.IsNullOrWhiteSpace(iconName)) + { + icon = iconService.GetIconByName(iconName); + } + if (request is not null) + { + if (icon is null && !string.IsNullOrWhiteSpace(request?.IconName)) + { + icon = iconService.GetIconByName(request.IconName); + } + if (icon is null && !string.IsNullOrWhiteSpace(request?.TypeName)) + { + icon = iconService.GetBestFitIcon(request.TypeName); + } + } + return icon; + } + + [HttpPatch] + public void ReloadDeviceLayouts() => DeviceLayoutService.ReloadLayouts(); +} diff --git a/AppBroker.App/Controller/SmarthomeController.cs b/AppBroker.App/Controller/SmarthomeController.cs new file mode 100644 index 0000000..2b5e83f --- /dev/null +++ b/AppBroker.App/Controller/SmarthomeController.cs @@ -0,0 +1,94 @@ +using AppBroker.Core.Devices; +using AppBroker.Core; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.SignalR; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AppBroker.Core.Managers; +using AppBrokerASP; +using System.Text.Json; +using NLog; + +namespace AppBroker.App.Controller; + +/// +/// Test summary for smarthome controller +/// +[Route("app/smarthome")] +public class SmarthomeController : ControllerBase +{ + private readonly IDeviceManager deviceManager; + private readonly NLog.ILogger logger; + + public SmarthomeController(IDeviceManager deviceManager, ILogger logger) + { + this.deviceManager = deviceManager; + this.logger = logger; + } + + /// + /// Used to update things on the app + /// + /// + /// + [HttpPost] + public async Task Update([FromBody] JsonApiSmarthomeMessage message) + { + if (deviceManager.Devices.TryGetValue(message.NodeId, out Device? device)) + { + switch (message.MessageType) + { + case MessageType.Get: + break; + case MessageType.Update: + await device.UpdateFromApp(message.Command, message.Parameters); + break; + case MessageType.Options: + device.OptionsFromApp(message.Command, message.Parameters); + break; + default: + break; + } + } + } + + [HttpGet] + public dynamic? GetConfig([FromQuery] long deviceId) => deviceManager.Devices.TryGetValue(deviceId, out Device? device) ? device.GetConfig() : null; + + [HttpPost("log")] + public void Log([FromBody] List logLines) + { + foreach (var item in logLines) + { + var info = new LogEventInfo( + item.LogLevel switch + { + AppLogLevel.Fatal => LogLevel.Fatal, + AppLogLevel.Error => LogLevel.Error, + AppLogLevel.Warning => LogLevel.Warn, + AppLogLevel.Info => LogLevel.Info, + AppLogLevel.Debug => LogLevel.Debug, + _ => LogLevel.Debug, + } + , item.LoggerName, item.Message); + info.TimeStamp = item.TimeStamp; + logger.Log(info); + } + } + +} + +public enum AppLogLevel +{ + Fatal, + Error, + Warning, + Info, + Debug +} +public record struct AppLog(AppLogLevel LogLevel, DateTime TimeStamp, string Message, string? LoggerName); diff --git a/AppBroker.App/Hubs/DeviceHub.cs b/AppBroker.App/Hubs/DeviceHub.cs new file mode 100644 index 0000000..00acb46 --- /dev/null +++ b/AppBroker.App/Hubs/DeviceHub.cs @@ -0,0 +1,83 @@ +using AppBroker.Core.Devices; +using AppBroker.Core; +using Newtonsoft.Json; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR; +using AppBroker.Core.Database; + +namespace AppBroker.App.Hubs; +public class DeviceHub +{ + + [Obsolete("Use REST Method instead")] + public static List GetAllDevices() + { + var devices = IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).ToList(); + var dev = JsonConvert.SerializeObject(devices); + + return devices; + } + + public record struct DeviceOverview(long Id, string TypeName, IReadOnlyCollection TypeNames, string FriendlyName); + [Obsolete("Use REST Method instead")] + public static List GetDeviceOverview() => IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).Select(x => new DeviceOverview(x.Id, x.TypeName, x.TypeNames, x.FriendlyName)).ToList(); + + [Obsolete("Use REST Method instead")] + public static void UpdateDevice(long id, string newName) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? stored)) + { + stored.FriendlyName = newName; + _ = DbProvider.UpdateDeviceInDb(stored); + stored.SendDataToAllSubscribers(); + } + + } + + + public static List Subscribe(DynamicHub hub, List DeviceIds) + { + string connectionId = hub.Context.ConnectionId; + var devices = new List(); + string? subMessage = "User subscribed to "; + foreach (long deviceId in DeviceIds) + { + + if (!IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) + continue; + + + if (!device.Subscribers.Any(x => x.ConnectionId == connectionId)) + device.Subscribers.Add(new Subscriber(connectionId, hub.Clients.Caller)); + devices.Add(device); + subMessage += device.Id + "/" + device.FriendlyName + ", "; + } + Console.WriteLine(subMessage); + var dev = JsonConvert.SerializeObject(devices); + + return devices; + } + + public static void Unsubscribe(DynamicHub hub, List DeviceIds) + { + string connectionId = hub.Context.ConnectionId; + var devices = new List(); + string? subMessage = "User unsubscribed from "; + foreach (long deviceId in DeviceIds) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) + { + + _ = device.Subscribers.RemoveWhere(x => x.ConnectionId == connectionId); + subMessage += device.Id + "/" + device.FriendlyName + ", "; + } + } + Console.WriteLine(subMessage); + } + +} diff --git a/AppBroker.App/Hubs/HistoryHub.cs b/AppBroker.App/Hubs/HistoryHub.cs new file mode 100644 index 0000000..81466a9 --- /dev/null +++ b/AppBroker.App/Hubs/HistoryHub.cs @@ -0,0 +1,85 @@ +using AppBroker.Core.Devices; +using AppBroker.Core.Models; +using AppBroker.Core; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.App.Hubs; +public class HistoryHub +{ + + [Obsolete("Use REST Method instead")] + public static List GetHistoryPropertySettings() => IInstanceContainer.Instance.HistoryManager.GetHistoryProperties(); + + [Obsolete("Use REST Method instead")] + public static void SetHistory(bool enable, long id, string name) + { + if (enable) + IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); + else + IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); + } + + [Obsolete("Use REST Method instead")] + public static void SetHistories(bool enable, List ids, string name) + { + if (enable) + { + foreach (var id in ids) + IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); + } + else + { + foreach (var id in ids) + IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); + } + } + + [Obsolete("Use REST Method instead")] + public static Task> GetIoBrokerHistories(long id, string dt) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + { + DateTime date = DateTime.Parse(dt).Date; + return device.GetHistory(date, date.AddDays(1).AddSeconds(-1)); + } + return Task.FromResult(new List()); + } + + public static Task GetIoBrokerHistory(long id, string dt, string propertyName) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + { + DateTime date = DateTime.Parse(dt).Date; + return device.GetHistory(date, date.AddDays(1).AddSeconds(-1), propertyName); + } + return Task.FromResult(History.Empty); + } + + public static Task> GetIoBrokerHistoriesRange(long id, string dt, string dt2) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + { + return device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2)); + } + + return Task.FromResult(new List()); + } + + [Obsolete("Use REST Method instead")] + public static async Task GetIoBrokerHistoryRange(long id, string dt, string dt2, string propertyName) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + { + return await device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2), propertyName) + ; + } + + return History.Empty; + } + +} diff --git a/AppBroker.App/Hubs/LayoutHub.cs b/AppBroker.App/Hubs/LayoutHub.cs new file mode 100644 index 0000000..f1c79ab --- /dev/null +++ b/AppBroker.App/Hubs/LayoutHub.cs @@ -0,0 +1,45 @@ +using AppBroker.Core.DynamicUI; +using AppBroker.Core; +using AppBrokerASP; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.App.Hubs; + +public partial class LayoutHub +{ + [Obsolete("Use REST Method instead")] + public static string GetHashCodeByTypeName(string typeName) => InstanceContainer.Instance.IconService.GetBestFitIcon(typeName).Hash; + [Obsolete("Use REST Method instead")] + public static string GetHashCodeByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName).Hash; + + [Obsolete("Use REST Method instead")] + public static SvgIcon GetIconByTypeName(string typename) => InstanceContainer.Instance.IconService.GetBestFitIcon(typename); + [Obsolete("Use REST Method instead")] + public static SvgIcon GetIconByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName); + [Obsolete("Use REST Method instead")] + public static SvgIcon GetIconByDeviceId(long deviceId) => InstanceContainer.Instance.IconService.GetBestFitIcon(InstanceContainer.Instance.DeviceManager.Devices[deviceId].TypeName); + + [Obsolete("Use REST Method instead")] + public static void ReloadDeviceLayouts() => DeviceLayoutService.ReloadLayouts(); + [Obsolete("Use REST Method instead")] + public static DeviceLayout? GetDeviceLayoutByName(string typename) => DeviceLayoutService.GetDeviceLayout(typename)?.layout; + [Obsolete("Use REST Method instead")] + public static DeviceLayout? GetDeviceLayoutByDeviceId(long id) => DeviceLayoutService.GetDeviceLayout(id)?.layout; + [Obsolete("Use REST Method instead")] + public static List GetAllDeviceLayouts() => DeviceLayoutService.GetAllLayouts(); + + [Obsolete("Use REST Method instead")] + public static LayoutNameWithHash? GetDeviceLayoutHashByDeviceId(long id) + { + var layoutHash = DeviceLayoutService.GetDeviceLayout(id); + if (layoutHash is null || layoutHash.Value.layout is null) + return null; + + return new(layoutHash.Value.layout.UniqueName, layoutHash.Value.hash); + } +} diff --git a/AppBroker.App/Hubs/Smarthome.cs b/AppBroker.App/Hubs/Smarthome.cs new file mode 100644 index 0000000..5b3bbe6 --- /dev/null +++ b/AppBroker.App/Hubs/Smarthome.cs @@ -0,0 +1,44 @@ +using AppBroker.Core; +using AppBroker.Core.Database; +using AppBroker.Core.Devices; +using AppBroker.Core.DynamicUI; +using AppBroker.Core.Models; + +using AppBrokerASP; + +using Microsoft.AspNetCore.SignalR; +using Microsoft.EntityFrameworkCore; + +using Newtonsoft.Json; + +namespace AppBroker.App.Hubs; + +public static class SmartHome +{ + [Obsolete("Use REST Method instead")] + public static async Task Update(JsonSmarthomeMessage message) + { + if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(message.NodeId, out Device? device)) + { + switch (message.MessageType) + { + case MessageType.Get: + break; + case MessageType.Update: + await device.UpdateFromApp(message.Command, message.Parameters); + break; + case MessageType.Options: + device.OptionsFromApp(message.Command, message.Parameters); + break; + default: + break; + } + //Console.WriteLine($"User send command {message.Command} to {device} with {message.Parameters}"); + } + } + + [Obsolete("Use REST Method instead")] + public static dynamic? GetConfig(long deviceId) => IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device) ? device.GetConfig() : null; + + public static async void SendUpdate(DynamicHub hub, Device device) => await (hub.Clients.All?.Update(device) ?? Task.CompletedTask); +} diff --git a/AppBroker.App/Model/DeviceOverview.cs b/AppBroker.App/Model/DeviceOverview.cs new file mode 100644 index 0000000..560d094 --- /dev/null +++ b/AppBroker.App/Model/DeviceOverview.cs @@ -0,0 +1,5 @@ +using System.ComponentModel.DataAnnotations; + +namespace AppBroker.App.Model; + +public record struct DeviceOverview(long Id, string FriendlyName, string TypeName, List TypeNames); diff --git a/AppBroker.App/Model/JsonApiSmarthomeMessage.cs b/AppBroker.App/Model/JsonApiSmarthomeMessage.cs new file mode 100644 index 0000000..116321b --- /dev/null +++ b/AppBroker.App/Model/JsonApiSmarthomeMessage.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + + +namespace AppBroker.Core; + +public partial class JsonApiSmarthomeMessage : BaseSmarthomeMessage +{ + public List Parameters { get; set; } + + + public JsonApiSmarthomeMessage(uint nodeId, MessageType messageType, Command command, params JToken[] parameters) + { + NodeId = nodeId; + MessageType = messageType; + Command = command; + Parameters = parameters.ToList(); + } + + public JsonApiSmarthomeMessage() + { + + } +} diff --git a/AppBroker.App/Model/LayoutNameWithHash.cs b/AppBroker.App/Model/LayoutNameWithHash.cs new file mode 100644 index 0000000..88b73b0 --- /dev/null +++ b/AppBroker.App/Model/LayoutNameWithHash.cs @@ -0,0 +1,4 @@ +namespace AppBroker.App.Hubs; + +public record LayoutNameWithHash(string Name, string Hash); + diff --git a/AppBroker.App/Plugin.cs b/AppBroker.App/Plugin.cs new file mode 100644 index 0000000..ecc8544 --- /dev/null +++ b/AppBroker.App/Plugin.cs @@ -0,0 +1,41 @@ + +using AppBroker.App.Hubs; +using AppBroker.Core; +using AppBroker.Core.Devices; +using AppBroker.Core.Extension; + +using AppBrokerASP.Extension; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; + +using NLog; + +using System.ComponentModel.DataAnnotations; + +namespace AppBroker.App; + +internal class Plugin : IPlugin +{ + public string Name => "App"; + + public int LoadOrder => int.MinValue; + + public bool Initialize(LogFactory logFactory) + { + return true; + } +} + +internal class ServiceExtender : IServiceExtender +{ + + public IEnumerable GetHubTypes() + { + yield return typeof(SmartHome); + yield return typeof(HistoryHub); + yield return typeof(DeviceHub); + yield return typeof(LayoutHub); + } +} diff --git a/AppBroker.Core/AppBroker.Core.csproj b/AppBroker.Core/AppBroker.Core.csproj index 4a988a1..2ffb770 100644 --- a/AppBroker.Core/AppBroker.Core.csproj +++ b/AppBroker.Core/AppBroker.Core.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable preview @@ -19,14 +19,16 @@ - - - + + + + + diff --git a/AppBroker.Core/BasicMessage.cs b/AppBroker.Core/BasicMessage.cs index 410e5ff..2ceb0cf 100644 --- a/AppBroker.Core/BasicMessage.cs +++ b/AppBroker.Core/BasicMessage.cs @@ -8,11 +8,11 @@ namespace AppBroker.Core; public abstract class BaseSmarthomeMessage { [JsonProperty("id")] - public virtual uint NodeId { get; set; } + public virtual long NodeId { get; set; } - [JsonProperty("m"), JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(StringEnumConverter))] public virtual MessageType MessageType { get; set; } - [JsonProperty("c"), JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(StringEnumConverter))] public virtual Command Command { get; set; } } diff --git a/AppBroker.Core/Configuration/DatabaseConfig.cs b/AppBroker.Core/Configuration/DatabaseConfig.cs index 08dfa7d..5afc68e 100644 --- a/AppBroker.Core/Configuration/DatabaseConfig.cs +++ b/AppBroker.Core/Configuration/DatabaseConfig.cs @@ -1,9 +1,10 @@ namespace AppBroker.Core.Configuration; -public class DatabaseConfig +public class DatabaseConfig : IConfig { public const string ConfigName = nameof(DatabaseConfig); + public string Name => ConfigName; public string BrokerDBConnectionString { get; set; } public string BrokerDatabasePluginName { get; set; } diff --git a/AppBroker.Core/Configuration/HistoryConfig.cs b/AppBroker.Core/Configuration/HistoryConfig.cs index af14fb7..6b91add 100644 --- a/AppBroker.Core/Configuration/HistoryConfig.cs +++ b/AppBroker.Core/Configuration/HistoryConfig.cs @@ -1,9 +1,10 @@ namespace AppBroker.Core.Configuration; -public class HistoryConfig +public class HistoryConfig : IConfig { public const string ConfigName = nameof(HistoryConfig); + public string Name => ConfigName; public bool UseOwnHistoryManager { get; set; } diff --git a/AppBroker.Core/Configuration/IConfig.cs b/AppBroker.Core/Configuration/IConfig.cs new file mode 100644 index 0000000..b81d9b5 --- /dev/null +++ b/AppBroker.Core/Configuration/IConfig.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Configuration; +public interface IConfig +{ + string Name { get; } +} diff --git a/AppBroker.Core/Configuration/IConfigManager.cs b/AppBroker.Core/Configuration/IConfigManager.cs index f35723a..2f3b1bf 100644 --- a/AppBroker.Core/Configuration/IConfigManager.cs +++ b/AppBroker.Core/Configuration/IConfigManager.cs @@ -8,4 +8,5 @@ public interface IConfigManager HistoryConfig HistoryConfig { get; } MqttConfig MqttConfig { get; } DatabaseConfig DatabaseConfig { get; } + IReadOnlyCollection PluginConfigs { get; } } \ No newline at end of file diff --git a/AppBroker.Core/Configuration/MqttConfig.cs b/AppBroker.Core/Configuration/MqttConfig.cs index 1a0798f..effdc3e 100644 --- a/AppBroker.Core/Configuration/MqttConfig.cs +++ b/AppBroker.Core/Configuration/MqttConfig.cs @@ -1,8 +1,9 @@ namespace AppBroker.Core.Configuration; -public class MqttConfig +public class MqttConfig : IConfig { public const string ConfigName = nameof(MqttConfig); + public string Name => ConfigName; public bool Enabled { get; set; } public int ConnectionBacklog { get; set; } diff --git a/AppBroker.Core/Database/AppDbContext.cs b/AppBroker.Core/Database/AppDbContext.cs new file mode 100644 index 0000000..13eedee --- /dev/null +++ b/AppBroker.Core/Database/AppDbContext.cs @@ -0,0 +1,32 @@ +using AppBroker.Core.Database.Model; + +using Microsoft.EntityFrameworkCore; + +using NonSucking.Framework.Extension.EntityFrameworkCore; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Database; +public class AppDbContext : BaseDbContext +{ + public DbSet Apps => Set(); + public DbSet AppConfigs => Set(); + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var dbConfig = IInstanceContainer.Instance.ConfigManager.DatabaseConfig; + DatabaseFactory.Initialize(new FileInfo(dbConfig.BrokerDatabasePluginName).FullName); + DatabaseType = dbConfig.BrokerDatabasePluginName; + foreach (var item in DatabaseFactory.DatabaseConfigurators) + { + if (DatabaseType.Contains(item.Name, StringComparison.OrdinalIgnoreCase)) + item.OnConfiguring(optionsBuilder, dbConfig.BrokerDBConnectionString).UseLazyLoadingProxies(); + } + + base.OnConfiguring(optionsBuilder); + } +} diff --git a/AppBroker.Core/Database/BrokerDbContext.cs b/AppBroker.Core/Database/BrokerDbContext.cs index 286a230..4d24b17 100644 --- a/AppBroker.Core/Database/BrokerDbContext.cs +++ b/AppBroker.Core/Database/BrokerDbContext.cs @@ -27,6 +27,7 @@ public class BrokerDbContext : BaseDbContext public DbSet Devices => Set(); public DbSet DeviceToDeviceMappings => Set(); //public DbSet HeaterConfigTemplates { get; set; } + public DbSet ConfigDatas => Set(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) diff --git a/AppBroker.Core/Database/DbProvider.cs b/AppBroker.Core/Database/DbProvider.cs index 674e995..609f4ef 100644 --- a/AppBroker.Core/Database/DbProvider.cs +++ b/AppBroker.Core/Database/DbProvider.cs @@ -1,7 +1,6 @@ using AppBroker.Core.Database.History; using AppBroker.Core.Devices; -using AppBrokerASP.Devices; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Migrations.Operations; @@ -13,9 +12,12 @@ namespace AppBroker.Core.Database; public static class DbProvider { public static BrokerDbContext BrokerDbContext => new(); + public static AppDbContext AppDbContext => new(); public static HistoryDbContext HistoryContext => new(); + //public static AppDbContext AppContext => new(); + private class CountResult { public int Count { get; set; } @@ -25,6 +27,7 @@ static DbProvider() { using var ctx = BrokerDbContext; using var ctx2 = HistoryContext; + using var ctx3 = AppDbContext; if (ctx2.Database.CanConnect() && ctx2.DatabaseType.Contains("sqlite", StringComparison.OrdinalIgnoreCase)) { @@ -52,7 +55,7 @@ static DbProvider() ctx.Database.Migrate(); ctx2.Database.Migrate(); - + ctx3.Database.Migrate(); } public static bool AddDeviceToDb(Device d) diff --git a/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.Designer.cs b/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.Designer.cs new file mode 100644 index 0000000..9b15090 --- /dev/null +++ b/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.Designer.cs @@ -0,0 +1,23 @@ +// +using System; + +using AppBroker.Core.Database.History; + + +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AppBroker.Core.Database.AppDb.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration(Id)] + partial class InitialCreate + { + public const string Id = $"20240509175553_InitialCreate"; + + } +} diff --git a/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.cs b/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.cs new file mode 100644 index 0000000..2e2f225 --- /dev/null +++ b/AppBroker.Core/Database/Migrations/AppDb/20240509175553_InitialCreate.cs @@ -0,0 +1,55 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Xml.Linq; + +using Microsoft.EntityFrameworkCore.Migrations; + +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + + +namespace AppBroker.Core.Database.AppDb.Migrations +{ + public partial class InitialCreate : Migration, IAutoMigrationTypeProvider + { + public IReadOnlyList GetEntityTypes() => new Type[] + { + typeof(AppModel), + typeof(AppConfigModel) + }; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.SetUpgradeOperations(this); + } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.SetDowngradeOperations(this); + } + + [Table("AppConfigs")] + public class AppConfigModel + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None), Column(Order = 0)] + public string Key { get; set; } + + public string Value { get; set; } + + public Guid AppId { get; set; } + + [Required, ForeignKey("AppId"), Key, Column(Order = 1)] + public AppModel App { get; set; } + } + [Table("Apps")] + public class AppModel + { + [Key] + public Guid Id { get; set; } + public string Name { get; set; } + + [InverseProperty(nameof(AppConfigModel.App))] + public ICollection Configs { get; set; } + } + + } +} diff --git a/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.Designer.cs b/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.Designer.cs new file mode 100644 index 0000000..cec73f0 --- /dev/null +++ b/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.Designer.cs @@ -0,0 +1,19 @@ +// +using System; +using AppBroker.Core.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace AppBroker.Core.Database.Migrations.BrokerDb +{ + [DbContext(typeof(BrokerDbContext))] + [Migration("20250221223124_AddedConfigData")] + partial class AddedConfigData + { + + } +} diff --git a/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.cs b/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.cs new file mode 100644 index 0000000..5ddca7d --- /dev/null +++ b/AppBroker.Core/Database/Migrations/BrokerDb/20250221223124_AddedConfigData.cs @@ -0,0 +1,83 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +using NonSucking.Framework.Extension.EntityFrameworkCore.Migrations; + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using static AppBroker.Core.Database.Migrations.BrokerDb.Initial; + +using DayOfWeek = AppBroker.Core.Models.DayOfWeek; + +namespace AppBroker.Core.Database.Migrations.BrokerDb +{ + public partial class AddedConfigData : Migration, IAutoMigrationTypeProvider + { + public IReadOnlyList GetEntityTypes() => new Type[] + { + typeof(DeviceModel), + typeof(HeaterConfigModel), + typeof(DeviceMappingModel), + typeof(ConfigDataModel) + }; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.SetUpgradeOperations(this); + } + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.SetDowngradeOperations(this); + } + + [Table("Devices")] + public class DeviceModel + { + [Key] + public long Id { get; set; } + [MaxLength(200)] + public string TypeName { get; set; } = ""; + [MaxLength(200)] + public string? FriendlyName { get; set; } + + public string? LastState { get; set; } + public DateTime? LastStateChange { get; set; } + public bool StartAutomatically { get; set; } + public string? DeserializationData { get; set; } + } + [Table("HeaterConfigs")] + public class HeaterConfigModel + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + public long? DeviceId { get; set; } + public DayOfWeek DayOfWeek { get; set; } + public DateTime TimeOfDay { get; set; } + public double Temperature { get; set; } + + [ForeignKey(nameof(DeviceId))] + public virtual DeviceModel? Device { get; set; } + } + + [Table("DeviceToDeviceMappings")] + public class DeviceMappingModel + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [ForeignKey("ParentId")] + public virtual DeviceModel? Parent { get; set; } + [ForeignKey("ChildId")] + public virtual DeviceModel? Child { get; set; } + } + [Table("ConfigDatas")] + public class ConfigDataModel + { + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Key { get; set; } + + public string Value { get; set; } + } + + } +} diff --git a/AppBroker.Core/Database/Model/AppConfigModel.cs b/AppBroker.Core/Database/Model/AppConfigModel.cs new file mode 100644 index 0000000..f646636 --- /dev/null +++ b/AppBroker.Core/Database/Model/AppConfigModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Database.Model; + +[Table("AppConfigs")] +public class AppConfigModel +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.None), Column(Order = 0)] + public string Key { get; set; } + + public string Value { get; set; } + + public Guid AppId { get; set; } + + [Required, ForeignKey("AppId"), Key, Column(Order = 1)] + public virtual AppModel App { get; set; } +} diff --git a/AppBroker.Core/Database/Model/AppModel.cs b/AppBroker.Core/Database/Model/AppModel.cs new file mode 100644 index 0000000..d4c4d8a --- /dev/null +++ b/AppBroker.Core/Database/Model/AppModel.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Database.Model; +[Table("Apps")] +public class AppModel +{ + [Key] + public Guid Id { get; set; } + public string Name { get; set; } + + [InverseProperty(nameof(AppConfigModel.App))] + public virtual ICollection Configs { get; set; } +} diff --git a/AppBroker.Core/Database/Model/ConfigDataModel.cs b/AppBroker.Core/Database/Model/ConfigDataModel.cs new file mode 100644 index 0000000..9e124f8 --- /dev/null +++ b/AppBroker.Core/Database/Model/ConfigDataModel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Database.Model; + +[Table("ConfigDatas")] +public class ConfigDataModel +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public string Key { get; set; } + + public string Value { get; set; } +} diff --git a/AppBroker.Core/Devices/Device.cs b/AppBroker.Core/Devices/Device.cs index 22288e9..7782285 100644 --- a/AppBroker.Core/Devices/Device.cs +++ b/AppBroker.Core/Devices/Device.cs @@ -13,7 +13,6 @@ using Microsoft.EntityFrameworkCore; using System.Linq; using System.Runtime.CompilerServices; -using System.Text.Json; namespace AppBroker.Core.Devices; @@ -67,6 +66,7 @@ public virtual string FriendlyName [JsonExtensionData] public Dictionary? DynamicStateData => IInstanceContainer.Instance.DeviceStateManager.GetCurrentState(Id); + private readonly Timer sendLastDataTimer; private readonly List toRemove = new(); private string friendlyName; diff --git a/AppBroker.Core/DynamicUI/DasboardSpecialType.cs b/AppBroker.Core/DynamicUI/DasboardSpecialType.cs index e569f49..4ec814b 100644 --- a/AppBroker.Core/DynamicUI/DasboardSpecialType.cs +++ b/AppBroker.Core/DynamicUI/DasboardSpecialType.cs @@ -5,7 +5,9 @@ namespace AppBroker.Core.DynamicUI; -[JsonConverter(typeof(StringEnumConverter), converterParameters: typeof(CamelCaseNamingStrategy))] +[JsonConverter(typeof(StringEnumConverter) + //,converterParameters: typeof(CamelCaseNamingStrategy) + )] public enum DasboardSpecialType { None = 0, diff --git a/AppBroker.Core/DynamicUI/DashboardDeviceLayout.cs b/AppBroker.Core/DynamicUI/DashboardDeviceLayout.cs index 6417347..51e45c3 100644 --- a/AppBroker.Core/DynamicUI/DashboardDeviceLayout.cs +++ b/AppBroker.Core/DynamicUI/DashboardDeviceLayout.cs @@ -1,3 +1,3 @@ namespace AppBroker.Core.DynamicUI; -public record DashboardDeviceLayout(List DashboardProperties); +public record DashboardDeviceLayout(List DashboardProperties); diff --git a/AppBroker.Core/DynamicUI/DashbardPropertyInfo.cs b/AppBroker.Core/DynamicUI/DashboardPropertyInfo.cs similarity index 68% rename from AppBroker.Core/DynamicUI/DashbardPropertyInfo.cs rename to AppBroker.Core/DynamicUI/DashboardPropertyInfo.cs index 8ee8479..b5a6bcb 100644 --- a/AppBroker.Core/DynamicUI/DashbardPropertyInfo.cs +++ b/AppBroker.Core/DynamicUI/DashboardPropertyInfo.cs @@ -1,7 +1,7 @@ namespace AppBroker.Core.DynamicUI; -public class DashbardPropertyInfo : LayoutBasePropertyInfo +public class DashboardPropertyInfo : LayoutBasePropertyInfo { public DasboardSpecialType SpecialType { get; set; } = DasboardSpecialType.None; } diff --git a/AppBroker.Core/DynamicUI/DetailPropertyInfo.cs b/AppBroker.Core/DynamicUI/DetailPropertyInfo.cs index db2c562..303b026 100644 --- a/AppBroker.Core/DynamicUI/DetailPropertyInfo.cs +++ b/AppBroker.Core/DynamicUI/DetailPropertyInfo.cs @@ -1,6 +1,7 @@ -namespace AppBroker.Core.DynamicUI; + +namespace AppBroker.Core.DynamicUI; -public class DetailPropertyInfo : LayoutBasePropertyInfo +public partial class DetailPropertyInfo : LayoutBasePropertyInfo { public int? TabInfoId { get; set; } public bool BlurryCard { get; set; } diff --git a/AppBroker.Core/DynamicUI/DeviceLayout.cs b/AppBroker.Core/DynamicUI/DeviceLayout.cs index 9f7ffcc..c24d565 100644 --- a/AppBroker.Core/DynamicUI/DeviceLayout.cs +++ b/AppBroker.Core/DynamicUI/DeviceLayout.cs @@ -1,16 +1,28 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using NSwag.Annotations; + namespace AppBroker.Core.DynamicUI; public record DeviceLayout( string UniqueName, + string IconName, string? TypeName, string[]? TypeNames, long[]? Ids, DashboardDeviceLayout? DashboardDeviceLayout, - DetailDeviceLayout? DetailDeviceLayout, - [property:Newtonsoft.Json.JsonExtensionData]IDictionary AdditionalData, + DetailDeviceLayout? DetailDeviceLayout, + List? NotificationSetup, + [property:Newtonsoft.Json.JsonExtensionData, OpenApiIgnore] Dictionary AdditionalDataDes, + int Version = 1, - bool ShowOnlyInDeveloperMode = false ); + bool ShowOnlyInDeveloperMode = false, + string Hash = "") +{ + public Dictionary AdditionalData => AdditionalDataDes; +} + + +public record NotificationSetup(string UniqueName, string TranslatableName, int Times = -1, List? DeviceIds = null, bool Global = false); \ No newline at end of file diff --git a/AppBroker.Core/DynamicUI/DeviceLayoutService.cs b/AppBroker.Core/DynamicUI/DeviceLayoutService.cs index 726c153..774baf6 100644 --- a/AppBroker.Core/DynamicUI/DeviceLayoutService.cs +++ b/AppBroker.Core/DynamicUI/DeviceLayoutService.cs @@ -37,11 +37,17 @@ private static string GetMD5StringFor(byte[] bytes) } #pragma warning restore CA5351 // Do Not Use Broken Cryptographic Algorithms - private static (DeviceLayout? layout, string hash) GetHashAndFile(string path) + private static DeviceLayout GetLayout(string path) { var text = File.ReadAllText(path); var hash = GetMD5StringFor(File.ReadAllBytes(path)); - return (JsonConvert.DeserializeObject(text), hash); + var layout = JsonConvert.DeserializeObject(text); + if(layout is null) + { + throw new FileLoadException($"Could not parse {text} to DeviceLayout"); + } + return layout with { AdditionalDataDes = layout.AdditionalDataDes ?? [], Hash= hash }; + } private static void FileChanged(object sender, FileSystemEventArgs e) { @@ -50,24 +56,24 @@ private static void FileChanged(object sender, FileSystemEventArgs e) _ = Task.Delay(100).ContinueWith((t) => { logger.Info($"Layout change detected: {e.FullPath}"); - var (layout, hash) = GetHashAndFile(e.FullPath); + var layout = GetLayout(e.FullPath); if (layout is null) return; HashSet subs = new(); - CheckLayout(layout, hash, subs); + CheckLayout(layout, layout.Hash, subs); if (layout.TypeNames is not null) { foreach (var item in layout.TypeNames) { - CheckLayout(layout with { TypeName = item }, hash, subs); + CheckLayout(layout with { TypeName = item }, layout.Hash, subs); } } - CheckLayoutIds(layout, hash, subs); + CheckLayoutIds(layout, layout.Hash, subs); }); } private static void CheckLayoutIds(DeviceLayout layout, string hash, HashSet subs) @@ -140,8 +146,8 @@ public static void ReloadLayouts() // TODO: try catch try { - var (layout, hash) = GetHashAndFile(file); - + var layout = GetLayout(file); + var hash = layout.Hash; if (layout is null) continue; diff --git a/AppBroker.Core/DynamicUI/EditParameter.cs b/AppBroker.Core/DynamicUI/EditParameter.cs index 28e3600..47e36fd 100644 --- a/AppBroker.Core/DynamicUI/EditParameter.cs +++ b/AppBroker.Core/DynamicUI/EditParameter.cs @@ -12,6 +12,7 @@ public class EditParameter public MessageType? MessageType { get; set; } public string? DisplayName { get; set; } public List? Parameters { get; set; } - [property: JsonExtensionData] - public Dictionary? ExtensionData { get; set; } + [JsonExtensionData] + public Dictionary? ExtensionDataDes { get; set; } + public Dictionary? ExtensionData => ExtensionDataDes; } diff --git a/AppBroker.Core/DynamicUI/EditType.cs b/AppBroker.Core/DynamicUI/EditType.cs index adcc1bd..09c0b3e 100644 --- a/AppBroker.Core/DynamicUI/EditType.cs +++ b/AppBroker.Core/DynamicUI/EditType.cs @@ -4,5 +4,5 @@ namespace AppBroker.Core.DynamicUI; -[JsonConverter(typeof(StringEnumConverter), converterParameters: typeof(CamelCaseNamingStrategy))] +[JsonConverter(typeof(StringEnumConverter))] public enum EditType { Button, RaisedButton, FloatingActionButton, IconButton, Toggle, Dropdown, Slider, Input, Icon, Radial } diff --git a/AppBroker.Core/DynamicUI/FontStyle.cs b/AppBroker.Core/DynamicUI/FontStyleSetting.cs similarity index 75% rename from AppBroker.Core/DynamicUI/FontStyle.cs rename to AppBroker.Core/DynamicUI/FontStyleSetting.cs index 9528644..0248205 100644 --- a/AppBroker.Core/DynamicUI/FontStyle.cs +++ b/AppBroker.Core/DynamicUI/FontStyleSetting.cs @@ -7,8 +7,8 @@ namespace AppBroker.Core.DynamicUI; /// /// Whether to slant the glyphs in the font /// -[JsonConverter(typeof(StringEnumConverter), converterParameters: typeof(CamelCaseNamingStrategy))] -public enum FontStyle +[JsonConverter(typeof(StringEnumConverter))] +public enum FontStyleSetting { /// /// Use the upright glyphs diff --git a/AppBroker.Core/DynamicUI/FontWeight.cs b/AppBroker.Core/DynamicUI/FontWeightSetting.cs similarity index 66% rename from AppBroker.Core/DynamicUI/FontWeight.cs rename to AppBroker.Core/DynamicUI/FontWeightSetting.cs index 8bf6bfa..d9cfdfb 100644 --- a/AppBroker.Core/DynamicUI/FontWeight.cs +++ b/AppBroker.Core/DynamicUI/FontWeightSetting.cs @@ -7,8 +7,8 @@ namespace AppBroker.Core.DynamicUI; /// /// The thickness of the glyphs used to draw the text /// -[JsonConverter(typeof(StringEnumConverter), converterParameters: typeof(CamelCaseNamingStrategy))] -public enum FontWeight +[JsonConverter(typeof(StringEnumConverter))] +public enum FontWeightSetting { Normal, Bold diff --git a/AppBroker.Core/DynamicUI/LayoutBasePropertyInfo.cs b/AppBroker.Core/DynamicUI/LayoutBasePropertyInfo.cs index d9f3796..2ab78d8 100644 --- a/AppBroker.Core/DynamicUI/LayoutBasePropertyInfo.cs +++ b/AppBroker.Core/DynamicUI/LayoutBasePropertyInfo.cs @@ -7,7 +7,7 @@ public class LayoutBasePropertyInfo { public string Name { get; set; } = ""; public int Order { get; set; } - public TextStyle? TextStyle { get; set; } + public TextSettings? TextStyle { get; set; } public PropertyEditInformation? EditInfo { get; set; } public int? RowNr { get; set; } public string UnitOfMeasurement { get; set; } = ""; @@ -18,8 +18,8 @@ public class LayoutBasePropertyInfo public int? Precision { get; set; } [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } + public Dictionary? ExtensionDataDes { get; set; } + public Dictionary? ExtensionData => ExtensionDataDes; public string DisplayName { get; set; } = ""; -} - +} \ No newline at end of file diff --git a/AppBroker.Core/DynamicUI/PropertyEditInformation.cs b/AppBroker.Core/DynamicUI/PropertyEditInformation.cs index 5879e62..f3476cd 100644 --- a/AppBroker.Core/DynamicUI/PropertyEditInformation.cs +++ b/AppBroker.Core/DynamicUI/PropertyEditInformation.cs @@ -5,16 +5,17 @@ namespace AppBroker.Core.DynamicUI; public class PropertyEditInformation { - public MessageType EditCommand { get; set; } + public MessageType MessageType { get; set; } public List EditParameter { get; set; } = default!; - public EditType EditType { get; set; } + public string EditType { get; set; } public string? Display { get; set; } public string? HubMethod { get; set; } public string? ValueName { get; set; } public object? ActiveValue { get; set; } public string? Dialog { get; set; } [JsonExtensionData] - public Dictionary? ExtensionData { get; set; } + public Dictionary? ExtensionDataDes { get; set; } + public Dictionary? ExtensionData => ExtensionDataDes; } diff --git a/AppBroker.Core/DynamicUI/TextSettings.cs b/AppBroker.Core/DynamicUI/TextSettings.cs new file mode 100644 index 0000000..715e52c --- /dev/null +++ b/AppBroker.Core/DynamicUI/TextSettings.cs @@ -0,0 +1,3 @@ +namespace AppBroker.Core.DynamicUI; + +public record TextSettings(double? FontSize = null, string? FontFamily = null, FontWeightSetting FontWeight = FontWeightSetting.Normal, FontStyleSetting FontStyle = FontStyleSetting.Normal); diff --git a/AppBroker.Core/DynamicUI/TextStyle.cs b/AppBroker.Core/DynamicUI/TextStyle.cs deleted file mode 100644 index 48d685d..0000000 --- a/AppBroker.Core/DynamicUI/TextStyle.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace AppBroker.Core.DynamicUI; - -public record TextStyle(double? FontSize = null, string FontFamily = "", FontWeight FontWeight = FontWeight.Normal, FontStyle FontStyle = FontStyle.Normal); diff --git a/AppBroker.Core/Extension/INotifier.cs b/AppBroker.Core/Extension/INotifier.cs new file mode 100644 index 0000000..47ce968 --- /dev/null +++ b/AppBroker.Core/Extension/INotifier.cs @@ -0,0 +1,13 @@ +using AppBroker.Core.Devices; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Core.Extension; +public interface INotifier +{ + +} diff --git a/AppBroker.Core/Extension/IPlugin.cs b/AppBroker.Core/Extension/IPlugin.cs index 56c618a..3ec37b6 100644 --- a/AppBroker.Core/Extension/IPlugin.cs +++ b/AppBroker.Core/Extension/IPlugin.cs @@ -17,10 +17,22 @@ namespace AppBroker.Core.Extension /// public interface IPlugin { - public string Name { get; } + string Name { get; } + /// + /// Gets the numeric order for when to load the plugin, higher numbers means loaded later + /// + int LoadOrder { get; } bool Initialize(NLog.LogFactory logFactory); void RegisterTypes() { } } + + + public interface IAppConfigurator + { + string UniqueName { get; } + IDictionary? GetConfigs(); + } + } diff --git a/AppBroker.Core/Extensions.cs b/AppBroker.Core/Extensions.cs index 38bd68e..8bc2f77 100644 --- a/AppBroker.Core/Extensions.cs +++ b/AppBroker.Core/Extensions.cs @@ -1,7 +1,6 @@ using AppBroker.Core.Database.Model; using AppBroker.Core.Devices; -using AppBrokerASP.Devices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; diff --git a/AppBroker.Core/ISmartHomeClient.cs b/AppBroker.Core/ISmartHomeClient.cs index bc02149..7593bc0 100644 --- a/AppBroker.Core/ISmartHomeClient.cs +++ b/AppBroker.Core/ISmartHomeClient.cs @@ -3,8 +3,8 @@ namespace AppBroker.Core; -public interface ISmartHomeClient -{ - Task Update(Device device); - Task UpdateUi(DeviceLayout deviceLayout, string hash); -} +//public interface ISmartHomeClient +//{ +// Task Update(Device device); +// Task UpdateUi(DeviceLayout deviceLayout, string hash); +//} diff --git a/AppBroker.Core/IconService.cs b/AppBroker.Core/IconService.cs index fc711cd..b987fe4 100644 --- a/AppBroker.Core/IconService.cs +++ b/AppBroker.Core/IconService.cs @@ -6,7 +6,7 @@ namespace AppBroker.Core; [NonSucking.Framework.Serialization.Nooson] -public partial record SvgIcon(string Name, string Hash, [property: JsonIgnore] string Path, byte[]? Data); +public partial record SvgIcon(string Name, string Hash,[property: JsonIgnore] string Path, byte[]? Data, string TypeName =""); #pragma warning disable CA5351 // Do Not Use Broken Cryptographic Algorithms @@ -71,7 +71,7 @@ public SvgIcon GetBestFitIcon(string typeName) var iconBytes = File.ReadAllBytes(path); var existing = iconCache.FirstOrDefault(x => x.Value.Path == path).Value; - result = existing == default ? new(tmpTypeName, GetMD5StringFor(iconBytes), path, iconBytes) : existing; + result = existing == default ? new(tmpTypeName, GetMD5StringFor(iconBytes), path, iconBytes, tmpTypeName) : existing; break; } diff --git a/AppBroker.Core/JsonSmarthomeMessage.cs b/AppBroker.Core/JsonSmarthomeMessage.cs index ec4ebef..aa37e7a 100644 --- a/AppBroker.Core/JsonSmarthomeMessage.cs +++ b/AppBroker.Core/JsonSmarthomeMessage.cs @@ -5,20 +5,15 @@ using nj = Newtonsoft.Json; -namespace AppBroker.Core; +namespace AppBroker.App; //[NonSucking.Framework.Serialization.Nooson] public partial class JsonSmarthomeMessage : BaseSmarthomeMessage { [nj.JsonProperty("p")] public List Parameters { get; set; } - [JsonIgnore] - public override uint NodeId { get; set; } - [JsonProperty("id")] - public long LongNodeId { get; set; } - - public JsonSmarthomeMessage(uint nodeId, MessageType messageType, Command command, params JToken[] parameters) + public JsonSmarthomeMessage(long nodeId, MessageType messageType, Command command, params JToken[] parameters) { NodeId = nodeId; MessageType = messageType; diff --git a/AppBroker.Core/Subscriber.cs b/AppBroker.Core/Subscriber.cs index 8432e0e..5c77e8e 100644 --- a/AppBroker.Core/Subscriber.cs +++ b/AppBroker.Core/Subscriber.cs @@ -3,8 +3,8 @@ public class Subscriber { public string ConnectionId { get; set; } - public ISmartHomeClient SmarthomeClient { get; internal set; } - public Subscriber(string connectionId, ISmartHomeClient smarthomeClient) + public dynamic SmarthomeClient { get; internal set; } + public Subscriber(string connectionId, dynamic smarthomeClient) { ConnectionId = connectionId; SmarthomeClient = smarthomeClient; diff --git a/AppBroker.Generators.Test/AppBroker.Generators.Test.csproj b/AppBroker.Generators.Test/AppBroker.Generators.Test.csproj index faa587d..9b0e4aa 100644 --- a/AppBroker.Generators.Test/AppBroker.Generators.Test.csproj +++ b/AppBroker.Generators.Test/AppBroker.Generators.Test.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable false diff --git a/AppBroker.IOBroker/AppBroker.IOBroker.csproj b/AppBroker.IOBroker/AppBroker.IOBroker.csproj index f837d00..151eec3 100644 --- a/AppBroker.IOBroker/AppBroker.IOBroker.csproj +++ b/AppBroker.IOBroker/AppBroker.IOBroker.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable @@ -21,10 +21,4 @@ - - - - - - diff --git a/AppBroker.IOBroker/Plugin.cs b/AppBroker.IOBroker/Plugin.cs index 2ffcedb..abd2f5b 100644 --- a/AppBroker.IOBroker/Plugin.cs +++ b/AppBroker.IOBroker/Plugin.cs @@ -12,6 +12,7 @@ namespace AppBroker.IOBroker; public class Plugin : IPlugin { public string Name { get; } + public int LoadOrder => int.MinValue; public bool Initialize(LogFactory logFactory) { diff --git a/AppBroker.Mail/AppBroker.Mail.csproj b/AppBroker.Mail/AppBroker.Mail.csproj new file mode 100644 index 0000000..78bf2f7 --- /dev/null +++ b/AppBroker.Mail/AppBroker.Mail.csproj @@ -0,0 +1,29 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppBroker.Mail/AssemblyAttributes.cs b/AppBroker.Mail/AssemblyAttributes.cs new file mode 100644 index 0000000..db45463 --- /dev/null +++ b/AppBroker.Mail/AssemblyAttributes.cs @@ -0,0 +1,5 @@ + + +using AppBroker.Core.Extension; + +[assembly: Plugin()] \ No newline at end of file diff --git a/AppBroker.Mail/Plugin.cs b/AppBroker.Mail/Plugin.cs new file mode 100644 index 0000000..161dd6f --- /dev/null +++ b/AppBroker.Mail/Plugin.cs @@ -0,0 +1,81 @@ +using AppBroker.Core; +using AppBroker.Core.Devices; +using AppBroker.Core.Extension; +using AppBroker.Zigbee2Mqtt.Devices; + +using AppBrokerASP; + +using MailKit; +using MailKit.Net.Smtp; +using MailKit.Security; + + +using MimeKit; + +using NLog; + +using System.Runtime.InteropServices; + +namespace AppBroker.Zigbee2Mqtt; + +internal class Plugin : IPlugin +{ + private Logger logger; + private Dictionary lastReceivedTimer = new Dictionary(); + public int LoadOrder => int.MinValue; + + public string Name => "Mail"; + + + public bool Initialize(LogFactory logFactory) + { + logger = logFactory.GetCurrentClassLogger(); + IInstanceContainer.Instance.DeviceStateManager.StateChanged += DeviceStateManager_StateChanged; + + return true; + } + + private void DeviceStateManager_StateChanged(object? sender, StateChangeArgs e) + { + if (!IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(e.Id, out var device)) + return; + + if (device is not Zigbee2MqttDevice || !device.TypeNames.Contains("WSDCGQ11LM")) + return; + + ref var timer = ref CollectionsMarshal.GetValueRefOrAddDefault(lastReceivedTimer, e.Id, out var exists); + + if (!exists) + timer = new Timer(SendMail, device, TimeSpan.FromMinutes(120), Timeout.InfiniteTimeSpan); + else + timer!.Change(TimeSpan.FromMinutes(120), Timeout.InfiniteTimeSpan); + } + + private void SendMail(object? state) + { + Device? device = (Device)state; + Task.Run(async () => + { + using var mail = new MimeMessage(); + var builder = new BodyBuilder(); + mail.From.Add(new MailboxAddress("smarthome@susch.eu", "smarthome@susch.eu")); + mail.To.Add(MailboxAddress.Parse("mail@susch.eu")); + + mail.Subject = "Gerät sendet keine Daten"; + builder.TextBody = $"Das Gerät {device.FriendlyName} mit der Id {device.Id} scheint nicht mehr verbunden zu sein"; + builder.HtmlBody = $"Das Gerät {device.FriendlyName} mit der Id {device.Id} scheint nicht mehr verbunden zu sein"; + mail.Body = builder.ToMessageBody(); + + using var client = new SmtpClient(new ProtocolLogger(Console.OpenStandardOutput())); + client.CheckCertificateRevocation = true; + client.ServerCertificateValidationCallback = (_, __, ___, ____) => true; + + await client.ConnectAsync("mail.gallimathias.de", 465, useSsl: true); + await client.AuthenticateAsync("smarthome@susch.eu", "ocj+V}#R0c=>_`,R4:Ud'U;(e&qOZytoz3$Um]jpFxfR{1CN=YIph0x~.+? + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AppBroker.Notifications/AssemblyAttributes.cs b/AppBroker.Notifications/AssemblyAttributes.cs new file mode 100644 index 0000000..db45463 --- /dev/null +++ b/AppBroker.Notifications/AssemblyAttributes.cs @@ -0,0 +1,5 @@ + + +using AppBroker.Core.Extension; + +[assembly: Plugin()] \ No newline at end of file diff --git a/AppBroker.Notifications/Configuration/FirebaseConfig.cs b/AppBroker.Notifications/Configuration/FirebaseConfig.cs new file mode 100644 index 0000000..900f924 --- /dev/null +++ b/AppBroker.Notifications/Configuration/FirebaseConfig.cs @@ -0,0 +1,17 @@ +using AppBroker.Core.Configuration; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace AppBroker.Notifications.Configuration; +public record struct FirebaseOptions(string apiKey, string appId, string messagingSenderId, string projectId, string storageBucket, string? iosBundleId = null, string? authDomain = null); +public class FirebaseConfig : IConfig +{ + public string Name => "Firebase"; + + public Dictionary Options { get; set; } = new(); +} diff --git a/AppBroker.Notifications/Controller/NotificationController.cs b/AppBroker.Notifications/Controller/NotificationController.cs new file mode 100644 index 0000000..0b6720e --- /dev/null +++ b/AppBroker.Notifications/Controller/NotificationController.cs @@ -0,0 +1,66 @@ +using AppBroker.Core; +using AppBroker.Core.Configuration; +using AppBroker.Core.Database; +using AppBroker.Notifications.Configuration; +using AppBroker.Notifications.Hubs; + +using FirebaseAdmin.Messaging; + +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + + +namespace AppBroker.Notifications.Controller; + +public record AllOneTimeNotifications(List Topics); + +[Route("notification")] +public class NotificationController : ControllerBase +{ + private readonly IConfigManager configManager; + + public NotificationController(IConfigManager configManager) + { + this.configManager = configManager; + } + + [HttpPost("sendNotification")] + public async Task SendNotification([FromBody] AppNotification notification) + { + await AppNotificationService.SendNotificationToDevices(notification); + return Ok(); + } + + [HttpGet("firebaseOptions")] + public Dictionary? GetFirebaseOptions() + { + return configManager.PluginConfigs.OfType().FirstOrDefault()?.Options; + } + + [HttpGet("nextNotificationId")] + public string NextNotificationId(string uniqueName, long? deviceId) + { + var deviceIdStr = deviceId?.ToString() ?? ""; + var key = $"{uniqueName}{deviceIdStr} Unique Notification"; + using var ctx = DbProvider.BrokerDbContext; + var val = ctx.ConfigDatas.FirstOrDefault(x => x.Key == key); + + if (val is null) + { + val = ctx.ConfigDatas.Add(new() { Key = key, Value = $"{uniqueName}_{deviceIdStr}_{Guid.NewGuid()}" }).Entity; + ctx.SaveChanges(); + } + + return val.Value; + } + + [HttpGet("allOneTimeNotifications")] + public async Task AllOneTimeNotifications() + { + using var ctx = DbProvider.BrokerDbContext; + return new AllOneTimeNotifications(await ctx.ConfigDatas + .Where(x => EF.Functions.Like(x.Key, "% Unique Notification")) + .Select(x => x.Value) + .ToListAsync()); + } +} diff --git a/AppBroker.Notifications/Hubs/NotificationHub.cs b/AppBroker.Notifications/Hubs/NotificationHub.cs new file mode 100644 index 0000000..0eb3daf --- /dev/null +++ b/AppBroker.Notifications/Hubs/NotificationHub.cs @@ -0,0 +1,127 @@ +using AppBroker.Core; +using AppBroker.Core.Database; +using AppBroker.Notifications.Configuration; + +using FirebaseAdmin.Messaging; + +using Microsoft.AspNetCore.SignalR; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; + +namespace AppBroker.Notifications.Hubs; + + + +public class VisibleAppNotification : AppNotification +{ + public string Title { get; set; } + public string? Body { get; set; } + public int? TTL { get; set; } + public VisibleAppNotification(string title, string topic) : base(topic) + { + Title = title; + } + + public override Message? ConvertToFirebaseMessage() + { + return new Message() + { + Notification = new Notification { Title = Title, Body = Body ?? "" }, + Topic = Topic, + Android = new AndroidConfig { Priority = Priority.High, TimeToLive = TTL == null ? null : TimeSpan.FromSeconds(TTL.Value) } + }; + } +} + +public abstract class AppNotification +{ + public string TypeName => this.GetType().Name; + public string Topic { get; set; } + public bool WasOneTime { get; set; } + public AppNotification(string topic) + { + Topic = topic; + } + + public virtual Message? ConvertToFirebaseMessage() => null; +} + +public static partial class AppNotificationService +{ + internal static ConcurrentDictionary NotificationEnabledClients { get; } = new(); + internal static DynamicHub NotificationHub { get; set; } + + [GeneratedRegex("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")] + private static partial Regex EndsWithGuid(); + + + public static async Task SendNotificationToDevices(AppNotification notification) + { + var couldBeOneTime = EndsWithGuid().IsMatch(notification.Topic); + if (couldBeOneTime) + { + using var ctx = DbProvider.BrokerDbContext; + var val = ctx.ConfigDatas.FirstOrDefault(x => x.Value == notification.Topic); + + if (val is not null) + { + ctx.ConfigDatas.Remove(val); + ctx.SaveChanges(); + notification.WasOneTime = true; + } + } + + var firebaseMsg = notification.ConvertToFirebaseMessage(); + if (firebaseMsg is not null) + await FirebaseMessaging.DefaultInstance.SendAsync(firebaseMsg); + + + foreach (var item in NotificationEnabledClients.ToArray()) + { + if (item.Value.Item1.IsCancellationRequested) + { + NotificationEnabledClients.TryRemove(item); + continue; + } + item.Value.Item2.Notify(notification); + } + } +} + +public class NotificationHub +{ + public static Dictionary? GetFirebaseOptions() + { + return IInstanceContainer.Instance.ConfigManager.PluginConfigs.OfType().FirstOrDefault()?.Options; + } + + public static void Activate(DynamicHub hub) + { + if (AppNotificationService.NotificationHub != hub) + { + AppNotificationService.NotificationHub = hub; + } + if (!AppNotificationService.NotificationEnabledClients.ContainsKey(hub.Context.ConnectionId)) + { + AppNotificationService.NotificationEnabledClients[hub.Context.ConnectionId] = (hub.Context.ConnectionAborted, hub.Clients.Caller); + } + + var toDelete = AppNotificationService + .NotificationEnabledClients + .Where(x => x.Value.Item1.IsCancellationRequested) + .ToList(); + foreach (var item in toDelete) + { + AppNotificationService.NotificationEnabledClients.TryRemove(item); + } + } + +} + + diff --git a/AppBroker.Notifications/NotificationService.cs b/AppBroker.Notifications/NotificationService.cs new file mode 100644 index 0000000..db23939 --- /dev/null +++ b/AppBroker.Notifications/NotificationService.cs @@ -0,0 +1,139 @@ +using AppBroker.Core; +using AppBroker.Notifications.Hubs; + +using AppBrokerASP; + +using FirebaseAdmin.Messaging; + +using Microsoft.EntityFrameworkCore.ChangeTracking.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal; + +using System.Collections.Concurrent; +using System.Runtime.InteropServices; + +using Timer = System.Timers.Timer; + +namespace AppBroker.Notifications; + +/// +/// +/// +/// Will be used for string.Format. Should not be empy. Parameters are DeviceId and TypeName +/// +/// +/// +public record struct NotificationSetting( + + string TopicFormat, + string DisplayName, + long? DeviceId = null, + string? TypeName = null); + +public interface INotificationChecker +{ + /// + /// Completes the message to make it sendable + /// + /// Non complete pre filled message + /// Null if no message should be send, otherwise the complete message + Message? CreateMessage(Message message); + + IEnumerable GetNotificationSettings(); +} + + +public interface IStateChangeNotificationChecker : INotificationChecker +{ + bool ShouldNotify(StateChangeArgs e); +} + +public interface ITimedNotificationChecker : INotificationChecker +{ + DateTimeOffset GetNextNotificationTime(); +} + +public class NotificationService +{ + private readonly Dictionary> notificationCheckers = new(); + private readonly List notifyOnNextRun = new(); + private readonly Timer timer; + + internal NotificationService() + { + InstanceContainer.Instance.DeviceStateManager.StateChanged += StateManager_StateChanged; + timer = new Timer(TimeSpan.FromSeconds(5)); + timer.Elapsed += Timer_Elapsed; + } + + private void Timer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e) + { + foreach (var item in notifyOnNextRun) + { + foreach (var setting in item.GetNotificationSettings()) + { + var topic = string.Format(setting.TopicFormat, setting.DeviceId, setting.TypeName); + + var message = new Message() + { + Topic = topic, + Android = new AndroidConfig { Priority = Priority.High, TimeToLive = TimeSpan.FromSeconds(10) } + }; + //AppNotificationService.SendNotificationToDevices(new VisibleAppNotification()); + if (message is not null) + message = item.CreateMessage(message); + FirebaseMessaging.DefaultInstance.SendAsync(message); + } + } + notifyOnNextRun.Clear(); + + foreach (ITimedNotificationChecker item in notificationCheckers[typeof(ITimedNotificationChecker)]) + { + var next = item.GetNextNotificationTime(); + if (next > DateTimeOffset.UtcNow.AddSeconds(5)) + continue; + notifyOnNextRun.Add(item); + } + } + + private void StateManager_StateChanged(object? sender, StateChangeArgs e) + { + if (notificationCheckers.TryGetValue(typeof(IStateChangeNotificationChecker), out var items)) + { + foreach (IStateChangeNotificationChecker item in items) + { + if (item.ShouldNotify(e)) + { + var message = new Message() + { + Topic = InstanceContainer.Instance.ServerConfigManager.CloudConfig.ConnectionID + e.Id + e.PropertyName, + Android = new AndroidConfig { Priority = Priority.High, TimeToLive = TimeSpan.FromSeconds(10) } + }; + message = item.CreateMessage(message); + if (message is not null) + FirebaseMessaging.DefaultInstance.SendAsync(message); + } + } + } + } + + public void RegisterNotificationChecker(INotificationChecker checker) + { + foreach (var inter in checker.GetType().GetInterfaces()) + { + if (inter.IsAssignableFrom(typeof(INotificationChecker))) + { + ref var list = ref CollectionsMarshal.GetValueRefOrAddDefault(notificationCheckers, inter, out var exists); + if (!exists) + list = []; + list!.Add(checker); + } + } + + if (checker is ITimedNotificationChecker && !timer.Enabled) + { + timer.Start(); + } + } +} diff --git a/AppBroker.Notifications/Plugin.cs b/AppBroker.Notifications/Plugin.cs new file mode 100644 index 0000000..188b4a0 --- /dev/null +++ b/AppBroker.Notifications/Plugin.cs @@ -0,0 +1,45 @@ +using AppBroker.Core; +using AppBroker.Core.Devices; +using AppBroker.Core.Extension; +using AppBroker.Notifications.Hubs; + +using AppBrokerASP; +using AppBrokerASP.Extension; + +using FirebaseAdmin; +using FirebaseAdmin.Messaging; + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +using NLog; + +using System.Runtime.InteropServices; + +namespace AppBroker.Notifications; + +internal class Plugin : IPlugin +{ + private Logger logger; + + public string Name => "Notifications"; + public int LoadOrder => int.MinValue; + + public void RegisterTypes() + { + IInstanceContainer.Instance.RegisterDynamic(new NotificationService()); + } + + public bool Initialize(LogFactory logFactory) + { + logger = logFactory.GetCurrentClassLogger(); + + var app = FirebaseApp.Create(new AppOptions { Credential = Google.Apis.Auth.OAuth2.GoogleCredential.FromFile("service_account.json") }); + return true; + } +} + +internal class ServiceExtender : IServiceExtender +{ + public IEnumerable GetHubTypes() { yield return typeof(NotificationHub); } +} diff --git a/AppBroker.PainlessMesh.Elsa/AppBroker.PainlessMesh.Elsa.csproj b/AppBroker.PainlessMesh.Elsa/AppBroker.PainlessMesh.Elsa.csproj index fe70022..369ef75 100644 --- a/AppBroker.PainlessMesh.Elsa/AppBroker.PainlessMesh.Elsa.csproj +++ b/AppBroker.PainlessMesh.Elsa/AppBroker.PainlessMesh.Elsa.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/AppBroker.PainlessMesh/AppBroker.PainlessMesh.csproj b/AppBroker.PainlessMesh/AppBroker.PainlessMesh.csproj index f12db1e..a73e0db 100644 --- a/AppBroker.PainlessMesh/AppBroker.PainlessMesh.csproj +++ b/AppBroker.PainlessMesh/AppBroker.PainlessMesh.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable @@ -37,10 +37,14 @@ + + + + diff --git a/AppBroker.PainlessMesh/BinarySmarthomeMessage.cs b/AppBroker.PainlessMesh/BinarySmarthomeMessage.cs index 2634157..c46eb78 100644 --- a/AppBroker.PainlessMesh/BinarySmarthomeMessage.cs +++ b/AppBroker.PainlessMesh/BinarySmarthomeMessage.cs @@ -9,11 +9,13 @@ namespace AppBroker.PainlessMesh; public partial class BinarySmarthomeMessage : BaseSmarthomeMessage { public SmarthomeHeader Header { get; set; } - public override uint NodeId { get => base.NodeId; set => base.NodeId = value; } + //public override uint NodeId { get => base.NodeId; set => base.NodeId = value; } + [JsonProperty("m"), JsonConverter(typeof(StringEnumConverter))] public override MessageType MessageType { get => base.MessageType; set => base.MessageType = value; } - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(StringEnumConverter)), JsonProperty("c")] public override Command Command { get => base.Command; set => base.Command = value; } + public ByteLengthList Parameters { get; set; } //public List Parameters2 { get; set; } diff --git a/AppBroker.PainlessMesh/Hubs/PainlessHub.cs b/AppBroker.PainlessMesh/Hubs/PainlessHub.cs new file mode 100644 index 0000000..6970e41 --- /dev/null +++ b/AppBroker.PainlessMesh/Hubs/PainlessHub.cs @@ -0,0 +1,16 @@ +using AppBroker.Core; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.PainlessMesh.Hubs; +public class PainlessHub +{ + public static void UpdateTime() + { + IInstanceContainer.Instance.GetDynamic()?.UpdateTime(); + } +} diff --git a/AppBroker.PainlessMesh/Plugin.cs b/AppBroker.PainlessMesh/Plugin.cs index e73b083..99d375c 100644 --- a/AppBroker.PainlessMesh/Plugin.cs +++ b/AppBroker.PainlessMesh/Plugin.cs @@ -1,9 +1,12 @@ using AppBroker.Core; using AppBroker.Core.Extension; +using AppBroker.PainlessMesh.Hubs; using AppBroker.PainlessMesh.Ota; using AppBrokerASP; +using AppBrokerASP.Extension; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Configuration; using NLog; @@ -12,8 +15,8 @@ namespace AppBroker.PainlessMesh; internal class Plugin : IPlugin { public string Name => "Plainless Mesh"; + public int LoadOrder => int.MinValue; - public bool Initialize(LogFactory logFactory) { var cm = InstanceContainer.Instance.ConfigManager; @@ -34,9 +37,17 @@ public bool Initialize(LogFactory logFactory) if (painlessMeshConfig.Enabled) { - mqttManager.Connect().ContinueWith(_=> mqttManager.Subscribe()); + mqttManager.Connect().ContinueWith(_ => mqttManager.Subscribe()); mm.Start(um); } return true; } } + +internal class ServiceExtender : IServiceExtender +{ + public IEnumerable GetHubTypes() + { + yield return typeof(PainlessHub); + } +} \ No newline at end of file diff --git a/AppBroker.PainlessMesh/SerialClient.cs b/AppBroker.PainlessMesh/SerialClient.cs new file mode 100644 index 0000000..0192c07 --- /dev/null +++ b/AppBroker.PainlessMesh/SerialClient.cs @@ -0,0 +1,80 @@ + +using System; +using System.Collections.Generic; +using System.IO.Ports; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace AppBroker.PainlessMesh; +//internal class SerialClient +//{ +// public System.IO.Ports.SerialPort SerialPort { get; set; } + +// private CancellationTokenSource cts = new CancellationTokenSource(); +// private readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + +// private const int HeaderSize = sizeof(int) + sizeof(int) + sizeof(byte); + +// public SerialClient() +// { +// SerialPort = new SerialPort("/dev/ttyUSB1", 256000); +// SerialPort.Open(); +// _ = Task.Run(() => Read(cts.Token), cts.Token); +// this.logger = logger; +// } + +// public void Read(CancellationToken token) +// { +// while (!token.IsCancellationRequested) +// { +// try +// { + +// } +// catch (TimeoutException) { } +// } +// } + +// private bool ReadExactly(byte[] buffer, int offset, int count) +// { +// int read = offset; +// try +// { + +// do +// { +// if (count > 10000) +// return false; +// int ret = SerialPort.Read(buffer, read, count); +// if (ret <= 0) +// { +// return false; +// } +// read += ret; +// } while (read < count); +// } +// catch (IOException ioe) +// { +// logger.Error(ioe); +// return false; +// } +// return true; +// } + +// public void Send(PackageType packageType, Span data, uint nodeId) +// { +// if (!SerialPort.IsOpen) +// return; + +// Span buffer = stackalloc byte[HeaderSize + data.Length]; +// _ = BitConverter.TryWriteBytes(buffer, buffer.Length); +// _ = BitConverter.TryWriteBytes(buffer[sizeof(int)..], nodeId); +// buffer[sizeof(int) + sizeof(int)] = (byte)packageType; +// data.CopyTo(buffer[(sizeof(int) + sizeof(int) + sizeof(byte))..]); + +// SerialPort.Write(BitConverter.GetBytes(buffer.Length), 0, HeaderSize); +// SerialPort.BaseStream.Write(buffer); +// } +//} diff --git a/AppBroker.Windmill/AppBroker.Windmill.csproj b/AppBroker.Windmill/AppBroker.Windmill.csproj new file mode 100644 index 0000000..bdee09d --- /dev/null +++ b/AppBroker.Windmill/AppBroker.Windmill.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/AppBroker.Windmill/AssemblyAttributes.cs b/AppBroker.Windmill/AssemblyAttributes.cs new file mode 100644 index 0000000..db45463 --- /dev/null +++ b/AppBroker.Windmill/AssemblyAttributes.cs @@ -0,0 +1,5 @@ + + +using AppBroker.Core.Extension; + +[assembly: Plugin()] \ No newline at end of file diff --git a/AppBroker.Windmill/Configuration/WindmillConfig.cs b/AppBroker.Windmill/Configuration/WindmillConfig.cs new file mode 100644 index 0000000..51f44cf --- /dev/null +++ b/AppBroker.Windmill/Configuration/WindmillConfig.cs @@ -0,0 +1,11 @@ +using AppBroker.Core.Configuration; + + +namespace AppBroker.Windmill.Configuration; + +public class WindmillConfig : IConfig +{ + public string Name => "Windmill"; + + public string Url { get; set; } = "http://192.168.49.123:8100/api/r/stateChanged"; +} diff --git a/AppBroker.Windmill/Plugin.cs b/AppBroker.Windmill/Plugin.cs new file mode 100644 index 0000000..87b0afa --- /dev/null +++ b/AppBroker.Windmill/Plugin.cs @@ -0,0 +1,29 @@ +using AppBroker.Core; +using AppBroker.Core.Extension; + +using AppBrokerASP; + +using Microsoft.Extensions.Configuration; + +using NLog; + +namespace AppBroker.Windmill; + +internal class Plugin : IPlugin +{ + public string Name => "Windmill"; + public int LoadOrder => int.MinValue; + + public void RegisterTypes() + { + } + + public bool Initialize(LogFactory logFactory) + { + var forwarder = new StateForwarder(new HttpClient()); + IInstanceContainer.Instance.DeviceStateManager.StateChanged += (_,e)=> forwarder.Forward(e); + + + return true; + } +} diff --git a/AppBroker.Windmill/StateForwarder.cs b/AppBroker.Windmill/StateForwarder.cs new file mode 100644 index 0000000..cfaf396 --- /dev/null +++ b/AppBroker.Windmill/StateForwarder.cs @@ -0,0 +1,38 @@ +using AppBroker.Core; +using AppBroker.Windmill.Configuration; + +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http.Json; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; + +namespace AppBroker.Windmill +{ + internal class StateForwarder + { + private WindmillConfig? config; + private readonly HttpClient client; + + public StateForwarder(HttpClient client) + { + this.client = client; + } + + public async Task Forward(StateChangeArgs stateChange) + { + if (stateChange.OldValue == stateChange.NewValue) + return; + config ??= IInstanceContainer.Instance.ConfigManager.PluginConfigs.OfType().First(); + var json = JObject.FromObject(stateChange); + json.Add("IdHex", stateChange.Id.ToString("x2")); + using var sc = new StringContent(json.ToString(), Encoding.UTF8, MediaTypeNames.Application.Json); + await client.PostAsync(config.Url, sc, CancellationToken.None); + } + } +} diff --git a/AppBroker.Zigbee2Mqtt/AppBroker.Zigbee2Mqtt.csproj b/AppBroker.Zigbee2Mqtt/AppBroker.Zigbee2Mqtt.csproj index 930f5c0..c7017d2 100644 --- a/AppBroker.Zigbee2Mqtt/AppBroker.Zigbee2Mqtt.csproj +++ b/AppBroker.Zigbee2Mqtt/AppBroker.Zigbee2Mqtt.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable @@ -11,13 +11,13 @@ - - - - - - - - - + + + + + + + + + diff --git a/AppBroker.Zigbee2Mqtt/Devices/SuschHeater.cs b/AppBroker.Zigbee2Mqtt/Devices/SuschHeater.cs new file mode 100644 index 0000000..12615e7 --- /dev/null +++ b/AppBroker.Zigbee2Mqtt/Devices/SuschHeater.cs @@ -0,0 +1,185 @@ +using AppBroker.Core; +using AppBroker.Core.Devices; +using AppBroker.Core.Models; + +using Esprima.Ast; + +using Newtonsoft.Json.Linq; + +using System.Text; + + +namespace AppBroker.Zigbee2Mqtt.Devices; +[Flags] +internal enum DayOfWeekF +{ + Sunday = 1 << 0, + Monday = 1 << 1, + Tuesday = 1 << 2, + Wednesday = 1 << 3, + Thursday = 1 << 4, + Friday = 1 << 5, + Saturday = 1 << 6, + VacationOrAway = 1 << 7, +} + +internal class Setting +{ + public DayOfWeekF DayOfWeek { get; set; } + public ushort Time { get; set; } + public short Temperature { get; set; } + + public Setting() + { + + } + public Setting(Span bytes) + { + DayOfWeek = (DayOfWeekF)bytes[0]; + Time = (ushort)(bytes[2] << 8 | bytes[1]); + Temperature = (short)(bytes[4] << 8 | bytes[3]); + } +} + +[DeviceName("SuschHeater")] +internal class SuschHeater : Zigbee2MqttDevice +{ + public SuschHeater(Zigbee2MqttDeviceJson device, long id) : base(device, id) + { + } + + public SuschHeater(Zigbee2MqttDeviceJson device, long id, string typeName) : base(device, id, typeName) + { + } + + public override async Task UpdateFromApp(Command command, List parameters) + { + switch (command) + { + case Command.Temp: + float temp = (float)parameters[0]; + await zigbeeManager.SetValue(Id, "heating_setpoint", temp); + break; + case Command.Mode: + var val = parameters[0].ToString(); + await zigbeeManager.SetValue(Id, "system_mode", val); + break; + default: + await base.UpdateFromApp(command, parameters); + break; + } + } + + public override void OptionsFromApp(Command command, List parameters) + { + switch (command) + { + case Command.Temp: + { + var configs = parameters.Skip(1).Select(x => x.ToDeObject()).GroupBy(x => new { x.TimeOfDay, x.Temperature }); + + List settings = new List(); + + foreach (var item in configs) + { + var newSetting = new Setting(); + var key = item.Key; + newSetting.Temperature = (short)(key.Temperature * 100); + newSetting.Time = (ushort)(key.TimeOfDay.TimeOfDay.Hours * 60 + key.TimeOfDay.TimeOfDay.Minutes); + foreach (var hc in item) + { + newSetting.DayOfWeek |= (DayOfWeekF)(1 << ((byte)(hc.DayOfWeek + 1) % 7)); + } + settings.Add(newSetting); + } + + StringBuilder builder = new StringBuilder($$""" + { + "transitions": {{settings.Count}}, + "mode": 0 + """); + + for (int i = 0; i < 10; i++) + { + builder.Append(','); + int dow = 0; + int transitionTime = 0; + int setPoint = 0; + if (settings.Count > i) + { + var item = settings[i]; + dow = (int)item.DayOfWeek; + transitionTime = item.Time; + setPoint = item.Temperature; + } + builder.Append( + $$""" + "day_of_week_{{i + 1}}": {{dow}}, + "transition_time_{{i + 1}}": {{transitionTime}}, + "set_point_{{i + 1}}": {{setPoint}} + """ + ); + } + builder.Append("}"); + + zigbeeManager.SetCommand(Id, 0xff00, 0xff, builder.ToString()); + break; + } + } + + SendDataToAllSubscribers(); + } + + internal override Dictionary ConvertStates(Dictionary customStates) + { + if (customStates.TryGetValue("current_target_num", out var curTargetNum) + && curTargetNum.ToObject() is uint curTarget) + { + var temp = (curTarget >> 16); + var time = (int)((curTarget >> 4) & 0xFFF); + var dayOfWeek = (curTarget & 0xF); + + customStates["current_target_temp"] = temp / 100d; + customStates["current_target_time"] = new DateTime(DateOnly.MinValue, new TimeOnly(time / 60, time % 60), DateTimeKind.Utc); + customStates["current_target_dow"] = dayOfWeek; + } + if(customStates.TryGetValue("running_mode", out var runningMode)) + { + customStates["running_mode_b"] = runningMode.ToString() != "off"; + } + + return customStates; + } + + public override dynamic? GetConfig() + { + var currentConfig = GetState("current_config"); + if (currentConfig is null) + return null; + + var data = Convert.FromBase64String(currentConfig.ToString()); + + var numberOfTransitions = data[2]; + var settings = new Setting[numberOfTransitions]; + for (int i = 0; i < numberOfTransitions; i++) + { + settings[i] = new(data.AsSpan()[(4 + i * 5)..]); + } + + List configs = new List(); + + foreach (var item in settings) + { + var dow = (int)item.DayOfWeek; + for (int i = 0; i < 7; i += 1) + { + if (((1 << i) & dow) > 0) + { + configs.Add(new HeaterConfig((Core.Models.DayOfWeek)((i + 6) % 7), new DateTime(DateOnly.MinValue, new TimeOnly(item.Time / 60, item.Time % 60)), item.Temperature / 100d)); + } + } + + } + return Newtonsoft.Json.JsonConvert.SerializeObject(configs); + } +} diff --git a/AppBroker.Zigbee2Mqtt/Devices/Zigbee2MqttDevice.cs b/AppBroker.Zigbee2Mqtt/Devices/Zigbee2MqttDevice.cs index c005180..21184ec 100644 --- a/AppBroker.Zigbee2Mqtt/Devices/Zigbee2MqttDevice.cs +++ b/AppBroker.Zigbee2Mqtt/Devices/Zigbee2MqttDevice.cs @@ -234,9 +234,7 @@ protected override Context ExtendEngine(Context engine) protected override bool FriendlyNameChanging(string oldName, string newName) { -#if DEBUG - return false; -#endif + if (string.IsNullOrWhiteSpace(newName)) return false; try @@ -247,7 +245,9 @@ protected override bool FriendlyNameChanging(string oldName, string newName) return true; } logger.Info($"Trying to rename {oldName} to {newName} for device with id {Id}"); +#if !(DEBUG) client.EnqueueAsync("zigbee2mqtt/bridge/request/device/rename", $"{{\"from\": \"{oldName}\", \"to\": \"{newName}\"}}"); +#endif if (zigbeeManager.friendlyNameToIdMapping.TryGetValue(oldName, out var id) && id == Id && !zigbeeManager.friendlyNameToIdMapping.ContainsKey(newName) @@ -265,4 +265,9 @@ protected override bool FriendlyNameChanging(string oldName, string newName) } return false; } + + internal virtual Dictionary ConvertStates(Dictionary customStates) + { + return customStates; + } } diff --git a/AppBroker.Zigbee2Mqtt/Devices/ZigbeeLamp.cs b/AppBroker.Zigbee2Mqtt/Devices/ZigbeeLamp.cs index b0c50a4..504ca9a 100644 --- a/AppBroker.Zigbee2Mqtt/Devices/ZigbeeLamp.cs +++ b/AppBroker.Zigbee2Mqtt/Devices/ZigbeeLamp.cs @@ -36,7 +36,7 @@ public override async void OptionsFromApp(Command command, List paramete case Command.Delay: var transitionTime = parameters[0].ToObject(); SetState(nameof(transitionTime), transitionTime); - await zigbeeManager.SetValue(FriendlyName, "transition_time", transitionTime); + await zigbeeManager.SetValue(Id, "transition_time", transitionTime); break; } } @@ -48,22 +48,22 @@ public override async Task UpdateFromApp(Command command, List parameter case Command.Temp: var colorTemp = parameters[0].ToObject(); SetState(nameof(colorTemp), colorTemp); - await zigbeeManager.SetValue(FriendlyName, "color_temp", colorTemp); + await zigbeeManager.SetValue(Id, "color_temp", colorTemp); break; case Command.Brightness: var brightness = System.Math.Clamp(parameters[0].ToObject(), (byte)0, (byte)254); SetState(nameof(brightness), brightness); - await zigbeeManager.SetValue(FriendlyName, nameof(brightness), brightness); + await zigbeeManager.SetValue(Id, nameof(brightness), brightness); break; case Command.SingleColor: var state = true; SetState(nameof(state), state); - await zigbeeManager.SetValue(FriendlyName, nameof(state), "ON"); + await zigbeeManager.SetValue(Id, nameof(state), "ON"); break; case Command.Off: state = false; SetState(nameof(state), state); - await zigbeeManager.SetValue(FriendlyName, nameof(state), "OFF"); + await zigbeeManager.SetValue(Id, nameof(state), "OFF"); break; default: break; diff --git a/AppBroker.Zigbee2Mqtt/Devices/ZigbeeSwitch.cs b/AppBroker.Zigbee2Mqtt/Devices/ZigbeeSwitch.cs index eaee046..57705cd 100644 --- a/AppBroker.Zigbee2Mqtt/Devices/ZigbeeSwitch.cs +++ b/AppBroker.Zigbee2Mqtt/Devices/ZigbeeSwitch.cs @@ -23,12 +23,12 @@ public override async Task UpdateFromApp(Command command, List parameter { case Command.On: SetState("state", true); - await zigbeeManager.SetValue(FriendlyName, "state", "ON"); + await zigbeeManager.SetValue(Id, "state", "ON"); break; case Command.Off: SetState("state", false); - await zigbeeManager.SetValue(FriendlyName, "state", "OFF"); + await zigbeeManager.SetValue(Id, "state", "OFF"); break; case Command.None: Console.WriteLine(string.Join(",", parameters.Select(x => x.ToObject()))); diff --git a/AppBroker.Zigbee2Mqtt/Plugin.cs b/AppBroker.Zigbee2Mqtt/Plugin.cs index 773bc4a..c116567 100644 --- a/AppBroker.Zigbee2Mqtt/Plugin.cs +++ b/AppBroker.Zigbee2Mqtt/Plugin.cs @@ -12,6 +12,7 @@ namespace AppBroker.Zigbee2Mqtt; internal class Plugin : IPlugin { public string Name => "Zigbee2MQTT"; + public int LoadOrder => int.MinValue; public void RegisterTypes() { diff --git a/AppBroker.Zigbee2Mqtt/Zigbee2MqttManager.cs b/AppBroker.Zigbee2Mqtt/Zigbee2MqttManager.cs index e02a517..7237e00 100644 --- a/AppBroker.Zigbee2Mqtt/Zigbee2MqttManager.cs +++ b/AppBroker.Zigbee2Mqtt/Zigbee2MqttManager.cs @@ -16,6 +16,8 @@ using AppBroker.Core.DynamicUI; using ZigbeeConfig = AppBroker.Zigbee2Mqtt.Zigbee2MqttConfig; +using Quartz.Util; +using Microsoft.EntityFrameworkCore.Metadata.Internal; namespace AppBroker.Zigbee2Mqtt; @@ -46,7 +48,7 @@ public async Task Subscribe() try { logger.Debug("Subscribing to zigbee2mqtt topic"); - await MQTTClient.SubscribeAsync("zigbee2mqtt/#"); + await MQTTClient.SubscribeAsync($"{config.Topic}/#"); } catch (Exception ex) @@ -57,35 +59,62 @@ public async Task Subscribe() public Task SetOption(string name, string propName, JToken value) { - return MQTTClient.EnqueueAsync("zigbee2mqtt/bridge/request/device/options", $$"""{"id":{{name}}, "options":{"{{propName}}":{{value}}} }"""); + return MQTTClient.EnqueueAsync($"{config.Topic}/bridge/request/device/options", $$"""{"id":{{name}}, "options":{"{{propName}}":{{value}}} }"""); } - public async Task SetValue(string deviceName, string propertyName, JToken newValue) + public Task SetCommand(long deviceId, ushort cluster, byte command, JToken payload) + { + var body = + $$""" + { + "command": { + "cluster": {{cluster}}, + "command": {{command}}, + "payload": {{payload.ToString()}} + } + } + """; + var hexId = deviceId.ToString("x2"); + return MQTTClient.EnqueueAsync($"{config.Topic}/0x{hexId}/set", body); + } + + public Task SetValue(string deviceName, string propertyName, JToken newValue) { if (MQTTClient is null) - return false; + return Task.FromResult(false); + var deviceId = IInstanceContainer.Instance.DeviceManager.Devices.FirstOrDefault(x => x.Value.FriendlyName == deviceName).Key; + if (deviceId == default) + return Task.FromResult(false); + + return SetValue(deviceId, propertyName, newValue); - logger.Info($"Updating device {deviceName} state {propertyName} with new value {newValue}"); - await MQTTClient.EnqueueAsync($"zigbee2mqtt/{deviceName}/set/{propertyName}", newValue.ToString()); - return true; } - public Task SetValue(long deviceId, string propertyName, JToken newValue) + public async Task SetValue(long deviceId, string propertyName, JToken newValue) { - if (MQTTClient is null || IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out var device)) - return Task.FromResult(false); - return SetValue(device.FriendlyName, propertyName, newValue); + if (MQTTClient is null) + return false; + var hexId = deviceId.ToString("x2"); + + logger.Info($"Updating device 0x{hexId} state {propertyName} with new value {newValue}"); + + string val = newValue.Type switch + { + JTokenType.Float => newValue.Value().ToString(CultureInfo.InvariantCulture), + _ => newValue.ToString(), + }; + + await MQTTClient.EnqueueAsync($"{config.Topic}/0x{hexId}/set/{propertyName}", val); + return true; } public Task SetValue(Device device, string propertyName, JToken newValue) { - if (MQTTClient is null) - return Task.FromResult(false); - return SetValue(device.FriendlyName, propertyName, newValue); + return SetValue(device.Id, propertyName, newValue); } public Task EnqueueToZigbee(string path, JToken payload) { - return MQTTClient.EnqueueAsync($"zigbee2mqtt/{path}", payload.ToString()); + return MQTTClient.EnqueueAsync($"{config.Topic}/{path}", payload.ToString()); } public async Task Connect() @@ -93,7 +122,6 @@ public async Task Connect() logger.Debug("Connecting to mqtt"); if (MQTTClient is not null) { - logger.Debug("Already connected to mqtt, returing existing instance"); return MQTTClient; } @@ -170,7 +198,16 @@ private async Task Mqtt_ApplicationMessageReceivedAsync(MqttApplicationMessageRe } else if (topic == "bridge/devices") { - devices = JsonConvert.DeserializeObject(payload); + try + { + + devices = JsonConvert.DeserializeObject(payload); + } + catch (Exception) + { + + throw; + } using (var ctx = DbProvider.BrokerDbContext) { foreach (var item in devices!) @@ -178,10 +215,10 @@ private async Task Mqtt_ApplicationMessageReceivedAsync(MqttApplicationMessageRe var id = long.Parse(item.IEEEAddress[2..], NumberStyles.HexNumber); logger.Debug($"Trying to create new device {id}"); var dbDevice = ctx.Devices.FirstOrDefault(x => x.Id == id); - if (dbDevice is not null && !string.IsNullOrWhiteSpace(dbDevice.FriendlyName) && dbDevice.FriendlyName != item.FriendlyName) + if (dbDevice is not null && !string.IsNullOrWhiteSpace(dbDevice.FriendlyName) && !string.Equals(dbDevice.FriendlyName, item.FriendlyName, StringComparison.OrdinalIgnoreCase)) { logger.Info($"Friendly name of Zigbee2Mqtt Device {item.FriendlyName} does not match saved name {dbDevice.FriendlyName}, updating"); - await MQTTClient.EnqueueAsync("zigbee2mqtt/bridge/request/device/rename", $"{{\"from\": \"{item.IEEEAddress}\", \"to\": \"{dbDevice.FriendlyName}\"}}"); + await MQTTClient.EnqueueAsync($"{config.Topic}/bridge/request/device/rename", $"{{\"from\": \"{item.IEEEAddress}\", \"to\": \"{dbDevice.FriendlyName}\"}}"); item.FriendlyName = dbDevice.FriendlyName; } @@ -228,6 +265,10 @@ private async Task Mqtt_ApplicationMessageReceivedAsync(MqttApplicationMessageRe else if (topic == "bridge/extensions") { + } + else if (topic == "bridge/definitions") + { + } else if (topic.EndsWith("/availability", StringComparison.OrdinalIgnoreCase)) { @@ -244,10 +285,14 @@ private async Task Mqtt_ApplicationMessageReceivedAsync(MqttApplicationMessageRe { logger.Warn($"Couldn't set availability ({payload}) on {deviceName}"); } + } + else if (topic == "bridge/converters") + { + } else { - logger.Warn($"[{topic}] {payload}"); + logger.Trace($"[{topic}] {payload}"); if (devices is null) { logger.Trace($"Got state before device {topic}, is something wrong with the retained messages of the mqtt broker?"); @@ -293,6 +338,9 @@ private Dictionary ReplaceCustomStates(long id, Dictionary : Device where T : - INumber, - IAdditionOperators, - IDivisionOperators, IMinMaxValue -{ - private readonly Dictionary storedStates = new(); - private readonly Dictionary devices; - private readonly string ownPropertyName; - private readonly GroupingMode mode; - - public GroupingDevice(long nodeId, GroupingMode mode, string propName, params long[] ids) : base(nodeId) - { - this.mode = mode; - ownPropertyName = propName; - devices = ids.ToDictionary(x => x, _ => propName); - IInstanceContainer.Instance.DeviceStateManager.StateChanged += DeviceStateManager_StateChanged; - - } - - - public GroupingDevice(long nodeId, GroupingMode mode, string propName, params (string name, long id)[] ids) : base(nodeId) - { - this.mode = mode; - ownPropertyName = propName; - devices = ids.ToDictionary(x => x.id, x => x.name); - IInstanceContainer.Instance.DeviceStateManager.StateChanged += DeviceStateManager_StateChanged; - } - - private void DeviceStateManager_StateChanged(object? sender, StateChangeArgs e) - { - if (!devices.TryGetValue(e.Id, out var propName) || e.PropertyName != propName) - return; - - var value = e.NewValue.ToObject(); - storedStates[e.Id] = value; - - if(storedStates.Count != devices.Count) - { - foreach (var item in devices) - { - if (storedStates.ContainsKey(item.Key)) - continue; - - storedStates[item.Key] = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(item.Key, item.Value).ToObject(); - } - } - - - var newValue = mode switch - { - GroupingMode.Sum => storedStates.Values.Aggregate((x, y) => x + y), - GroupingMode.Min => storedStates.Values.Min(), - GroupingMode.Max => storedStates.Values.Max(), - GroupingMode.Avg => storedStates.Values.Aggregate((x, y) => x + y) / GenericCaster.Cast(storedStates.Count), - _ => default - }; - if (newValue != default) - { - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(Id, ownPropertyName, JToken.FromObject(newValue!)); - Console.WriteLine(newValue); - } - } -} diff --git a/AppBroker.susch/Plugin.cs b/AppBroker.susch/Plugin.cs deleted file mode 100644 index 08593a6..0000000 --- a/AppBroker.susch/Plugin.cs +++ /dev/null @@ -1,158 +0,0 @@ - -using AppBroker.Core; -using AppBroker.Core.Devices; -using AppBroker.Core.Extension; - -using NLog; - -using System.ComponentModel.DataAnnotations; - -namespace AppBroker.susch; - -public class ByteDevice : Device -{ - public bool? Something - { - get => IInstanceContainer.Instance.DeviceStateManager.GetSingleState(Id, "something")?.ToObject(); - set => IInstanceContainer.Instance.DeviceStateManager.SetSingleState(Id, "something", value); - } - - public ByteDevice(long nodeId, string? typeName) : base(nodeId, typeName) - { - } -} -internal class Plugin : IPlugin -{ - public string Name => "Zigbee2MQTT"; - private readonly List emptyParams = new(); - - - public bool Initialize(LogFactory logFactory) - { - - IInstanceContainer.Instance.DeviceStateManager.StateChanged += DeviceStateManager_StateChanged; - - //var add = new GroupingDevice(0xDDFF, GroupingMode.Avg, "temperature", 0x00158d0002c9ff2a, 0x00158d0002c7775e, 0x00158d0002ca01dc, 0x00158d0002ca02fb, 1234); - - //IInstanceContainer.Instance.DeviceManager.AddNewDevice(add); - - //var bd1 = new ByteDevice(0xFF00, "bytedevice"); - //var bd2 = new ByteDevice(0xFF01, "bytedevice"); - //var bd3 = new ByteDevice(0xFF02, "bytedevice"); - //var bd4 = new ByteDevice(0xFF03, "bytedevice"); - //var byteDevices = new[] { bd1, bd2, bd3, bd4 }; - //Task.Run(async () => { - // var random = new Random(); - // await Task.Delay(1000); - // while (true) - // { - // await Task.Delay(300); - // var next = random.Next(0, 4); - // var bd = byteDevices[next]; - // bd.Something = !(bd.Something ?? false); - // } - //}); - - //IInstanceContainer.Instance.DeviceManager.AddNewDevices(byteDevices); - //var byteGroup = new GroupingDevice(0xDEFF, GroupingMode.Min, "something", bd1.Id, bd2.Id, bd3.Id, bd4.Id); - - //IInstanceContainer.Instance.DeviceManager.AddNewDevice(byteGroup); - return true; - } - - const string ActionName = "action"; - - private void DeviceStateManager_StateChanged(object? sender, StateChangeArgs e) - { - if ((ulong)e.Id is 0x7cb03eaa0a0869d9 or 0xa4c1380aa5199538 - && e.PropertyName == "state" - && e.NewValue.ToObject() == false - && IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(e.Id, out Device? device)) - { - _ = device.UpdateFromApp(Command.On, emptyParams); //Simulate toggling to true via app - } - else if ((ulong)e.Id is 0x001788010c255322 - && e.PropertyName == ActionName - || e.PropertyName == "action_duration") - { - var toControlJson = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(e.Id, "controlId"); - long lamp = unchecked((long)0xbc33acfffe180f06), ledstrip = 763955710; - - if (toControlJson is null) - toControlJson = lamp; - - long controlId = toControlJson.ToObject(); - - - var gotDevice = IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(controlId, out var dev); - - var action = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(e.Id, ActionName).ToObject(); - switch (action) - { - case HueRemoteAction.on_press: - break; - case HueRemoteAction.on_hold: - if (controlId != lamp) - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(e.Id, "controlId", lamp, StateFlags.StoreLastState); - break; - case HueRemoteAction.on_press_release: - { - var currentState = (IInstanceContainer.Instance.DeviceStateManager.GetSingleState(lamp, "state") ?? false).ToObject(); - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(lamp, "state", currentState ? "OFF" : "ON", StateFlags.SendToThirdParty); - break; - } - case HueRemoteAction.on_hold_release: - break; - case HueRemoteAction.off_press: - break; - case HueRemoteAction.off_hold: - if (controlId != ledstrip) - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(e.Id, "controlId", ledstrip, StateFlags.StoreLastState); - break; - case HueRemoteAction.off_press_release: - { - var currentState = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(ledstrip, "colorMode"); - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(ledstrip, "colorMode", currentState.ToString() == "Off" ? "SingleColor" : "Off", StateFlags.SendToThirdParty); - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(ledstrip, "colorNumber", 4278190080, StateFlags.SendToThirdParty); -#if DEBUG - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(ledstrip, out var ledStripDev)) - ledStripDev.SendDataToAllSubscribers(); -#endif - break; - } - break; - case HueRemoteAction.off_hold_release: - break; - case HueRemoteAction.up_press: - case HueRemoteAction.up_hold: - { - var currentBrightness = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(controlId, "brightness").ToObject(); - var newBrightness = Math.Min(byte.MaxValue, currentBrightness + 10); - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(controlId, "brightness", newBrightness, StateFlags.SendToThirdParty); - break; - } - case HueRemoteAction.up_press_release: - break; - case HueRemoteAction.up_hold_release: - break; - case HueRemoteAction.down_press: - case HueRemoteAction.down_hold: - { - var currentBrightness = IInstanceContainer.Instance.DeviceStateManager.GetSingleState(controlId, "brightness").ToObject(); - var newBrightness = Math.Max(byte.MinValue, currentBrightness - 10); - IInstanceContainer.Instance.DeviceStateManager.SetSingleState(controlId, "brightness", newBrightness, StateFlags.SendToThirdParty); - break; - } - case HueRemoteAction.down_press_release: - break; - case HueRemoteAction.down_hold_release: - break; - } - } - } - - internal enum HueRemoteAction - { - on_press, on_hold, on_press_release, on_hold_release, off_press, off_hold, off_press_release, off_hold_release, up_press, up_hold, up_press_release, up_hold_release, down_press, down_hold, down_press_release, down_hold_release, recall_0, recall_1 - } -} diff --git a/AppBrokerASP/AppBrokerASP.csproj b/AppBrokerASP/AppBrokerASP.csproj index 2956128..35f7f7a 100644 --- a/AppBrokerASP/AppBrokerASP.csproj +++ b/AppBrokerASP/AppBrokerASP.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable preview enable @@ -22,6 +22,7 @@ + @@ -50,34 +51,36 @@ - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + + - - - - + + + + - - + + - - + @@ -102,6 +105,7 @@ + PreserveNewest diff --git a/AppBrokerASP/Configuration/ConfigManager.cs b/AppBrokerASP/Configuration/ConfigManager.cs index 120bd77..1abd8f6 100644 --- a/AppBrokerASP/Configuration/ConfigManager.cs +++ b/AppBrokerASP/Configuration/ConfigManager.cs @@ -1,5 +1,7 @@ using AppBroker.Core.Configuration; +using AppBrokerASP.Plugins; + namespace AppBrokerASP.Configuration; public class ConfigManager : IConfigManager @@ -11,6 +13,9 @@ public class ConfigManager : IConfigManager public HistoryConfig HistoryConfig { get; } public CloudConfig CloudConfig { get; } public DatabaseConfig DatabaseConfig { get; } + public IReadOnlyCollection PluginConfigs => pluginConfigs; + + private List pluginConfigs = new(); private static readonly string ConfigFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "appbroker"); private const string ZigbeeConfigName = "zigbee.json"; @@ -54,5 +59,11 @@ public ConfigManager() DatabaseConfig = new DatabaseConfig(); configuration.GetSection(DatabaseConfig.ConfigName).Bind(DatabaseConfig); + + foreach (var item in InstanceContainer.Instance.PluginLoader.Configs) + { + configuration.GetSection(item.Name).Bind(item); + pluginConfigs.Add(item); + } } } diff --git a/AppBrokerASP/DeviceLayouts/Clocked LED.json b/AppBrokerASP/DeviceLayouts/Clocked LED.json index cbe95e6..01ddd1e 100644 --- a/AppBrokerASP/DeviceLayouts/Clocked LED.json +++ b/AppBrokerASP/DeviceLayouts/Clocked LED.json @@ -1,6 +1,7 @@ { "UniqueName": "Clocked LED", "TypeName": "Clocked LED", + "IconName": "XiaomiTempSensor", "Ids": [], "DashboardDeviceLayout": { "DashboardProperties": [ diff --git a/AppBrokerASP/DeviceLayouts/ConnectedToggler.json b/AppBrokerASP/DeviceLayouts/ConnectedToggler.json index 68f140c..1f8fcba 100644 --- a/AppBrokerASP/DeviceLayouts/ConnectedToggler.json +++ b/AppBrokerASP/DeviceLayouts/ConnectedToggler.json @@ -1,6 +1,7 @@ { "UniqueName": "ConnectedToggler", "TypeName": "ConnectedToggler", + "IconName": "XiaomiTempSensor", "Ids": [], "DashboardDeviceLayout": { "DashboardProperties": [ diff --git a/AppBrokerASP/DeviceLayouts/ContactSensor.json b/AppBrokerASP/DeviceLayouts/ContactSensor.json new file mode 100644 index 0000000..61af0fb --- /dev/null +++ b/AppBrokerASP/DeviceLayouts/ContactSensor.json @@ -0,0 +1,147 @@ +{ + "Version": 2, + "UniqueName": "ContactSensorLayout", + "TypeNames": [ "SNZB-04", "WL-19DWZ" ], + "IconName": "XiaomiTempSensor", + "NotificationSetup": [ + { + "UniqueName": "DingDongNotification", + "TranslatableName": "Klingel", + "Global": false + }, + { + "UniqueName": "Fridge15MinutesNotification", + "TranslatableName": "Über 15 Minuten geöffnet", + "Global": false + } + ], + "DashboardDeviceLayout": { + "DashboardProperties": [ + { + "Name": "contact", + "Order": 2, + "RowNr": 1, + "EditInfo": { + "EditType": "toggle", + "EditCommand": "Update", + "EditParameter": [ + { + "Command": "zigbee", + "Value": true, + "Parameters": [ + ] + }, + { + "Command": "zigbee", + "Value": false, + "Parameters": [ + ] + } + ], + "Display": "Status", + "ActiveValue": false + } + }, + { + "Name": "id", + "Order": 2, + "RowNr": 2, + "Hex": true, + "ShowOnlyInDeveloperMode": true + } + ] + }, + "DetailDeviceLayout": { + "PropertyInfos": [ + { + "Name": "friendlyName", + "Order": 0, + "RowNr": 0, + "TextStyle": { + "FontSize": 25.0, + "FontFamily": "FontName", + "FontWeight": "bold", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none" + }, + { + "Name": "id", + "Order": 1, + "RowNr": 1, + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "bold", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "ShowOnlyInDeveloperMode": true + }, + { + "Name": "contact", + "Order": 0, + "RowNr": 2, + "DisplayName": "Geoeffnet: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none" + }, + { + "Name": "lastReceived", + "Order": 0, + "RowNr": 6, + "DisplayName": "Zuletzt empfangen: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "Format": "dd.MM.yyyy HH:mm:ss" + }, + { + "Name": "linkQuality", + "Order": 0, + "RowNr": 7, + "DisplayName": "Verbindungsqualität: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none" + } + ], + "TabInfos": [ + { + "Id": 0, + "IconName": "home", + "Order": 1, + "LinkedDevice": null + } + ], + "HistoryProperties": [ + { + "PropertyName": "contact", + "XAxisName": "Kontakt", + "UnitOfMeasurement": "", + "IconName": "power-swtich-59454", + "BrightThemeColor": 4292149248, + "DarkThemeColor": 4294922834, + "ChartType": "step" + } + ] + } +} \ No newline at end of file diff --git a/AppBrokerASP/DeviceLayouts/DemoWidget - Kopie.json b/AppBrokerASP/DeviceLayouts/DemoWidget - Kopie.json index 1528399..a26996f 100644 --- a/AppBrokerASP/DeviceLayouts/DemoWidget - Kopie.json +++ b/AppBrokerASP/DeviceLayouts/DemoWidget - Kopie.json @@ -1,101 +1,446 @@ { - "Version": 3, - "UniqueName": "TestDevice", - "TypeName": "TestDevice", + "Version": 2, + "UniqueName": "SuschHeater", + "IconName": "XiaomiTempSensor", + "TypeNames": [ + "SuschHeater" + ], "DashboardDeviceLayout": { "DashboardProperties": [ { - "Name": "id", - "Order": 1, - "RowNr": 2 + "Name": "current_target_temp", + "Order": 0, + "RowNr": 0, + "TextStyle": { + "FontSize": 22.0, + "FontFamily": "FontName", + "FontWeight": "bold", + "FontStyle": "normal" + }, + "UnitOfMeasurement": " °C" }, { - "Name": "speaking", - "EditParameter": [], + "Name": "current_target_time", + "Order": 0, + "RowNr": 1, + "Format": "HH:mm", + "UnitOfMeasurement": " Uhr" + }, + { + "Name": "system_mode", + "Order": 0, "EditInfo": { - "EditType": "button", - "Dialog": "HeaterConfig", - "Display": "Heizplan einstellen" - } + "EditType": "icon", + "MessageType": "Update", + "EditParameter": [ + { + "Command": "None", + "Value": "off", + "CodePoint": 62148, + "FontFamily": "MaterialIcons" + }, + { + "Command": "None", + "Value": "heat", + "Disable": true + }, + { + "Command": "None", + "Value": "auto", + "Disable": true + } + ] + }, + "SpecialType": "right" }, { - "Name": "targetTemp", - "RowNr": 3 + "Name": "running_mode", + "Order": 0, + "RowNr": 0, + "EditInfo": { + "EditType": "icon", + "MessageType": "Update", + "EditParameter": [ + { + "Command": "None", + "Value": "off", + "Disable": true + }, + { + "Command": "None", + "Value": "heat", + "CodePoint": 984437, + "FontFamily": "MaterialIcons", + "Size": 18.0 + }, + { + "Command": "None", + "Value": "cool", + "CodePoint": 57399, + "FontFamily": "MaterialIcons", + "Size": 18.0 + } + ] + }, + "SpecialType": "right" }, { - "Name": "dayOfWeek", - "RowNr": 4 + "Name": "used_temperature_source", + "Order": 0, + "EditInfo": { + "EditType": "icon", + "MessageType": "Update", + "EditParameter": [ + { + "Command": "None", + "Value": "none", + "CodePoint": 983712, + "FontFamily": "MaterialIcons", + "Color": 4294901760 + }, + { + "Command": "None", + "Value": "local", + "CodePoint": 983712, + "FontFamily": "MaterialIcons", + "Color": 4294967040 + }, + { + "Command": "None", + "Value": "remote", + "CodePoint": 57399, + "FontFamily": "MaterialIcons", + "Disable": true + } + ] + }, + "SpecialType": "right" }, { - "Name": "timeOfDay", - "RowNr": 5 + "Name": "id", + "Order": 2, + "RowNr": 3, + "Hex": true, + "ShowOnlyInDeveloperMode": true } ] }, "DetailDeviceLayout": { "PropertyInfos": [ + { + "Name": "id", + "Order": 1, + "RowNr": 60, + "EditParameter": [], + "EditInfo": { + "EditType": "button", + "Dialog": "HeaterConfig", + "Display": "Heizplan einstellen", + "EditParameter": [ + { + "Name": "icon", + "CodePoint": 58751, + "FontFamily": "MaterialIcons" + } + ] + }, + "TabInfoId": 0, + "SpecialType": "none", + "blurryCard": true, + "Expanded": true + }, + { + "Name": "friendlyName", + "Order": 0, + "RowNr": 0, + "TextStyle": { + "FontSize": 25.0, + "FontFamily": "FontName", + "FontWeight": "bold", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none" + }, + // { + // "Name": "id", + // "Order": 1, + // "RowNr": 1, + // "TextStyle": { + // "FontSize": 16.0, + // "FontFamily": "FontName", + // "FontWeight": "bold", + // "FontStyle": "normal" + // }, + // "TabInfoId": 0, + // "SpecialType": "none", + // "ShowOnlyInDeveloperMode": true + // }, + { + "Name": "system_mode", + "Order": 1, + "RowNr": 50, + "EditParameter": [], + "EditInfo": { + "EditType": "Toggle", + "MessageType": "Update", + "ActiveValue": "auto", + "EditParameter": [ + { + "Command": 10, + "Value": "off", + "DisplayName": "Heizung: ", + "Parameters": [ + "off" + ] + }, + { + "Command": 10, + "Value": "auto", + "DisplayName": "Heizung: ", + "Parameters": [ + "auto" + ] + }, + // { + // "Name": "icon", + // "CodePoint": 62757, + // "FontFamily": "Smarthome" + // } + //{ + // "Command": 10, + // "Value": "heat", + // "DisplayName": "Heizen", + // "Parameters": [ + // "heat" + // ] + //} + ] + }, + "TabInfoId": 0, + "SpecialType": "none", + "Expanded": true, + "blurryCard": true + }, + { + "Name": "current_target", + "Order": 0, + "RowNr": 10, + "DisplayName": "Aktuelles Ziel: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "Expanded": true, + "BlurryCard": true + }, { "Name": "temperature", + "Order": 0, + "RowNr": 20, + "DisplayName": "Ausgelesene Temparatur: ", + "UnitOfMeasurement": " °C", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "BlurryCard": true, + "Expanded": true + }, + { + "Name": "linkQuality", + "Order": 0, + "RowNr": 150, + "DisplayName": "Verbindungsqualität: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "ShowOnlyInDeveloperMode": true, + "BlurryCard": true, + "Expanded": true + }, + { + "Name": "update", + "Order": 0, + "RowNr": 70, + "DisplayName": "Installierte Version: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "ShowOnlyInDeveloperMode": true, + "Expanded": true, + "JsonPath": "$.installed_version", + "BlurryCard": true + }, + { + "Name": "update", + "Order": 0, + "RowNr": 80, + "DisplayName": "Letzte Version: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, "TabInfoId": 0, + "SpecialType": "none", + "ShowOnlyInDeveloperMode": true, + "Expanded": true, + "JsonPath": "$.latest_version", + "BlurryCard": true + }, + //{ + // "Name": "temperature", + // "Order": 2, + // "RowNr": 9, + // "UnitOfMeasurement": " °C", + // "EditParameter": [], + // "EditInfo": { + // "EditType": "Radial", + // "MessageType": "Update", + // "CurrentValueProp": "heating_setpoint", + // "Interval": 5, + // "RadiusFactor": 10.9, + // "ShowValueAbove": true, + // "StartAngle": 265, + // "EndAngle": 275, + // "TickOffset": 0.001, + // "CenterY": 5.45, + // "CenterX": 0.5, + // "LabelOffset": 0.005, + // "Thickness": 0.005, + // "Margin": 0, + // "MinorTickInterval": 5, + // "HeightFactor":0.2, + // "EditParameter": [ + // { + // "Command": 6 + // } + // ] + // }, + // "TabInfoId": 0, + // "Expanded": true, + // "SpecialType": "none" + //}, + // { + // "Name": "heating_setpoint", + // "Order": 0, + // "RowNr": 16, + // "DisplayName": "Manuelle Temperatur Einstellung: ", + // "TextStyle": { + // "FontSize": 16.0, + // "FontFamily": "FontName", + // "FontWeight": "normal", + // "FontStyle": "normal" + // }, + // "TabInfoId": 0, + // "SpecialType": "none" + // }, + { + "Name": "heating_setpoint", + "Order": 1, + "RowNr": 40, + "DisplayName": "Manuelle Temperatur Einstellung: ", "EditInfo": { - "EditType": "Radial", - "EditCommand": "Update", + "EditType": "AdvancedSlider", + "MessageType": "Update", + "Display": "", "EditParameter": [ { - "Command": 889, - "Value": false, - "Parameters": [] + "Command": 6, + "Parameters": [], + "Value": { + "Min": 5.0, + "Max": 35.0, + "Divisions": 30 + } } ], - "Min": 5.0, - "Max": 35.0, - "Interval": 5, - "StartAngle": 180, - "EndAngle": 0, "GradientColors": [ [ 255, - 0, - 255, + 33, + 150, 255 ], [ 255, - 0, - 0, - 0 + 255, + 193, + 7 ], [ 255, 255, - 0, - 0 + 67, + 54 ] ], - "Margin": 96, - "Displays": [ - { - "Text": "Id: ", - "Prop": "id" - }, - { - "Text": "Ziel: ", - "Prop": "targetTemp", - "Unit": " °C" - }, - { - "Text": "Aktuell: ", - "Prop": "temperature" - }, - { - "Text": "Ziel: ", - "Prop": "timeOfDay", - "Unit": " Uhr" - } - ] + "Interval": 5, + "MinorTickInterval": 4 }, - "UnitOfMeasurement": " °C" + "Precision": 1, + "TabInfoId": 0, + "SpecialType": "none", + "UnitOfMeasurement": " °C", + "Expanded": true, + "BlurryCard": true + }, + { + "Name": "lastReceivedFormatted", + "Order": 0, + "RowNr": 30, + "DisplayName": "Zuletzt empfangen: ", + "TextStyle": { + "FontSize": 16.0, + "FontFamily": "FontName", + "FontWeight": "normal", + "FontStyle": "normal" + }, + "TabInfoId": 0, + "SpecialType": "none", + "Format": "dd", + "BlurryCard": true, + "Expanded": true + } + ], + "TabInfos": [ + { + "Id": 0, + "IconName": "home", + "Order": 1, + "LinkedDevice": null } ] - } + }, + "NotificationSetup": [ + { + "UniqueName": "MyUniqueHeaterNotification", + "TranslatableName": "Einmalig über 21°C", + "Times": "1" + }, + { + "UniqueName": "MyUniqueHeaterNotification2", + "TranslatableName": "Wir deaktiviert" + } + ] } \ No newline at end of file diff --git a/AppBrokerASP/DeviceLayouts/DemoWidget.json b/AppBrokerASP/DeviceLayouts/DemoWidget.json index 7a60bc1..88a8022 100644 --- a/AppBrokerASP/DeviceLayouts/DemoWidget.json +++ b/AppBrokerASP/DeviceLayouts/DemoWidget.json @@ -1,6 +1,15 @@ { + "Version": 2, "UniqueName": "WeatherDevice", "TypeName": "WeatherDevice", + "IconName": "XiaomiTempSensor", + "NotificationSetup": [ + { + "UniqueName": "DingDongNotification", + "TranslatableName": "Klingel", + "Global": true + } + ], "DashboardDeviceLayout": { "DashboardProperties": [ { diff --git a/AppBrokerASP/DeviceLayouts/EditTemoWidget.json b/AppBrokerASP/DeviceLayouts/EditTemoWidget.json index fb9f764..c693d7a 100644 --- a/AppBrokerASP/DeviceLayouts/EditTemoWidget.json +++ b/AppBrokerASP/DeviceLayouts/EditTemoWidget.json @@ -1,5 +1,6 @@ { "UniqueName": "DemoEdit", + "IconName": "XiaomiTempSensor", "TypeNames": [ "DemoEdit" ], "DashboardDeviceLayout": { "DashboardProperties": [ @@ -413,28 +414,28 @@ } }, { - "Name": "current", - "Order": 2, - "RowNr": 4, - "EditInfo": { - "EditType": "input", - "EditCommand": "Update", - "EditParameter": [ - { - "Command": "None", - "Parameters": [ "Current" ], - "Value": { - "Min": -25.0, - "Max": 40.0, - "Divisions": 5 - } - } - ], - "Display": "Ein tolles Label", - "ActiveValue": true, - "HintText": "Toller Hint", - "KeyboardType": "number" - } + "Name": "current", + "Order": 2, + "RowNr": 4, + "EditInfo": { + "EditType": "input", + "EditCommand": "Update", + "EditParameter": [ + { + "Command": "None", + "Parameters": [ "Current" ], + "Value": { + "Min": -25.0, + "Max": 40.0, + "Divisions": 5 + } + } + ], + "Display": "Ein tolles Label", + "ActiveValue": true, + "HintText": "Toller Hint", + "KeyboardType": "number" + } }, { @@ -443,7 +444,7 @@ "RowNr": 4, "TabInfoId": 0, "EditInfo": { - "EditType": "slider", + "EditType": "Slider", "EditCommand": "Update", "EditParameter": [ { diff --git a/AppBrokerASP/DeviceLayouts/Lampe - AB32840.json b/AppBrokerASP/DeviceLayouts/Lampe - AB32840.json index 3f41523..85dda7d 100644 --- a/AppBrokerASP/DeviceLayouts/Lampe - AB32840.json +++ b/AppBrokerASP/DeviceLayouts/Lampe - AB32840.json @@ -1,6 +1,7 @@ { "Version": 1, "UniqueName": "Osram Lampe Lightify", + "IconName": "XiaomiTempSensor", "TypeNames": [ "AB32840" ], @@ -40,7 +41,7 @@ "Order": 4, "RowNr": 4, "ShowOnlyInDeveloperMode": true - }, + } ] }, "DetailDeviceLayout": { @@ -95,12 +96,12 @@ }, "SpecialType": "none" }, - + { "Name": "color_temp", "RowNr": 3, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", @@ -154,7 +155,7 @@ "Name": "brightness", "RowNr": 5, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", @@ -355,7 +356,7 @@ } ], "HistoryProperties": [ - + ] } } \ No newline at end of file diff --git a/AppBrokerASP/DeviceLayouts/Lampe - Sascha Schlafzimmer.json b/AppBrokerASP/DeviceLayouts/Lampe - Sascha Schlafzimmer.json index 7abaf59..2f4b3d6 100644 --- a/AppBrokerASP/DeviceLayouts/Lampe - Sascha Schlafzimmer.json +++ b/AppBrokerASP/DeviceLayouts/Lampe - Sascha Schlafzimmer.json @@ -1,6 +1,7 @@ { "Version": 2, "UniqueName": "Sascha Schlafzimmer Lampe", + "IconName": "XiaomiTempSensor", "Ids": [ -4885370955287228666 ], "DashboardDeviceLayout": { "DashboardProperties": [ @@ -38,7 +39,7 @@ "Order": 4, "RowNr": 4, "ShowOnlyInDeveloperMode": true - }, + } ] }, "DetailDeviceLayout": { @@ -93,12 +94,12 @@ }, "SpecialType": "none" }, - + { "Name": "color_temp", "RowNr": 4, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", @@ -152,7 +153,7 @@ "Name": "brightness", "RowNr": 6, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", diff --git a/AppBrokerASP/DeviceLayouts/Lampe.json b/AppBrokerASP/DeviceLayouts/Lampe.json index 86847d4..3f673a0 100644 --- a/AppBrokerASP/DeviceLayouts/Lampe.json +++ b/AppBrokerASP/DeviceLayouts/Lampe.json @@ -1,6 +1,7 @@ { "Version": 13, "UniqueName": "Ikea Lampe1", + "IconName": "XiaomiTempSensor", "TypeNames": [ "LED1624G9", "LED1732G11", @@ -45,7 +46,7 @@ "Order": 4, "RowNr": 4, "ShowOnlyInDeveloperMode": true - }, + } ] }, "DetailDeviceLayout": { @@ -100,12 +101,12 @@ }, "SpecialType": "none" }, - + { "Name": "color_temp", "RowNr": 3, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", @@ -159,7 +160,7 @@ "Name": "brightness", "RowNr": 5, "Order": 0, - "Expanded":true, + "Expanded": true, "EditInfo": { "EditType": "slider", "EditCommand": "Update", diff --git a/AppBrokerASP/DeviceLayouts/LampeFarbig.json b/AppBrokerASP/DeviceLayouts/LampeFarbig.json index 18998f3..247e78a 100644 --- a/AppBrokerASP/DeviceLayouts/LampeFarbig.json +++ b/AppBrokerASP/DeviceLayouts/LampeFarbig.json @@ -2,6 +2,7 @@ //"Id\":-8922170722047078453,\"TypeName\":\"": null, "UniqueName": "Ikea Lampe Farbig", "TypeName": "TRADFRI bulb E14 CWS opal 600lm", + "IconName": "XiaomiTempSensor", //"Ids": [ -8922170722047078453 ], "DashboardDeviceLayout": { "DashboardProperties": [ @@ -15,7 +16,7 @@ "RowNr": 0, "UnitOfMeasurement": " K" }, - + { "Name": "id", "Order": 2, @@ -144,10 +145,10 @@ "Order": 1, "LinkedDevice": null } - + ], "HistoryProperties": [ - + ] } } \ No newline at end of file diff --git a/AppBrokerASP/DeviceLayouts/MCCGQ11LM.json b/AppBrokerASP/DeviceLayouts/MCCGQ11LM.json index 6087d6a..8a243da 100644 --- a/AppBrokerASP/DeviceLayouts/MCCGQ11LM.json +++ b/AppBrokerASP/DeviceLayouts/MCCGQ11LM.json @@ -1,9 +1,10 @@ { "UniqueName": "MCCGQ11LM", "TypeName": "MCCGQ11LM", + "IconName": "XiaomiTempSensor", "DashboardDeviceLayout": { "DashboardProperties": [ - + { "Name": "opened", "RowNr": 2 diff --git a/AppBrokerASP/DeviceLayouts/Osvalla.json b/AppBrokerASP/DeviceLayouts/Osvalla.json index 3e9e276..7c45ac3 100644 --- a/AppBrokerASP/DeviceLayouts/Osvalla.json +++ b/AppBrokerASP/DeviceLayouts/Osvalla.json @@ -1,6 +1,7 @@ { "UniqueName": "OsvallaPanelLayout", "TypeName": "OsvallaPanel", + "IconName": "XiaomiTempSensor", "Ids": [], "DashboardDeviceLayout": { "DashboardProperties": [ @@ -174,7 +175,8 @@ "UnitOfMeasurement": " °C", "IconName": "XiaomiTempSensor", "BrightThemeColor": 4292149248, - "DarkThemeColor": 4294922834 + "DarkThemeColor": 4294922834, + "ChartType": "line" }, { "PropertyName": "humidity", @@ -182,7 +184,8 @@ "UnitOfMeasurement": " %", "IconName": "cloud", "BrightThemeColor": 4280902399, - "DarkThemeColor": 4286755327 + "DarkThemeColor": 4286755327, + "ChartType": "line" }, { "PropertyName": "pressure", @@ -190,7 +193,8 @@ "UnitOfMeasurement": " kPA", "IconName": "barometer", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" } ] } diff --git a/AppBrokerASP/DeviceLayouts/SPZB0001.json b/AppBrokerASP/DeviceLayouts/SPZB0001.json index 32b5e43..fcd0f5d 100644 --- a/AppBrokerASP/DeviceLayouts/SPZB0001.json +++ b/AppBrokerASP/DeviceLayouts/SPZB0001.json @@ -1,6 +1,7 @@ { "UniqueName": "SPZB0001", - "TypeName": "SPZB0001", + "TypeNames": [ "SPZB0001" ], + "IconName": "XiaomiTempSensor", "DashboardDeviceLayout": { "DashboardProperties": [ { diff --git a/AppBrokerASP/DeviceLayouts/SampleLayout.json b/AppBrokerASP/DeviceLayouts/SampleLayout.json index e81009d..bc94ed3 100644 --- a/AppBrokerASP/DeviceLayouts/SampleLayout.json +++ b/AppBrokerASP/DeviceLayouts/SampleLayout.json @@ -1,6 +1,7 @@ { "UniqueName": "DemoLayout", "TypeName": "Demo", + "IconName": "XiaomiTempSensor", "Ids": [ -1, -2 diff --git a/AppBrokerASP/DeviceLayouts/TuyaSwitchSensor.json b/AppBrokerASP/DeviceLayouts/TuyaSwitchSensor.json index 18a5d09..ee35dad 100644 --- a/AppBrokerASP/DeviceLayouts/TuyaSwitchSensor.json +++ b/AppBrokerASP/DeviceLayouts/TuyaSwitchSensor.json @@ -1,5 +1,6 @@ { "UniqueName": "TuyaLayout", + "IconName": "XiaomiTempSensor", "TypeNames": [ "TuyaSwitchSensor", "TS011F_plug_1" @@ -55,7 +56,7 @@ "EditParameter": [ { "Command": "On", - "Value" : true + "Value": true }, { "Command": "Off", @@ -63,9 +64,9 @@ } ], "ActiveValue": true, - "Display": "Status", + "Display": "Status" } - }, + } ] }, "DetailDeviceLayout": { @@ -189,7 +190,7 @@ "RowNr": 33, "DisplayName": "Status: " }, - + { "Name": "state", "Order": 1, @@ -218,7 +219,8 @@ "UnitOfMeasurement": " W", "IconName": "XiaomiTempSensor", "BrightThemeColor": 4292149248, - "DarkThemeColor": 4294922834 + "DarkThemeColor": 4294922834, + "ChartType": "line" }, { "PropertyName": "current", @@ -226,7 +228,8 @@ "UnitOfMeasurement": " A", "IconName": "cloud", "BrightThemeColor": 4280902399, - "DarkThemeColor": 4286755327 + "DarkThemeColor": 4286755327, + "ChartType": "line" }, { "PropertyName": "energy", @@ -234,7 +237,8 @@ "UnitOfMeasurement": " kWh", "IconName": "droplet-59403", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" }, { "PropertyName": "voltage", @@ -242,7 +246,8 @@ "UnitOfMeasurement": " V", "IconName": "flash-59419", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" }, { "PropertyName": "state", @@ -250,7 +255,8 @@ "UnitOfMeasurement": " ", "IconName": "flash-59419", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" } ] } diff --git a/AppBrokerASP/DeviceLayouts/XiaomiTempSensor - Sascha.json b/AppBrokerASP/DeviceLayouts/XiaomiTempSensor - Sascha.json index 1c7ca3b..9792f2c 100644 --- a/AppBrokerASP/DeviceLayouts/XiaomiTempSensor - Sascha.json +++ b/AppBrokerASP/DeviceLayouts/XiaomiTempSensor - Sascha.json @@ -1,7 +1,8 @@ { "UniqueName": "SaschaTempSensorLayout", + "IconName": "XiaomiTempSensor", "Ids": [ 6066005697233659 ], -"ShowOnlyInDeveloperMode":false, + "ShowOnlyInDeveloperMode": false, "DashboardDeviceLayout": { "DashboardProperties": [ { @@ -289,7 +290,8 @@ "UnitOfMeasurement": " °C", "IconName": "XiaomiTempSensor", "BrightThemeColor": 4292149248, - "DarkThemeColor": 4294922834 + "DarkThemeColor": 4294922834, + "ChartType": "line" }, { "PropertyName": "humidity", @@ -297,7 +299,8 @@ "UnitOfMeasurement": " %", "IconName": "cloud", "BrightThemeColor": 4280902399, - "DarkThemeColor": 4286755327 + "DarkThemeColor": 4286755327, + "ChartType": "line" }, { "PropertyName": "pressure", @@ -305,7 +308,8 @@ "UnitOfMeasurement": " kPA", "IconName": "barometer", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" } ] } diff --git a/AppBrokerASP/DeviceLayouts/XiaomiTempSensor.json b/AppBrokerASP/DeviceLayouts/XiaomiTempSensor.json index c255219..51158b4 100644 --- a/AppBrokerASP/DeviceLayouts/XiaomiTempSensor.json +++ b/AppBrokerASP/DeviceLayouts/XiaomiTempSensor.json @@ -1,6 +1,7 @@ { - "Version": 1, + "Version": 1, "UniqueName": "TempSensorLayout", + "IconName": "XiaomiTempSensor", "TypeNames": [ "XiaomiTempSensor", "lumi.weather", "WSDCGQ11LM" ], "DashboardDeviceLayout": { "DashboardProperties": [ @@ -268,7 +269,8 @@ "UnitOfMeasurement": " °C", "IconName": "XiaomiTempSensor", "BrightThemeColor": 4292149248, - "DarkThemeColor": 4294922834 + "DarkThemeColor": 4294922834, + "ChartType": "line" }, { "PropertyName": "humidity", @@ -276,7 +278,8 @@ "UnitOfMeasurement": " %", "IconName": "cloud", "BrightThemeColor": 4280902399, - "DarkThemeColor": 4286755327 + "DarkThemeColor": 4286755327, + "ChartType": "line" }, { "PropertyName": "pressure", @@ -284,7 +287,8 @@ "UnitOfMeasurement": " kPA", "IconName": "barometer", "BrightThemeColor": 4278241363, - "DarkThemeColor": 4278249078 + "DarkThemeColor": 4278249078, + "ChartType": "line" } ] } diff --git a/AppBrokerASP/Dockerfile b/AppBrokerASP/Dockerfile index fec7ed9..8d96dc7 100644 --- a/AppBrokerASP/Dockerfile +++ b/AppBrokerASP/Dockerfile @@ -1,6 +1,6 @@ #See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base WORKDIR /app EXPOSE 80 EXPOSE 443 @@ -8,7 +8,7 @@ EXPOSE 5055 ARG TARGETPLATFORM -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /src COPY ["Nuget.Config", "."] COPY ["AppBrokerASP/AppBrokerASP.csproj", "AppBrokerASP/"] diff --git a/AppBrokerASP/Extension/IServiceExtender.cs b/AppBrokerASP/Extension/IServiceExtender.cs new file mode 100644 index 0000000..64d42b3 --- /dev/null +++ b/AppBrokerASP/Extension/IServiceExtender.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.SignalR; + +namespace AppBrokerASP.Extension; + + +public interface IServiceExtender +{ + void UseEndpoints(IEndpointRouteBuilder configure) { } + void ConfigureServices(IServiceCollection serviceCollection) { } + IEnumerable GetHubTypes() { yield break; } +} diff --git a/AppBrokerASP/Histories/HistoryManager.cs b/AppBrokerASP/Histories/HistoryManager.cs index beaf722..3a45e3c 100644 --- a/AppBrokerASP/Histories/HistoryManager.cs +++ b/AppBrokerASP/Histories/HistoryManager.cs @@ -140,8 +140,8 @@ public HistoryRecord[] GetHistoryFor(long deviceId, string propertyName, DateTim var values = ctx.ValueBases .Where(x => x.HistoryValueId == histProp.Id - && x.Timestamp > start.ToUniversalTime() - && x.Timestamp < end.ToUniversalTime()) + && x.Timestamp > new DateTime(start.Ticks, DateTimeKind.Utc) + && x.Timestamp < new DateTime(end.Ticks, DateTimeKind.Utc)) .ToArray(); logger.Info($"Loaded {values.Length} values for {propertyName} of device {deviceId} from {start} to {end}"); diff --git a/AppBrokerASP/InstanceContainer.cs b/AppBrokerASP/InstanceContainer.cs index a88e9ec..d9c80cd 100644 --- a/AppBrokerASP/InstanceContainer.cs +++ b/AppBrokerASP/InstanceContainer.cs @@ -6,6 +6,7 @@ using AppBrokerASP.Configuration; using AppBrokerASP.Histories; using AppBrokerASP.Manager; +using AppBrokerASP.Plugins; using AppBrokerASP.State; namespace AppBrokerASP; @@ -23,12 +24,14 @@ public class InstanceContainer : IInstanceContainer, IDisposable public IConfigManager ConfigManager => ServerConfigManager; public ConfigManager ServerConfigManager { get; } + public PluginLoader PluginLoader { get; } private Dictionary dynamicObjects = new(); - public InstanceContainer() + public InstanceContainer(PluginLoader pluginLoader) { IInstanceContainer.Instance = Instance = this; + PluginLoader = pluginLoader; IconService = new IconService(); ServerConfigManager = new ConfigManager(); DeviceStateManager = new DeviceStateManager(); diff --git a/AppBrokerASP/Manager/DeviceTypeMetaDataManager.cs b/AppBrokerASP/Manager/DeviceTypeMetaDataManager.cs index 697384f..505eba7 100644 --- a/AppBrokerASP/Manager/DeviceTypeMetaDataManager.cs +++ b/AppBrokerASP/Manager/DeviceTypeMetaDataManager.cs @@ -36,6 +36,7 @@ public DeviceTypeMetaDataManager(DeviceManager manager) .GetExecutingAssembly() .GetTypes() .Where(x => typeof(Device).IsAssignableFrom(x) && x != typeof(Device)) + .Concat(InstanceContainer.Instance.PluginLoader.DeviceTypes) .ToHashSet(); foreach (var type in deviceTypes) diff --git a/AppBrokerASP/Plugins/GenericControllerFeatureProvider.cs b/AppBrokerASP/Plugins/GenericControllerFeatureProvider.cs index 6cfe2bf..3b471a3 100644 --- a/AppBrokerASP/Plugins/GenericControllerFeatureProvider.cs +++ b/AppBrokerASP/Plugins/GenericControllerFeatureProvider.cs @@ -12,7 +12,7 @@ public class GenericControllerFeatureProvider : IApplicationFeatureProvider parts, ControllerFeature feature) { - var pluginLoader = IInstanceContainer.Instance.GetDynamic< PluginLoader>(); + var pluginLoader = InstanceContainer.Instance.PluginLoader; foreach (var type in pluginLoader.ControllerTypes) { diff --git a/AppBrokerASP/Plugins/PluginLoader.cs b/AppBrokerASP/Plugins/PluginLoader.cs index 0c1a7b2..99d05b5 100644 --- a/AppBrokerASP/Plugins/PluginLoader.cs +++ b/AppBrokerASP/Plugins/PluginLoader.cs @@ -1,9 +1,12 @@  using AppBroker.Core; +using AppBroker.Core.Configuration; using AppBroker.Core.Devices; using AppBroker.Core.Extension; using AppBroker.Core.HelperMethods; +using AppBrokerASP.Extension; + using Microsoft.AspNetCore.Mvc; using NLog; @@ -17,8 +20,11 @@ namespace AppBrokerASP.Plugins; public class PluginLoader { - - public List ControllerTypes { get; } = new List(); + internal List ControllerTypes { get; } = new List(); + internal List AppConfigurators { get; } = new(); + internal List ServiceExtenders { get; } = new(); + internal List Configs { get; } = new(); + internal List DeviceTypes { get; } = new(); private readonly List plugins = new(); private readonly ILogger logger; @@ -43,12 +49,24 @@ internal void LoadPlugins(Assembly ass) } else if (typeof(Device).IsAssignableFrom(type)) { - IInstanceContainer.Instance.DeviceTypeMetaDataManager.RegisterDeviceType(type); + DeviceTypes.Add(type); } else if (typeof(ControllerBase).IsAssignableFrom(type)) { ControllerTypes.Add(type); } + else if (typeof(IAppConfigurator).IsAssignableFrom(type)) + { + AppConfigurators.Add((IAppConfigurator)Activator.CreateInstance(type)!); + } + else if (typeof(IServiceExtender).IsAssignableFrom(type)) + { + ServiceExtenders.Add((IServiceExtender)Activator.CreateInstance(type)!); + } + else if (typeof(IConfig).IsAssignableFrom(type)) + { + Configs.Add((IConfig)Activator.CreateInstance(type)!); + } } } } @@ -108,10 +126,10 @@ public void LoadAssemblies() public void InitializePlugins(LogFactory logFactory) { - foreach (IPlugin plugin in plugins) + foreach (IPlugin plugin in plugins.OrderBy(x=>x.LoadOrder)) plugin.RegisterTypes(); - foreach (IPlugin plugin in plugins) + foreach (IPlugin plugin in plugins.OrderBy(x => x.LoadOrder)) { if (!plugin.Initialize(logFactory)) logger.Warn($"Plugin {plugin.Name} had errors in initialization :("); diff --git a/AppBrokerASP/Program.cs b/AppBrokerASP/Program.cs index 6f98478..1e88f5e 100644 --- a/AppBrokerASP/Program.cs +++ b/AppBrokerASP/Program.cs @@ -17,6 +17,11 @@ using MQTTnet.Server; using MQTTnet; using Newtonsoft.Json; +using System.Reflection.Emit; +using System.Reflection; +using Microsoft.AspNetCore.SignalR; +using System.Runtime.InteropServices; +using Microsoft.AspNetCore.Builder; namespace AppBrokerASP; @@ -31,17 +36,16 @@ public class Program #endif public static ushort UsedPortForSignalR { get; private set; } + + public static void Main(string[] args) { Console.OutputEncoding = Encoding.UTF8; Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); - _ = new InstanceContainer(); - var pluginLoader = new PluginLoader(LogManager.LogFactory); - IInstanceContainer.Instance.RegisterDynamic(pluginLoader); - pluginLoader.LoadAssemblies(); + _ = new InstanceContainer(pluginLoader); _ = DeviceLayoutService.InstanceDeviceLayouts; @@ -126,6 +130,15 @@ public static void Main(string[] args) var startup = new Startup(webBuilder.Configuration); startup.ConfigureServices(webBuilder.Services); + List hubTypes = new(); + foreach (var extender in pluginLoader.ServiceExtenders) + { + extender.ConfigureServices(webBuilder.Services); + foreach (var type in extender.GetHubTypes()) + { + hubTypes.Add(type); + } + } WebApplication? app = webBuilder.Build(); _ = app.UseWebSockets(); @@ -133,12 +146,19 @@ public static void Main(string[] args) _ = app.UseRouting(); _ = app.UseStaticFiles(); + Type dynamicHub = GenerateDynamicHub(hubTypes, mainLogger); _ = app.UseEndpoints(e => { - _ = e.MapFallbackToPage("/_Host"); - _ = e.MapHub(pattern: "/SmartHome/{id}"); - _ = e.MapHub(pattern: "/SmartHome"); + //_ = e.MapFallbackToPage("/_Host"); _ = e.MapControllers(); + var mapHubMethod = typeof(HubEndpointRouteBuilderExtensions).GetMethod("MapHub", 1, new[] { typeof(IEndpointRouteBuilder), typeof(string) }); + _ = mapHubMethod.MakeGenericMethod(dynamicHub).Invoke(null, new object[] { e, "/Smarthome" }); + _ = mapHubMethod.MakeGenericMethod(dynamicHub).Invoke(null, new object[] { e, "/Smarthome/{id}" }); + + foreach (var extender in pluginLoader.ServiceExtenders) + { + extender.UseEndpoints(e); + } if (mqttConfig.Enabled) { @@ -153,8 +173,6 @@ public static void Main(string[] args) { _ = app.UseMqttServer(server => { - - static async Task Server_RetainedMessagesClearedAsync(EventArgs arg) => File.Delete(InstanceContainer.Instance.ConfigManager.MqttConfig.RetainedMessageFilePath); static Task Server_LoadingRetainedMessageAsync(LoadingRetainedMessagesEventArgs arg) { @@ -185,7 +203,16 @@ static Task Server_LoadingRetainedMessageAsync(LoadingRetainedMessagesEventArgs { InstanceContainer.Instance.JavaScriptEngineManager.Initialize(); } - + if (app.Environment.IsDevelopment()) + { + //app.UseSwagger(); + //app.UseSwaggerUI(); + app.UseOpenApi(c => + { + }); // serve documents (same as app.UseSwagger()) + //app.UseSwaggerUi3(); // serve Swagger UI + app.UseReDoc(); // serve ReDoc UI + } pluginLoader.InitializePlugins(LogManager.LogFactory); app.Run(); @@ -201,6 +228,7 @@ static Task Server_LoadingRetainedMessageAsync(LoadingRetainedMessagesEventArgs } } + private static IPEndPoint CreateIPEndPoint(string endPoint) { string[] ep = endPoint.Split(':'); @@ -251,4 +279,45 @@ private static void AdvertiseServerPortsViaMDNS(ushort port) mdns.Start(); } + + private static Type GenerateDynamicHub(List hubTypes, Logger mainLogger) + { + AssemblyName assemblyName = new AssemblyName("DynamicHubAssembly"); + AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); + ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("DynamicModule"); + TypeBuilder typeBuilder = moduleBuilder.DefineType("RuntimeHub", TypeAttributes.Public, typeof(DynamicHub)); + foreach (var hubType in hubTypes) + { + foreach (var method in hubType.GetMethods()) + { + if (method.DeclaringType != hubType || (method.Attributes & MethodAttributes.Private) > 0) + continue; + if ((method.Attributes & MethodAttributes.Static) == 0) + { + mainLogger.Warn("Method {0}.{1} was not static, only public static methods are supported", hubType.FullName, method.Name); + continue; + } + var parameters = method.GetParameters(); + bool passThis = false; + if (parameters.Length > 0 && parameters[0].ParameterType == typeof(DynamicHub)) + passThis = true; + MethodBuilder methodBuilder = typeBuilder.DefineMethod( + method.Name, + MethodAttributes.Public | MethodAttributes.HideBySig, + method.ReturnType, + parameters.Skip(passThis ? 1 : 0).Select(x => x.ParameterType).ToArray()); + + var gen = methodBuilder.GetILGenerator(); + + for (int i = 0; i < parameters.Length; i++) + { + gen.Emit(OpCodes.Ldarg, i + (passThis ? 0 : 1)); + } + gen.EmitCall(OpCodes.Call, method, null); + gen.Emit(OpCodes.Ret); + } + } + Type dynamicType = typeBuilder.CreateType(); + return dynamicType; + } } diff --git a/AppBrokerASP/Smarthome.cs b/AppBrokerASP/Smarthome.cs index 3386751..7b60143 100644 --- a/AppBrokerASP/Smarthome.cs +++ b/AppBrokerASP/Smarthome.cs @@ -17,203 +17,203 @@ namespace AppBrokerASP; -public class SmartHome : Hub -{ - public SmartHome() - { - } +//public class SmartHome : Hub +//{ +// public SmartHome() +// { +// } - public override Task OnConnectedAsync() => + //public override Task OnConnectedAsync() => //foreach (var item in IInstanceContainer.Instance.DeviceManager.Devices.Values) // item.SendLastData(Clients.Caller); - base.OnConnectedAsync(); - - public async Task Update(JsonSmarthomeMessage message) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(message.LongNodeId, out Device? device)) - { - switch (message.MessageType) - { - case MessageType.Get: - break; - case MessageType.Update: - await device.UpdateFromApp(message.Command, message.Parameters); - break; - case MessageType.Options: - device.OptionsFromApp(message.Command, message.Parameters); - break; - default: - break; - } - //Console.WriteLine($"User send command {message.Command} to {device} with {message.Parameters}"); - } - } - - public void UpdateDevice(long id, string newName) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? stored)) - { - stored.FriendlyName = newName; - _ = DbProvider.UpdateDeviceInDb(stored); - stored.SendDataToAllSubscribers(); - } - - } - - public dynamic? GetConfig(long deviceId) => IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device) ? device.GetConfig() : null; - - public async void SendUpdate(Device device) => await (Clients.All?.Update(device) ?? Task.CompletedTask); - - public List GetAllDevices() - { - var devices = IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).ToList(); - var dev = JsonConvert.SerializeObject(devices); - - return devices; - } - - public List GetHistoryPropertySettings() => IInstanceContainer.Instance.HistoryManager.GetHistoryProperties(); - public void SetHistory(bool enable, long id, string name) - { - if (enable) - IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); - else - IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); - } - public void SetHistories(bool enable, List ids, string name) - { - if (enable) - { - foreach (var id in ids) - IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); - } - else - { - foreach (var id in ids) - IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); - } - } - - public record struct DeviceOverview(long Id, string TypeName, IReadOnlyCollection TypeNames, string FriendlyName); - public List GetDeviceOverview() => IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).Select(x => new DeviceOverview(x.Id, x.TypeName, x.TypeNames, x.FriendlyName)).ToList(); - - - public Task> GetIoBrokerHistories(long id, string dt) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) - { - DateTime date = DateTime.Parse(dt).Date; - return device.GetHistory(date, date.AddDays(1).AddSeconds(-1)); - } - return Task.FromResult(new List()); - } - - public virtual Task GetIoBrokerHistory(long id, string dt, string propertyName) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) - { - DateTime date = DateTime.Parse(dt).Date; - return device.GetHistory(date, date.AddDays(1).AddSeconds(-1), propertyName); - } - return Task.FromResult(History.Empty); - } - - public Task> GetIoBrokerHistoriesRange(long id, string dt, string dt2) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) - { - return device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2)); - } - - return Task.FromResult(new List()); - } - - public virtual async Task GetIoBrokerHistoryRange(long id, string dt, string dt2, string propertyName) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) - { - return await device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2), propertyName) - ; - } - - return History.Empty; - } - - - public List Subscribe(List DeviceIds) - { - string connectionId = Context.ConnectionId; - var devices = new List(); - string? subMessage = "User subscribed to "; - foreach (long deviceId in DeviceIds) - { - - if (!IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) - continue; - - - if (!device.Subscribers.Any(x => x.ConnectionId == connectionId)) - device.Subscribers.Add(new Subscriber(connectionId, Clients.Caller)); - devices.Add(device); - subMessage += device.Id + "/" + device.FriendlyName + ", "; - } - Console.WriteLine(subMessage); - var dev = JsonConvert.SerializeObject(devices); - - return devices; - } - - public void Unsubscribe(List DeviceIds) - { - string connectionId = Context.ConnectionId; - var devices = new List(); - string? subMessage = "User unsubscribed from "; - foreach (long deviceId in DeviceIds) - { - if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) - { - - _ = device.Subscribers.RemoveWhere(x => x.ConnectionId == connectionId); - subMessage += device.Id + "/" + device.FriendlyName + ", "; - } - } - Console.WriteLine(subMessage); - } - - public void UpdateTime() - { - //TODO How to call smarthome mesh manager update time, without knowing the existence of said manager? - } - - public string GetHashCodeByTypeName(string typeName) => InstanceContainer.Instance.IconService.GetBestFitIcon(typeName).Hash; - public string GetHashCodeByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName).Hash; - - public SvgIcon GetIconByTypeName(string typename) => InstanceContainer.Instance.IconService.GetBestFitIcon(typename); - public SvgIcon GetIconByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName); - public SvgIcon GetIconByDeviceId(long deviceId) => InstanceContainer.Instance.IconService.GetBestFitIcon(InstanceContainer.Instance.DeviceManager.Devices[deviceId].TypeName); - - public void ReloadDeviceLayouts() => DeviceLayoutService.ReloadLayouts(); - public DeviceLayout? GetDeviceLayoutByName(string typename) => DeviceLayoutService.GetDeviceLayout(typename)?.layout; - public DeviceLayout? GetDeviceLayoutByDeviceId(long id) => DeviceLayoutService.GetDeviceLayout(id)?.layout; - public List GetAllDeviceLayouts() => DeviceLayoutService.GetAllLayouts(); - - public record LayoutNameWithHash(string Name, string Hash); - public LayoutNameWithHash? GetDeviceLayoutHashByDeviceId(long id) - { - - var layoutHash = DeviceLayoutService.GetDeviceLayout(id); - if (layoutHash is null || layoutHash.Value.layout is null) - return null; - - return new(layoutHash.Value.layout.UniqueName, layoutHash.Value.hash); - } + //base.OnConnectedAsync(); + + //public async Task Update(JsonSmarthomeMessage message) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(message.LongNodeId, out Device? device)) + // { + // switch (message.MessageType) + // { + // case MessageType.Get: + // break; + // case MessageType.Update: + // await device.UpdateFromApp(message.Command, message.Parameters); + // break; + // case MessageType.Options: + // device.OptionsFromApp(message.Command, message.Parameters); + // break; + // default: + // break; + // } + // //Console.WriteLine($"User send command {message.Command} to {device} with {message.Parameters}"); + // } + //} + + //public void UpdateDevice(long id, string newName) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? stored)) + // { + // stored.FriendlyName = newName; + // _ = DbProvider.UpdateDeviceInDb(stored); + // stored.SendDataToAllSubscribers(); + // } + + //} + + //public dynamic? GetConfig(long deviceId) => IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device) ? device.GetConfig() : null; + + //public async void SendUpdate(Device device) => await (Clients.All?.Update(device) ?? Task.CompletedTask); + + //public List GetAllDevices() + //{ + // var devices = IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).ToList(); + // var dev = JsonConvert.SerializeObject(devices); + + // return devices; + //} + + //public List GetHistoryPropertySettings() => IInstanceContainer.Instance.HistoryManager.GetHistoryProperties(); + //public void SetHistory(bool enable, long id, string name) + //{ + // if (enable) + // IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); + // else + // IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); + //} + //public void SetHistories(bool enable, List ids, string name) + //{ + // if (enable) + // { + // foreach (var id in ids) + // IInstanceContainer.Instance.HistoryManager.EnableHistory(id, name); + // } + // else + // { + // foreach (var id in ids) + // IInstanceContainer.Instance.HistoryManager.DisableHistory(id, name); + // } + //} + + //public record struct DeviceOverview(long Id, string TypeName, IReadOnlyCollection TypeNames, string FriendlyName); + //public List GetDeviceOverview() => IInstanceContainer.Instance.DeviceManager.Devices.Select(x => x.Value).Where(x => x.ShowInApp).Select(x => new DeviceOverview(x.Id, x.TypeName, x.TypeNames, x.FriendlyName)).ToList(); + + + //public Task> GetIoBrokerHistories(long id, string dt) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // DateTime date = DateTime.Parse(dt).Date; + // return device.GetHistory(date, date.AddDays(1).AddSeconds(-1)); + // } + // return Task.FromResult(new List()); + //} + + //public virtual Task GetIoBrokerHistory(long id, string dt, string propertyName) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // DateTime date = DateTime.Parse(dt).Date; + // return device.GetHistory(date, date.AddDays(1).AddSeconds(-1), propertyName); + // } + // return Task.FromResult(History.Empty); + //} + + //public Task> GetIoBrokerHistoriesRange(long id, string dt, string dt2) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // return device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2)); + // } + + // return Task.FromResult(new List()); + //} + + //public virtual async Task GetIoBrokerHistoryRange(long id, string dt, string dt2, string propertyName) + //{ + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out Device? device)) + // { + // return await device.GetHistory(DateTime.Parse(dt), DateTime.Parse(dt2), propertyName) + // ; + // } + + // return History.Empty; + //} + + + //public List Subscribe(List DeviceIds) + //{ + // string connectionId = Context.ConnectionId; + // var devices = new List(); + // string? subMessage = "User subscribed to "; + // foreach (long deviceId in DeviceIds) + // { + + // if (!IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) + // continue; + + + // if (!device.Subscribers.Any(x => x.ConnectionId == connectionId)) + // device.Subscribers.Add(new Subscriber(connectionId, Clients.Caller)); + // devices.Add(device); + // subMessage += device.Id + "/" + device.FriendlyName + ", "; + // } + // Console.WriteLine(subMessage); + // var dev = JsonConvert.SerializeObject(devices); + + // return devices; + //} + + //public void Unsubscribe(List DeviceIds) + //{ + // string connectionId = Context.ConnectionId; + // var devices = new List(); + // string? subMessage = "User unsubscribed from "; + // foreach (long deviceId in DeviceIds) + // { + // if (IInstanceContainer.Instance.DeviceManager.Devices.TryGetValue(deviceId, out Device? device)) + // { + + // _ = device.Subscribers.RemoveWhere(x => x.ConnectionId == connectionId); + // subMessage += device.Id + "/" + device.FriendlyName + ", "; + // } + // } + // Console.WriteLine(subMessage); + //} + + //public void UpdateTime() + //{ + // //TODO How to call smarthome mesh manager update time, without knowing the existence of said manager? + //} + + //public string GetHashCodeByTypeName(string typeName) => InstanceContainer.Instance.IconService.GetBestFitIcon(typeName).Hash; + //public string GetHashCodeByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName).Hash; + + //public SvgIcon GetIconByTypeName(string typename) => InstanceContainer.Instance.IconService.GetBestFitIcon(typename); + //public SvgIcon GetIconByName(string iconName) => InstanceContainer.Instance.IconService.GetIconByName(iconName); + //public SvgIcon GetIconByDeviceId(long deviceId) => InstanceContainer.Instance.IconService.GetBestFitIcon(InstanceContainer.Instance.DeviceManager.Devices[deviceId].TypeName); + + //public void ReloadDeviceLayouts() => DeviceLayoutService.ReloadLayouts(); + //public DeviceLayout? GetDeviceLayoutByName(string typename) => DeviceLayoutService.GetDeviceLayout(typename)?.layout; + //public DeviceLayout? GetDeviceLayoutByDeviceId(long id) => DeviceLayoutService.GetDeviceLayout(id)?.layout; + //public List GetAllDeviceLayouts() => DeviceLayoutService.GetAllLayouts(); + + //public record LayoutNameWithHash(string Name, string Hash); + //public LayoutNameWithHash? GetDeviceLayoutHashByDeviceId(long id) + //{ + + // var layoutHash = DeviceLayoutService.GetDeviceLayout(id); + // if (layoutHash is null || layoutHash.Value.layout is null) + // return null; + + // return new(layoutHash.Value.layout.UniqueName, layoutHash.Value.hash); + //} //public DashboardDeviceLayout? GetDashboardDeviceLayoutByName(string typename) => DeviceLayoutService.GetDashboardDeviceLayout(typename); //public DashboardDeviceLayout? GetDashboardDeviceLayoutByDeviceId(long id) => DeviceLayoutService.GetDashboardDeviceLayout(id); //public DetailDeviceLayout? GetDetailDeviceLayoutByName(string typename) => DeviceLayoutService.GetDetailDeviceLayout(typename); //public DetailDeviceLayout? GetDetailDeviceLayoutByDeviceId(long id) => DeviceLayoutService.GetDetailDeviceLayout(id); -} +//} diff --git a/AppBrokerASP/Startup.cs b/AppBrokerASP/Startup.cs index 3f3836a..f5599be 100644 --- a/AppBrokerASP/Startup.cs +++ b/AppBrokerASP/Startup.cs @@ -8,6 +8,9 @@ using AppBroker.Core.Javascript; using AppBroker.Core; using AppBrokerASP.Plugins; +using Newtonsoft.Json.Linq; +using Microsoft.OpenApi.Models; +using Microsoft.AspNetCore.OpenApi; namespace AppBrokerASP; @@ -25,6 +28,27 @@ public void ConfigureServices(IServiceCollection services) // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.MinimumSameSitePolicy = SameSiteMode.None); + + //services.AddEndpointsApiExplorer(); + //services.AddSwaggerGen((c) => + //{ + // c.MapType(() => new OpenApiSchema() + // { + // OneOf = [ + // new OpenApiSchema() { Type = "object" }, + // new OpenApiSchema() { Type = "number" }, + // new OpenApiSchema() { Type = "integer" }, + // new OpenApiSchema() { Type = "boolean" }, + // new OpenApiSchema() { Type = "array" }, + // new OpenApiSchema() { Type = "string" }, + // ], + // Nullable = true + // }); + // c.SupportNonNullableReferenceTypes(); + // c.UseAllOfToExtendReferenceSchemas(); + // //opt.MapType(() => new OpenApiSchema { Type = typeof(JToken).Name }); + //}); + _ = services.AddCors(options => options.AddPolicy("CorsPolicy", builder => _ = builder .AllowAnyMethod() .AllowAnyHeader() @@ -32,6 +56,11 @@ public void ConfigureServices(IServiceCollection services) services .AddControllers() + .AddNewtonsoftJson((c) => + { + c.SerializerSettings.TypeNameHandling = Newtonsoft.Json.TypeNameHandling.Auto; + c.SerializerSettings.MetadataPropertyHandling = Newtonsoft.Json.MetadataPropertyHandling.ReadAhead; + }) .ConfigureApplicationPartManager(manager => { manager.FeatureProviders.Add(new GenericControllerFeatureProvider()); @@ -48,7 +77,14 @@ public void ConfigureServices(IServiceCollection services) signalRBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); - _ = services.AddRazorPages(); + //_ = services.AddRazorPages(); + //services.AddOpenApi("appbroker"); + //services.AddOpenApiDocument(); + services.AddSwaggerDocument(c => + { + c.RequireParametersWithoutDefault = true; + + }); _ = services.AddSingleton(); var container = InstanceContainer.Instance; diff --git a/AppBrokerASP/State/DeviceStateManager.cs b/AppBrokerASP/State/DeviceStateManager.cs index 1106b2b..cfa7374 100644 --- a/AppBrokerASP/State/DeviceStateManager.cs +++ b/AppBrokerASP/State/DeviceStateManager.cs @@ -93,10 +93,10 @@ public void SetSingleState(long id, string propertyName, JToken newVal, StateFla { propertyName = MapValueName(id, propertyName); InstanceContainer.Instance.DeviceManager.Devices.TryGetValue(id, out var device); + JToken? oldValue = null; if (deviceStates.TryGetValue(id, out var oldState)) { - if (oldState.ContainsKey(propertyName) && JToken.DeepEquals(oldState[propertyName], newVal)) return; @@ -104,6 +104,7 @@ public void SetSingleState(long id, string propertyName, JToken newVal, StateFla InstanceContainer.Instance.HistoryManager.StoreNewState(id, propertyName, oldVal, newVal); AddStatesForBackwartsCompatibilityForOldApp(id, propertyName, newVal); + oldState.TryGetValue(propertyName, out oldValue); oldState[propertyName] = newVal; } else @@ -115,16 +116,19 @@ public void SetSingleState(long id, string propertyName, JToken newVal, StateFla } if ((stateFlags & StateFlags.NotifyOfStateChange) > 0) - StateChanged?.Invoke(this, new(id, propertyName, null, newVal)); + Task.Run(() => StateChanged?.Invoke(this, new(id, propertyName, oldValue, newVal))); - if (device is not null && (stateFlags & StateFlags.SendToThirdParty) > 0) - device.ReceivedNewState(propertyName, newVal, stateFlags); + if (device is not null) + { + if ((stateFlags & StateFlags.SendToThirdParty) > 0) + device.ReceivedNewState(propertyName, newVal, stateFlags); - if (device is not null && (stateFlags & StateFlags.SendDataToApp) > 0) - device.StateDataUpdated(); + if ((stateFlags & StateFlags.SendDataToApp) > 0) + device.StateDataUpdated(); - if (device is not null && (stateFlags & StateFlags.StoreLastState) > 0) - StoreLastState(id, deviceStates[id], device); + if ((stateFlags & StateFlags.StoreLastState) > 0) + StoreLastState(id, deviceStates[id], device); + } } /// diff --git a/DbMigrator/DbMigrator.csproj b/DbMigrator/DbMigrator.csproj index c66119b..fd2bc07 100644 --- a/DbMigrator/DbMigrator.csproj +++ b/DbMigrator/DbMigrator.csproj @@ -2,24 +2,24 @@ Exe - net8.0 + net9.0 True - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + - + diff --git a/IoBrokerHistoryImporter/IoBrokerHistoryImporter.csproj b/IoBrokerHistoryImporter/IoBrokerHistoryImporter.csproj index 192ccdb..98bc015 100644 --- a/IoBrokerHistoryImporter/IoBrokerHistoryImporter.csproj +++ b/IoBrokerHistoryImporter/IoBrokerHistoryImporter.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable diff --git a/TestGenerator/TestGenerator.csproj b/TestGenerator/TestGenerator.csproj index 22c905b..b78e67e 100644 --- a/TestGenerator/TestGenerator.csproj +++ b/TestGenerator/TestGenerator.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable diff --git a/Tools/create_ca.sh b/Tools/create_ca.sh new file mode 100644 index 0000000..9fd1262 --- /dev/null +++ b/Tools/create_ca.sh @@ -0,0 +1,8 @@ +CANAME=Smarthome +# optional, create a directory +mkdir $CANAME +cd $CANAME +# generate aes encrypted private key +openssl genrsa -aes256 -out $CANAME.key 4096 +# create certificate, 1826 days = 5 years +openssl req -x509 -new -nodes -key $CANAME.key -sha256 -days 36500 -out $CANAME.crt -subj '/CN=Smarthome Root CA/C=DE/ST=/L=/O=susch' diff --git a/Tools/create_cert.sh b/Tools/create_cert.sh new file mode 100644 index 0000000..ca472a0 --- /dev/null +++ b/Tools/create_cert.sh @@ -0,0 +1,17 @@ +CANAME=Smarthome +cd $CANAME +# create certificate for service +MYCERT=wildcard +openssl req -new -nodes -out $MYCERT.csr -newkey rsa:4096 -keyout $MYCERT.key -subj "/CN=${MYCERT}/C=DE/ST=/L=/O=susch" +# create a v3 ext file for SAN properties +cat > $MYCERT.v3.ext << EOF +authorityKeyIdentifier=keyid,issuer +basicConstraints=CA:FALSE +keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment +subjectAltName = @alt_names +[alt_names] +DNS.1 = * +IP.1 = 192.168.49.22 +EOF +openssl x509 -req -in $MYCERT.csr -CA $CANAME.crt -CAkey $CANAME.key -CAcreateserial -out $MYCERT.crt -days 730 -sha256 -extfile $MYCERT.v3.ext +openssl pkcs12 -export -out $MYCERT.pfx -inkey $MYCERT.key -in $MYCERT.crt \ No newline at end of file diff --git a/TranslateTTMToHumanReadable/TranslateTTMToHumanReadable.csproj b/TranslateTTMToHumanReadable/TranslateTTMToHumanReadable.csproj index a97540f..ddee98c 100644 --- a/TranslateTTMToHumanReadable/TranslateTTMToHumanReadable.csproj +++ b/TranslateTTMToHumanReadable/TranslateTTMToHumanReadable.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 true