diff --git a/.vs/hass-workstation-service/DesignTimeBuild/.dtbcache.v2 b/.vs/hass-workstation-service/DesignTimeBuild/.dtbcache.v2
index 27b853f..a1b0305 100644
Binary files a/.vs/hass-workstation-service/DesignTimeBuild/.dtbcache.v2 and b/.vs/hass-workstation-service/DesignTimeBuild/.dtbcache.v2 differ
diff --git a/.vs/hass-workstation-service/v16/.suo b/.vs/hass-workstation-service/v16/.suo
index e8ad257..1ef96ae 100644
Binary files a/.vs/hass-workstation-service/v16/.suo and b/.vs/hass-workstation-service/v16/.suo differ
diff --git a/README.md b/README.md
index a57a8bf..efa3bf1 100644
--- a/README.md
+++ b/README.md
@@ -40,4 +40,4 @@ This sensor watches the UserNotificationState. This is normally used in applicat
### Dummy
-This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.
\ No newline at end of file
+This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.
diff --git a/UserInterface/App.axaml b/UserInterface/App.axaml
index 172252f..1e1a085 100644
--- a/UserInterface/App.axaml
+++ b/UserInterface/App.axaml
@@ -8,6 +8,7 @@
-
+
+
diff --git a/UserInterface/UserInterface.csproj b/UserInterface/UserInterface.csproj
index 100580f..3938707 100644
--- a/UserInterface/UserInterface.csproj
+++ b/UserInterface/UserInterface.csproj
@@ -1,4 +1,4 @@
-
+
WinExe
netcoreapp3.1
@@ -12,6 +12,7 @@
+
diff --git a/UserInterface/Util/OpenBrowser.cs b/UserInterface/Util/OpenBrowser.cs
new file mode 100644
index 0000000..f1c2fff
--- /dev/null
+++ b/UserInterface/Util/OpenBrowser.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace UserInterface.Util
+{
+ public class BrowserUtil
+ {
+ public static void OpenBrowser(string url)
+ {
+ try
+ {
+ Process.Start(url);
+ }
+ catch
+ {
+ // hack because of this: https://github.com/dotnet/corefx/issues/10361
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ url = url.Replace("&", "^&");
+ Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
+ {
+ Process.Start("xdg-open", url);
+ }
+ else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ {
+ Process.Start("open", url);
+ }
+ else
+ {
+ throw;
+ }
+ }
+ }
+ }
+}
diff --git a/UserInterface/ViewModels/AddSensorViewModel.cs b/UserInterface/ViewModels/AddSensorViewModel.cs
new file mode 100644
index 0000000..1fd95bf
--- /dev/null
+++ b/UserInterface/ViewModels/AddSensorViewModel.cs
@@ -0,0 +1,28 @@
+using hass_workstation_service.Communication.InterProcesCommunication.Models;
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace UserInterface.ViewModels
+{
+ public class AddSensorViewModel : ViewModelBase
+ {
+ private AvailableSensors selectedType;
+ private string description;
+
+ public string Description { get => description; set => this.RaiseAndSetIfChanged(ref description, value); }
+
+ private string moreInfoLink;
+
+ public string MoreInfoLink
+ {
+ get { return moreInfoLink; }
+ set { this.RaiseAndSetIfChanged(ref moreInfoLink, value); }
+ }
+
+
+ public AvailableSensors SelectedType { get => selectedType; set => this.RaiseAndSetIfChanged(ref selectedType, value); }
+ public string Name { get; set; }
+ }
+}
diff --git a/UserInterface/ViewModels/SensorSettingsViewModel.cs b/UserInterface/ViewModels/SensorSettingsViewModel.cs
new file mode 100644
index 0000000..dc2eca7
--- /dev/null
+++ b/UserInterface/ViewModels/SensorSettingsViewModel.cs
@@ -0,0 +1,24 @@
+using ReactiveUI;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace UserInterface.ViewModels
+{
+ public class SensorSettingsViewModel : ViewModelBase
+ {
+ private ICollection configuredSensors;
+
+ public ICollection ConfiguredSensors { get => configuredSensors; set => this.RaiseAndSetIfChanged(ref configuredSensors, value); }
+ }
+
+ public class SensorViewModel : ViewModelBase
+ {
+ private string _value;
+
+ public Guid Id { get; set; }
+ public string Type { get; set; }
+ public string Name { get; set; }
+ public string Value { get => _value; set => this.RaiseAndSetIfChanged(ref _value, value); }
+ }
+}
diff --git a/UserInterface/Views/AddSensorDialog.axaml b/UserInterface/Views/AddSensorDialog.axaml
new file mode 100644
index 0000000..bb5fdd8
--- /dev/null
+++ b/UserInterface/Views/AddSensorDialog.axaml
@@ -0,0 +1,19 @@
+
+
+ Sensor type
+
+
+
+
+ Name
+
+
+
+
diff --git a/UserInterface/Views/AddSensorDialog.axaml.cs b/UserInterface/Views/AddSensorDialog.axaml.cs
new file mode 100644
index 0000000..52b567e
--- /dev/null
+++ b/UserInterface/Views/AddSensorDialog.axaml.cs
@@ -0,0 +1,86 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using hass_workstation_service.Communication.InterProcesCommunication.Models;
+using hass_workstation_service.Communication.NamedPipe;
+using JKang.IpcServiceFramework.Client;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Dynamic;
+using System.Linq;
+using System.Text.Json;
+using UserInterface.Util;
+using UserInterface.ViewModels;
+
+namespace UserInterface.Views
+{
+ public class AddSensorDialog : Window
+ {
+ private readonly IIpcClient client;
+ public ComboBox comboBox { get; set; }
+ public AddSensorDialog()
+ {
+ this.InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ this.comboBox = this.FindControl("ComboBox");
+ this.comboBox.Items = Enum.GetValues(typeof(AvailableSensors)).Cast();
+
+ // register IPC clients
+ ServiceProvider serviceProvider = new ServiceCollection()
+ .AddNamedPipeIpcClient("addsensor", pipeName: "pipeinternal")
+ .BuildServiceProvider();
+
+ // resolve IPC client factory
+ IIpcClientFactory clientFactory = serviceProvider
+ .GetRequiredService>();
+
+ // create client
+ this.client = clientFactory.CreateClient("addsensor");
+
+
+ DataContext = new AddSensorViewModel();
+ }
+
+ public async void Save(object sender, RoutedEventArgs args)
+ {
+ var item = ((AddSensorViewModel)this.DataContext);
+ dynamic model = new { Name = item.Name };
+ string json = JsonSerializer.Serialize(model);
+ await this.client.InvokeAsync(x => x.AddSensor(item.SelectedType, json));
+ Close();
+ }
+
+ public void ComboBoxClosed(object sender, SelectionChangedEventArgs args)
+ {
+ var item = ((AddSensorViewModel)this.DataContext);
+ switch (item.SelectedType)
+ {
+ case AvailableSensors.UserNotificationStateSensor:
+ item.Description = "This sensor watches the UserNotificationState. This is normally used in applications to determine if it is appropriate to send a notification but we can use it to expose this state. \n ";
+ item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#usernotificationstate";
+ break;
+ case AvailableSensors.DummySensor:
+ item.Description = "This sensor spits out a random number every second. Useful for testing, maybe you'll find some other use for it.";
+ item.MoreInfoLink = "https://github.com/sleevezipper/hass-workstation-service#dummy";
+ break;
+ default:
+ item.Description = null;
+ item.MoreInfoLink = null;
+ break;
+ }
+ }
+ public void OpenInfo(object sender, RoutedEventArgs args)
+ {
+ var item = ((AddSensorViewModel)this.DataContext);
+ BrowserUtil.OpenBrowser(item.MoreInfoLink);
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/UserInterface/Views/MainWindow.axaml b/UserInterface/Views/MainWindow.axaml
index c2123af..50d8e41 100644
--- a/UserInterface/Views/MainWindow.axaml
+++ b/UserInterface/Views/MainWindow.axaml
@@ -8,17 +8,15 @@
x:Class="UserInterface.Views.MainWindow"
Icon="/Assets/hass-workstation-logo.ico"
SizeToContent="WidthAndHeight"
- Title="Settings"
- Background="LightGray">
-
-
-
-
+ Title="Settings">
+
+
+
-
-
-
+
+
+
@@ -28,7 +26,7 @@
-->
-
-
+
+
diff --git a/UserInterface/Views/SensorSettings.axaml b/UserInterface/Views/SensorSettings.axaml
index 328e5f9..ba2c1f0 100644
--- a/UserInterface/Views/SensorSettings.axaml
+++ b/UserInterface/Views/SensorSettings.axaml
@@ -4,17 +4,25 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
MaxWidth="500"
- x:Class="UserInterface.Views.SensorSettings">
+ x:Class="UserInterface.Views.SensorSettings" >
- Mqtt broker
-
-
- IP or hostname
-
- Username
-
- Password
-
-
+
+
+
+
+
+
+
+
+ Add some sensors by clicking the "Add" button.
+
+
+
diff --git a/UserInterface/Views/SensorSettings.axaml.cs b/UserInterface/Views/SensorSettings.axaml.cs
index 2a2e62c..fa5041d 100644
--- a/UserInterface/Views/SensorSettings.axaml.cs
+++ b/UserInterface/Views/SensorSettings.axaml.cs
@@ -10,12 +10,17 @@ using System.Reactive.Linq;
using UserInterface.ViewModels;
using System.Security;
using hass_workstation_service.Communication.InterProcesCommunication.Models;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls.ApplicationLifetimes;
namespace UserInterface.Views
{
public class SensorSettings : UserControl
{
private readonly IIpcClient client;
+ private DataGrid _dataGrid { get; set; }
+ private bool sensorsNeedToRefresh { get; set; }
public SensorSettings()
{
@@ -33,23 +38,61 @@ namespace UserInterface.Views
this.client = clientFactory.CreateClient("sensors");
- DataContext = new BrokerSettingsViewModel();
+ DataContext = new SensorSettingsViewModel();
+ GetConfiguredSensors();
+ this._dataGrid = this.FindControl("Grid");
}
- public void Ping(object sender, RoutedEventArgs args) {
- var result = this.client.InvokeAsync(x => x.Ping("ping")).Result;
- }
- public void Configure(object sender, RoutedEventArgs args)
+
+ public async void GetConfiguredSensors()
{
- var model = (BrokerSettingsViewModel)this.DataContext;
- var result = this.client.InvokeAsync(x => x.WriteMqttBrokerSettingsAsync(new MqttSettings() { Host = model.Host, Username = model.Username, Password = model.Password }));
+ sensorsNeedToRefresh = false;
+ List status = await this.client.InvokeAsync(x => x.GetConfiguredSensors());
+
+ ((SensorSettingsViewModel)this.DataContext).ConfiguredSensors = status.Select(s => new SensorViewModel() { Name = s.Name, Type = s.Type, Value = s.Value, Id = s.Id }).ToList();
+ while (!sensorsNeedToRefresh)
+ {
+ await Task.Delay(2000);
+ List statusUpdated = await this.client.InvokeAsync(x => x.GetConfiguredSensors());
+ var configuredSensors = ((SensorSettingsViewModel)this.DataContext).ConfiguredSensors;
+ statusUpdated.ForEach(s =>
+ {
+ var configuredSensor = configuredSensors.FirstOrDefault(cs => cs.Id == s.Id);
+ if (configuredSensor != null)
+ {
+ configuredSensor.Value = s.Value;
+
+ configuredSensors.FirstOrDefault(cs => cs.Id == s.Id).Value = s.Value;
+ }
+ });
+ }
+
+ }
+ public void Delete(object sender, RoutedEventArgs args)
+ {
+ var item = ((SensorViewModel)this._dataGrid.SelectedItem);
+ this.client.InvokeAsync(x => x.RemoveSensorById(item.Id));
+ // TODO: improve this. it is not working well.
+ sensorsNeedToRefresh = true;
+ GetConfiguredSensors();
}
+ public async void AddSensor(object sender, RoutedEventArgs args)
+ {
+ AddSensorDialog dialog = new AddSensorDialog();
+ if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ await dialog.ShowDialog(desktop.MainWindow);
+ GetConfiguredSensors();
+ }
+ }
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
+
+
}
}
diff --git a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs
index 97fff4a..4a7ecbc 100644
--- a/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs
+++ b/hass-workstation-service/Communication/InterProcesCommunication/InterProcessApi.cs
@@ -1,9 +1,15 @@
using hass_workstation_service.Communication.InterProcesCommunication.Models;
using hass_workstation_service.Communication.NamedPipe;
+using hass_workstation_service.Communication.Util;
using hass_workstation_service.Data;
+using hass_workstation_service.Domain.Sensors;
+using Serilog;
using System;
using System.Collections.Generic;
+using System.Dynamic;
+using System.Linq;
using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
namespace hass_workstation_service.Communication.InterProcesCommunication
@@ -57,5 +63,42 @@ namespace hass_workstation_service.Communication.InterProcesCommunication
{
return this._configurationService.IsAutoStartEnabled();
}
+
+ public List GetConfiguredSensors()
+ {
+ return this._configurationService.ConfiguredSensors.Select(s => new ConfiguredSensorModel() { Name = s.Name, Type = s.GetType().Name, Value = s.PreviousPublishedState, Id = s.Id }).ToList();
+ }
+
+ public void RemoveSensorById(Guid id)
+ {
+ this._configurationService.DeleteConfiguredSensor(id);
+ }
+
+ public void AddSensor(AvailableSensors sensorType, string json)
+ {
+ var serializerOptions = new JsonSerializerOptions
+ {
+ Converters = { new DynamicJsonConverter() }
+ };
+ dynamic model = JsonSerializer.Deserialize(json, serializerOptions);
+
+ AbstractSensor sensorToCreate = null;
+ switch (sensorType)
+ {
+ case AvailableSensors.UserNotificationStateSensor:
+ sensorToCreate = new UserNotificationStateSensor(this._publisher, model.Name);
+ break;
+ case AvailableSensors.DummySensor:
+ sensorToCreate = new DummySensor(this._publisher, model.Name);
+ break;
+ default:
+ Log.Logger.Error("Unknown sensortype");
+ break;
+ }
+ if (sensorToCreate != null)
+ {
+ this._configurationService.AddConfiguredSensor(sensorToCreate);
+ }
+ }
}
}
diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs
index b686baa..a85f5fe 100644
--- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs
+++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractInterfaces.cs
@@ -14,5 +14,8 @@ namespace hass_workstation_service.Communication.NamedPipe
MqqtClientStatus GetMqqtClientStatus();
void EnableAutostart(bool enable);
bool IsAutoStartEnabled();
+ List GetConfiguredSensors();
+ void RemoveSensorById(Guid id);
+ void AddSensor(AvailableSensors sensorType, string json);
}
}
diff --git a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs
index a39663d..ff95e36 100644
--- a/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs
+++ b/hass-workstation-service/Communication/InterProcesCommunication/ServiceContractModels.cs
@@ -1,4 +1,5 @@
-using System;
+using hass_workstation_service.Domain.Sensors;
+using System;
using System.Collections.Generic;
using System.Text;
@@ -16,4 +17,18 @@ namespace hass_workstation_service.Communication.InterProcesCommunication.Models
public bool IsConnected { get; set; }
public string Message { get; set; }
}
+
+ public class ConfiguredSensorModel
+ {
+ public Guid Id { get; set; }
+ public string Type { get; set; }
+ public string Name { get; set; }
+ public string Value { get; set; }
+ }
+
+ public enum AvailableSensors
+ {
+ UserNotificationStateSensor,
+ DummySensor
+ }
}
diff --git a/hass-workstation-service/Communication/Util/DynamicJsonConverter.cs b/hass-workstation-service/Communication/Util/DynamicJsonConverter.cs
new file mode 100644
index 0000000..6856b40
--- /dev/null
+++ b/hass-workstation-service/Communication/Util/DynamicJsonConverter.cs
@@ -0,0 +1,127 @@
+
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+namespace hass_workstation_service.Communication.Util
+{
+ ///
+ /// Temp Dynamic Converter
+ /// by:tchivs@live.cn
+ ///
+ public class DynamicJsonConverter : JsonConverter
+ {
+ public override dynamic Read(ref Utf8JsonReader reader,
+ Type typeToConvert,
+ JsonSerializerOptions options)
+ {
+
+ if (reader.TokenType == JsonTokenType.True)
+ {
+ return true;
+ }
+
+ if (reader.TokenType == JsonTokenType.False)
+ {
+ return false;
+ }
+
+ if (reader.TokenType == JsonTokenType.Number)
+ {
+ if (reader.TryGetInt64(out long l))
+ {
+ return l;
+ }
+
+ return reader.GetDouble();
+ }
+
+ if (reader.TokenType == JsonTokenType.String)
+ {
+ if (reader.TryGetDateTime(out DateTime datetime))
+ {
+ return datetime;
+ }
+
+ return reader.GetString();
+ }
+
+ if (reader.TokenType == JsonTokenType.StartObject)
+ {
+ using JsonDocument documentV = JsonDocument.ParseValue(ref reader);
+ return ReadObject(documentV.RootElement);
+ }
+ // Use JsonElement as fallback.
+ // Newtonsoft uses JArray or JObject.
+ JsonDocument document = JsonDocument.ParseValue(ref reader);
+ return document.RootElement.Clone();
+ }
+
+ private object ReadObject(JsonElement jsonElement)
+ {
+ IDictionary expandoObject = new ExpandoObject();
+ foreach (var obj in jsonElement.EnumerateObject())
+ {
+ var k = obj.Name;
+ var value = ReadValue(obj.Value);
+ expandoObject[k] = value;
+ }
+ return expandoObject;
+ }
+ private object? ReadValue(JsonElement jsonElement)
+ {
+ object? result = null;
+ switch (jsonElement.ValueKind)
+ {
+ case JsonValueKind.Object:
+ result = ReadObject(jsonElement);
+ break;
+ case JsonValueKind.Array:
+ result = ReadList(jsonElement);
+ break;
+ case JsonValueKind.String:
+ //TODO: Missing Datetime&Bytes Convert
+ result = jsonElement.GetString();
+ break;
+ case JsonValueKind.Number:
+ //TODO: more num type
+ result = 0;
+ if (jsonElement.TryGetInt64(out long l))
+ {
+ result = l;
+ }
+ break;
+ case JsonValueKind.True:
+ result = true;
+ break;
+ case JsonValueKind.False:
+ result = false;
+ break;
+ case JsonValueKind.Undefined:
+ case JsonValueKind.Null:
+ result = null;
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+ return result;
+ }
+
+ private object? ReadList(JsonElement jsonElement)
+ {
+ IList