From ee462cd9dd132b2d85cbd8bc6f0a5d1ee214e678 Mon Sep 17 00:00:00 2001 From: amadeo-alex <68441479+amadeo-alex@users.noreply.github.com> Date: Sun, 3 Mar 2024 18:49:56 +0100 Subject: [PATCH] Release: 2.1.0-beta1 (#60) ### Changes - Added "SetAudioInputCommand" (thanks to Chrisdegod for the suggestion) - https://github.com/hass-agent/HASS.Agent/pull/41 - Added "ScreenshotSensor", functioning as camera entity (huge thanks for @denisabt for the inital PR with implementation to the original repo)- https://github.com/hass-agent/HASS.Agent/pull/42 - Added option to ignore the MQTT/connection grace period when system wakes from hibernation (thanks to @larena1 for the suggestion)- https://github.com/hass-agent/HASS.Agent/pull/51 - Added option to use modern (white on transparent) icon for the HASS.Agent's trayicon (big thanks to @MBaliver for the suggestion and the icon) - https://github.com/hass-agent/HASS.Agent/pull/55 - Added note to "PowershellSensor" description as a reminder to Home Assistant's 255 character limit for the payload/state (thanks to @EpicLPer for suggestion) - https://github.com/hass-agent/HASS.Agent/pull/58 - Last but not least, all dependencies have been bumped to the newest possbile version - https://github.com/hass-agent/HASS.Agent/pull/56 ### Fixes - "SetAudioOutputCommand" and "SetApplicationVolume" commands are now properly configurable from the UI (thanks to Chrisdegod for reporting) - https://github.com/hass-agent/HASS.Agent/pull/39 - Updated VirtualDesktop management library to stop it crashing on some Windows 11 systems - https://github.com/hass-agent/HASS.Agent/pull/40 - Sensor state being evaluated constantly, ignoring the configured update interval (thanks to @shupershuff for reporting) - https://github.com/hass-agent/HASS.Agent/pull/45 - "PowershellCommand" arguments not being bound/parsed properly when provided with payload/action (thanks to @shupershuff for reporting) - https://github.com/hass-agent/HASS.Agent/pull/47 - "ActiveWindow" sensor not using proper encoding, resulting in some characters being replaced with "?" (thanks to @greghesp for reporting) - https://github.com/hass-agent/HASS.Agent/pull/50 - Sensor/command discovery payload messages are now sent on HASS.Agent start and configuration change instead of constantly - should reduce the load on Home Assistant (thanks to @Anto79-ops for reporting) - https://github.com/hass-agent/HASS.Agent/pull/5 - "MicrophoneProcessSensor" description has been changed to more accurately describe its function (thanks to @Gvolten for reporting) - https://github.com/hass-agent/HASS.Agent/pull/57 #### Note This beta release includes changes from [2.0.2-beta1](https://github.com/hass-agent/HASS.Agent/releases/tag/2.0.2-beta1) and [2.0.2-beta2](https://github.com/hass-agent/HASS.Agent/releases/tag/2.0.2-beta2) --- src/HASS.Agent.Installer/InstallerScript.iss | 2 +- .../Commands/CommandsManager.cs | 19 +- .../HASS.Agent.Satellite.Service.csproj | 22 +- .../Sensors/SensorsManager.cs | 32 +- .../HASS.Agent.Shared/Enums/CommandType.cs | 4 + .../HASS.Agent.Shared/Enums/SensorType.cs | 6 +- .../HASS.Agent.Shared.csproj | 15 +- .../InternalCommands/SetAudioInputCommand.cs | 67 ++ .../SingleValue/ActiveWindowSensor.cs | 22 +- .../SingleValue/ScreenshotSensor.cs | 93 +++ .../HomeAssistant/Sensors/PowershellSensor.cs | 92 +-- .../Managers/PowershellManager.cs | 625 +++++++++--------- .../AbstractSingleValueSensor.cs | 210 +++--- .../HomeAssistant/DiscoveryConfigModel.cs | 14 + .../Localization/Languages.Designer.cs | 18 + .../Resources/Localization/Languages.de.resx | 6 + .../Resources/Localization/Languages.en.resx | 9 +- .../Resources/Localization/Languages.es.resx | 6 + .../Resources/Localization/Languages.fr.resx | 6 + .../Resources/Localization/Languages.nl.resx | 6 + .../Resources/Localization/Languages.pl.resx | 6 + .../Localization/Languages.pt-br.resx | 6 + .../Resources/Localization/Languages.resx | 6 + .../Resources/Localization/Languages.ru.resx | 6 + .../Resources/Localization/Languages.sl.resx | 6 + .../Resources/Localization/Languages.tr.resx | 6 + .../HASS.Agent/Commands/CommandsManager.cs | 30 +- .../Configuration/ConfigMqtt.Designer.cs | 99 +-- .../Controls/Configuration/ConfigMqtt.cs | 5 + .../Controls/Configuration/ConfigMqtt.resx | 59 +- .../Configuration/ConfigTrayIcon.Designer.cs | 528 ++++++++------- .../Configuration/ConfigTrayIcon.resx | 62 +- .../HASS.Agent/Forms/Commands/CommandsMod.cs | 3 + .../HASS.Agent/Forms/Configuration.cs | 4 + src/HASS.Agent/HASS.Agent/Forms/Main.cs | 17 +- src/HASS.Agent/HASS.Agent/Forms/Main.resx | 156 +++-- .../HASS.Agent/Forms/Sensors/SensorsMod.cs | 43 +- src/HASS.Agent/HASS.Agent/HASS.Agent.csproj | 24 +- src/HASS.Agent/HASS.Agent/MQTT/MqttManager.cs | 37 +- .../HASS.Agent/Managers/SystemStateManager.cs | 21 +- .../HASS.Agent/Models/Config/AppSettings.cs | 3 + .../Localization/Languages.Designer.cs | 50 +- .../Resources/Localization/Languages.de.resx | 28 +- .../Resources/Localization/Languages.en.resx | 28 +- .../Resources/Localization/Languages.es.resx | 28 +- .../Resources/Localization/Languages.fr.resx | 32 +- .../Resources/Localization/Languages.nl.resx | 32 +- .../Resources/Localization/Languages.pl.resx | 32 +- .../Localization/Languages.pt-br.resx | 28 +- .../Resources/Localization/Languages.resx | 20 +- .../Resources/Localization/Languages.ru.resx | 30 +- .../Resources/Localization/Languages.sl.resx | 32 +- .../Resources/Localization/Languages.tr.resx | 30 +- .../HASS.Agent/Resources/modern-tray-icon.ico | Bin 0 -> 52111 bytes .../HASS.Agent/Sensors/SensorsManager.cs | 40 +- .../HASS.Agent/Settings/StoredCommands.cs | 3 + .../HASS.Agent/Settings/StoredSensors.cs | 23 +- 57 files changed, 1837 insertions(+), 1000 deletions(-) create mode 100644 src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Commands/InternalCommands/SetAudioInputCommand.cs create mode 100644 src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ScreenshotSensor.cs create mode 100644 src/HASS.Agent/HASS.Agent/Resources/modern-tray-icon.ico diff --git a/src/HASS.Agent.Installer/InstallerScript.iss b/src/HASS.Agent.Installer/InstallerScript.iss index 6b2aa517..8e7190f5 100644 --- a/src/HASS.Agent.Installer/InstallerScript.iss +++ b/src/HASS.Agent.Installer/InstallerScript.iss @@ -9,7 +9,7 @@ ; Standard installation constants #define MyAppName "HASS.Agent" -#define MyAppVersion "2.0.1" +#define MyAppVersion "2.1.0-beta1" #define MyAppPublisher "HASS.Agent Team" #define MyAppURL "https://hass-agent.io" #define MyAppExeName "HASS.Agent.exe" diff --git a/src/HASS.Agent/HASS.Agent.Satellite.Service/Commands/CommandsManager.cs b/src/HASS.Agent/HASS.Agent.Satellite.Service/Commands/CommandsManager.cs index d65f8dba..872371fd 100644 --- a/src/HASS.Agent/HASS.Agent.Satellite.Service/Commands/CommandsManager.cs +++ b/src/HASS.Agent/HASS.Agent.Satellite.Service/Commands/CommandsManager.cs @@ -15,6 +15,8 @@ internal static class CommandsManager private static bool _active = true; private static bool _pause; + private static bool _discoveryPublished = false; + private static DateTime _lastAutoDiscoPublish = DateTime.MinValue; /// @@ -122,20 +124,22 @@ private static async void Process() // do we have commands? if (!CommandsPresent()) continue; - // publish availability & sensor autodisco's every 30 sec + // publish availability & autodiscovery every 30 sec if ((DateTime.Now - _lastAutoDiscoPublish).TotalSeconds > 30) { - // let hass know we're still here await Variables.MqttManager.AnnounceAvailabilityAsync(); - // publish command autodisco's - foreach (var command in Variables.Commands.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + if (!_discoveryPublished) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; - await command.PublishAutoDiscoveryConfigAsync(); + foreach (var command in Variables.Commands.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; + await command.PublishAutoDiscoveryConfigAsync(); + } + + _discoveryPublished = true; } - // are we subscribed? if (!_subscribed) { foreach (var command in Variables.Commands.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) @@ -146,7 +150,6 @@ private static async void Process() _subscribed = true; } - // log moment _lastAutoDiscoPublish = DateTime.Now; } diff --git a/src/HASS.Agent/HASS.Agent.Satellite.Service/HASS.Agent.Satellite.Service.csproj b/src/HASS.Agent/HASS.Agent.Satellite.Service/HASS.Agent.Satellite.Service.csproj index f8cf3dd8..e9e2d70f 100644 --- a/src/HASS.Agent/HASS.Agent.Satellite.Service/HASS.Agent.Satellite.Service.csproj +++ b/src/HASS.Agent/HASS.Agent.Satellite.Service/HASS.Agent.Satellite.Service.csproj @@ -8,7 +8,7 @@ dotnet-HASSAgentSatelliteService-6E4FA50A-3AC9-4E66-8671-9FAB92372154 x64 x64;x86 - 2.0.2-beta2 + 2.1.0-beta1 HASS.Agent Team HASS.Agent Satellite Service HASS.Agent.Satellite.Service @@ -17,9 +17,9 @@ https://github.com/hass-agent/HASS.Agent https://github.com/hass-agent/HASS.Agent hass.png - 2.0.2 + 2.1.0 hass.ico - 2.0.2 + 2.1.0 10.0.17763.0 false @@ -29,22 +29,22 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + - + diff --git a/src/HASS.Agent/HASS.Agent.Satellite.Service/Sensors/SensorsManager.cs b/src/HASS.Agent/HASS.Agent.Satellite.Service/Sensors/SensorsManager.cs index 8a7e3623..10122601 100644 --- a/src/HASS.Agent/HASS.Agent.Satellite.Service/Sensors/SensorsManager.cs +++ b/src/HASS.Agent/HASS.Agent.Satellite.Service/Sensors/SensorsManager.cs @@ -14,6 +14,8 @@ internal static class SensorsManager private static bool _active = true; private static bool _pause; + private static bool _discoveryPublished = false; + private static DateTime _lastAutoDiscoPublish = DateTime.MinValue; /// @@ -116,32 +118,34 @@ private static async void Process() // optionally flag as the first real run if (!firstRunDone) firstRunDone = true; - // publish availability & sensor autodisco's every 30 sec + // publish availability & autodiscovery every 30 sec if ((DateTime.Now - _lastAutoDiscoPublish).TotalSeconds > 30) { - // let hass know we're still here await Variables.MqttManager.AnnounceAvailabilityAsync(); - // publish the autodisco's - if (SingleValueSensorsPresent()) + if (!_discoveryPublished) { - foreach (var sensor in Variables.SingleValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + if (SingleValueSensorsPresent()) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; - await sensor.PublishAutoDiscoveryConfigAsync(); + foreach (var sensor in Variables.SingleValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; + await sensor.PublishAutoDiscoveryConfigAsync(); + } } - } - if (MultiValueSensorsPresent()) - { - foreach (var sensor in Variables.MultiValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + if (MultiValueSensorsPresent()) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; - await sensor.PublishAutoDiscoveryConfigAsync(); + foreach (var sensor in Variables.MultiValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; + await sensor.PublishAutoDiscoveryConfigAsync(); + } } + + _discoveryPublished = true; } - // log moment _lastAutoDiscoPublish = DateTime.Now; } diff --git a/src/HASS.Agent/HASS.Agent.Shared/Enums/CommandType.cs b/src/HASS.Agent/HASS.Agent.Shared/Enums/CommandType.cs index ee0fa0a0..e92db473 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Enums/CommandType.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Enums/CommandType.cs @@ -105,6 +105,10 @@ public enum CommandType [EnumMember(Value = "SetAudioOutputCommand")] SetAudioOutputCommand, + [LocalizedDescription("CommandType_SetAudioInputCommand", typeof(Languages))] + [EnumMember(Value = "SetAudioInputCommand")] + SetAudioInputCommand, + [LocalizedDescription("CommandType_ShutdownCommand", typeof(Languages))] [EnumMember(Value = "ShutdownCommand")] ShutdownCommand, diff --git a/src/HASS.Agent/HASS.Agent.Shared/Enums/SensorType.cs b/src/HASS.Agent/HASS.Agent.Shared/Enums/SensorType.cs index bf3b31eb..65348afa 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Enums/SensorType.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Enums/SensorType.cs @@ -165,6 +165,10 @@ public enum SensorType [LocalizedDescription("SensorType_InternalDeviceSensor", typeof(Languages))] [EnumMember(Value = "InternalDeviceSensor")] - InternalDeviceSensor + InternalDeviceSensor, + + [LocalizedDescription("SensorType_ScreenshotSensor", typeof(Languages))] + [EnumMember(Value = "ScreenshotSensor")] + ScreenshotSensor } } \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/HASS.Agent.Shared.csproj b/src/HASS.Agent/HASS.Agent.Shared/HASS.Agent.Shared.csproj index ab356af1..aa05de02 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/HASS.Agent.Shared.csproj +++ b/src/HASS.Agent/HASS.Agent.Shared/HASS.Agent.Shared.csproj @@ -10,9 +10,9 @@ Shared functions and models for the HASS.Agent platform. https://github.com/hass-agent/HASS.Agent https://github.com/hass-agent/HASS.Agent - 2.0.2 - 2.0.2 - 2.0.2-beta2 + 2.1.0 + 2.1.0 + 2.1.0-beta1 logo_128.png True hassagent.ico @@ -30,18 +30,19 @@ - + - + - + - + + diff --git a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Commands/InternalCommands/SetAudioInputCommand.cs b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Commands/InternalCommands/SetAudioInputCommand.cs new file mode 100644 index 00000000..292b09ba --- /dev/null +++ b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Commands/InternalCommands/SetAudioInputCommand.cs @@ -0,0 +1,67 @@ +using CoreAudio; +using HASS.Agent.Shared.Enums; +using Newtonsoft.Json; +using Serilog; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; + +namespace HASS.Agent.Shared.HomeAssistant.Commands.InternalCommands; + +public class SetAudioInputCommand : InternalCommand +{ + private const string DefaultName = "setaudioinput"; + + private string InputDevice { get => CommandConfig; } + + public SetAudioInputCommand(string entityName = DefaultName, string name = DefaultName, string audioDevice = "", CommandEntityType entityType = CommandEntityType.Button, string id = default) : base(entityName ?? DefaultName, name ?? null, audioDevice, entityType, id) + { + State = "OFF"; + } + + public override void TurnOn() + { + if (string.IsNullOrWhiteSpace(InputDevice)) + { + Log.Error("[SETAUDIOIN] Error, input device name cannot be null/blank"); + + return; + } + + TurnOnWithAction(InputDevice); + } + + private MMDevice GetAudioDeviceOrDefault(string playbackDeviceName) + { + var devices = Variables.AudioDeviceEnumerator.EnumerateAudioEndPoints(DataFlow.Capture, DeviceState.Active); + var playbackDevice = devices.Where(d => d.DeviceFriendlyName == playbackDeviceName).FirstOrDefault(); + + return playbackDevice ?? Variables.AudioDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Communications); + } + + public override void TurnOnWithAction(string action) + { + State = "ON"; + + try + { + var outputDevice = GetAudioDeviceOrDefault(action); + if (outputDevice == Variables.AudioDeviceEnumerator.GetDefaultAudioEndpoint(DataFlow.Capture, Role.Communications)) + return; + + outputDevice.Selected = true; + } + catch (Exception ex) + { + Log.Error("[SETAUDIOIN] Error while processing action '{action}': {err}", action, ex.Message); + } + finally + { + State = "OFF"; + } + } +} diff --git a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ActiveWindowSensor.cs b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ActiveWindowSensor.cs index c6711c3e..713a50e7 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ActiveWindowSensor.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ActiveWindowSensor.cs @@ -16,10 +16,12 @@ public ActiveWindowSensor(int? updateInterval = null, string entityName = Defaul public override DiscoveryConfigModel GetAutoDiscoveryConfig() { - if (Variables.MqttManager == null) return null; + if (Variables.MqttManager == null) + return null; var deviceConfig = Variables.MqttManager.GetDeviceConfigModel(); - if (deviceConfig == null) return null; + if (deviceConfig == null) + return null; return AutoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { @@ -43,16 +45,20 @@ public override string GetState() [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); - [DllImport("user32.dll")] - private static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int GetWindowTextLength(IntPtr hWnd); + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + private static extern int GetWindowText(IntPtr hWnd, StringBuilder builder, int count); private static string GetActiveWindowTitle() { - const int nChars = 256; - var buff = new StringBuilder(nChars); - var handle = GetForegroundWindow(); + var windowHandle = GetForegroundWindow(); + var titleLength = GetWindowTextLength(windowHandle) + 1; + var builder = new StringBuilder(titleLength); + var windowTitle = GetWindowText(windowHandle, builder, titleLength) > 0 ? builder.ToString() : string.Empty; - return GetWindowText(handle, buff, nChars) > 0 ? buff.ToString() : string.Empty; + return windowTitle.Length > 255 ? windowTitle[..255]: windowTitle; //Note(Amadeo): to make sure we don't exceed HA limitation of 255 payload length } } } diff --git a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ScreenshotSensor.cs b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ScreenshotSensor.cs new file mode 100644 index 00000000..0fc98195 --- /dev/null +++ b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/GeneralSensors/SingleValue/ScreenshotSensor.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.DirectoryServices.ActiveDirectory; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using HASS.Agent.Shared.Models.HomeAssistant; +using Serilog; +using System.Windows.Forms; +using System.Xml.Linq; +using System.Runtime.InteropServices; +using System.Drawing.Imaging; +using System.Text.RegularExpressions; + +namespace HASS.Agent.Shared.HomeAssistant.Sensors.GeneralSensors.SingleValue; +public class ScreenshotSensor : AbstractSingleValueSensor +{ + private const string DefaultName = "screenshot"; + + public int ScreenIndex; + + public ScreenshotSensor(string screenIndex = "0", int? updateInterval = null, string entityName = DefaultName, string name = DefaultName, string id = default) : base(entityName ?? DefaultName, name ?? null, updateInterval ?? 15, id) + { + ScreenIndex = int.TryParse(screenIndex, out var parsedScreenIndex) ? parsedScreenIndex : 0; + Domain = "camera"; + } + + public override DiscoveryConfigModel GetAutoDiscoveryConfig() + { + if (Variables.MqttManager == null) + return null; + + var deviceConfig = Variables.MqttManager.GetDeviceConfigModel(); + if (deviceConfig == null) + return null; + + return AutoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new CameraSensorDiscoveryConfigModel() + { + EntityName = EntityName, + Name = Name, + Unique_id = Id, + Device = deviceConfig, + Image_encoding = "b64", + Icon = "mdi:camera", + State_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{ObjectId}/state", + Topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{ObjectId}/state", + Availability_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/sensor/{deviceConfig.Name}/availability" + }); + } + + public override string GetState() + { + var screenCount = Screen.AllScreens.Length; + if (ScreenIndex >= screenCount || ScreenIndex < 0) + { + Log.Warning("[SCREENSHOT] Wrong index '{index}' - returning image for screen 0", ScreenIndex); + ScreenIndex = 0; + } + + var screenImage = CaptureScreen(ScreenIndex); + return Convert.ToBase64String(screenImage); + } + + private byte[] CaptureScreen(int screenIndex) + { + try + { + return CapturePngFile(Screen.AllScreens[screenIndex]); + } + catch (Exception ex) + { + Log.Error(ex, "[SCREENSHOT] Internal Error capturing screen {index}, {ex}", ex.Message); + return Array.Empty(); + } + } + + private byte[] CapturePngFile(Screen screen) + { + var captureRectangle = screen.Bounds; + var captureBitmap = new Bitmap(captureRectangle.Width, captureRectangle.Height, PixelFormat.Format32bppArgb); + var captureGraphics = Graphics.FromImage(captureBitmap); + captureGraphics.CopyFromScreen(captureRectangle.Left, captureRectangle.Top, 0, 0, captureRectangle.Size); + + using var ms = new MemoryStream(); + captureBitmap.Save(ms, ImageFormat.Png); + + return ms.ToArray(); + } + + public override string GetAttributes() => string.Empty; +} diff --git a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/PowershellSensor.cs b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/PowershellSensor.cs index a84e57ea..ccbd2859 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/PowershellSensor.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/HomeAssistant/Sensors/PowershellSensor.cs @@ -5,59 +5,63 @@ using HASS.Agent.Shared.Models.HomeAssistant; using Serilog; -namespace HASS.Agent.Shared.HomeAssistant.Sensors +namespace HASS.Agent.Shared.HomeAssistant.Sensors; + +/// +/// Sensor containing the result of the provided Powershell command or script +/// +public class PowershellSensor : AbstractSingleValueSensor { - /// - /// Sensor containing the result of the provided Powershell command or script - /// - public class PowershellSensor : AbstractSingleValueSensor + private const string DefaultName = "powershellsensor"; + + public string Command { get; private set; } + public bool ApplyRounding { get; private set; } + public int? Round { get; private set; } + + public PowershellSensor(string command, bool applyRounding = false, int? round = null, int? updateInterval = null, string entityName = DefaultName, string name = DefaultName, string id = default) : base(entityName ?? DefaultName, name ?? null, updateInterval ?? 10, id) { - private const string DefaultName = "powershellsensor"; + Command = command; + ApplyRounding = applyRounding; + Round = round; + } - public string Command { get; private set; } - public bool ApplyRounding { get; private set; } - public int? Round { get; private set; } + public override DiscoveryConfigModel GetAutoDiscoveryConfig() + { + if (Variables.MqttManager == null) + return null; - public PowershellSensor(string command, bool applyRounding = false, int? round = null, int? updateInterval = null, string entityName = DefaultName, string name = DefaultName, string id = default) : base(entityName ?? DefaultName, name ?? null, updateInterval ?? 10, id) - { - Command = command; - ApplyRounding = applyRounding; - Round = round; - } + var deviceConfig = Variables.MqttManager.GetDeviceConfigModel(); + if (deviceConfig == null) + return null; - public override DiscoveryConfigModel GetAutoDiscoveryConfig() - { - if (Variables.MqttManager == null) return null; - - var deviceConfig = Variables.MqttManager.GetDeviceConfigModel(); - if (deviceConfig == null) return null; - - return AutoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() - { - EntityName = EntityName, - Name = Name, - Unique_id = Id, - Device = deviceConfig, - State_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{EntityName}/state", - Availability_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/availability" - }); - } - - public override string GetState() + return AutoDiscoveryConfigModel ?? SetAutoDiscoveryConfigModel(new SensorDiscoveryConfigModel() { - var executed = PowershellManager.ExecuteWithOutput(Command, TimeSpan.FromMinutes(5), out var output, out var errors); - if (!executed) return "error_during_execution"; + EntityName = EntityName, + Name = Name, + Unique_id = Id, + Device = deviceConfig, + State_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/{EntityName}/state", + Availability_topic = $"{Variables.MqttManager.MqttDiscoveryPrefix()}/{Domain}/{deviceConfig.Name}/availability" + }); + } - if (string.IsNullOrWhiteSpace(output) && string.IsNullOrWhiteSpace(errors)) return string.Empty; - if (string.IsNullOrWhiteSpace(output)) return errors.Trim(); + public override string GetState() + { + var executed = PowershellManager.ExecuteWithOutput(Command, TimeSpan.FromMinutes(5), out var output, out var errors); + if (!executed) + return "error_during_execution"; - // optionally apply rounding - if (ApplyRounding && Round != null && double.TryParse(output, out var dblValue)) { output = Math.Round(dblValue, (int)Round).ToString(CultureInfo.CurrentCulture); } + if (string.IsNullOrWhiteSpace(output) && string.IsNullOrWhiteSpace(errors)) + return string.Empty; + if (string.IsNullOrWhiteSpace(output)) + return errors.Trim(); - // done - return string.IsNullOrWhiteSpace(errors) ? output : $"{output} | {errors}"; - } + // optionally apply rounding + if (ApplyRounding && Round != null && double.TryParse(output, out var dblValue)) { output = Math.Round(dblValue, (int)Round).ToString(CultureInfo.CurrentCulture); } - public override string GetAttributes() => string.Empty; + // done + return string.IsNullOrWhiteSpace(errors) ? output : $"{output} | {errors}"; } + + public override string GetAttributes() => string.Empty; } \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Managers/PowershellManager.cs b/src/HASS.Agent/HASS.Agent.Shared/Managers/PowershellManager.cs index 576823dc..f28df5d7 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Managers/PowershellManager.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Managers/PowershellManager.cs @@ -2,322 +2,323 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; using CliWrap; using Newtonsoft.Json; using Serilog; -namespace HASS.Agent.Shared.Managers +namespace HASS.Agent.Shared.Managers; + +/// +/// Performs Powershell-related actions +/// +public static class PowershellManager { - /// - /// Performs Powershell-related actions - /// - public static class PowershellManager - { - /// - /// Execute a Powershell command without waiting for or checking results - /// - /// - /// - public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, string.Empty, false); - - /// - /// Executes a Powershell script without waiting for or checking results - /// - /// - /// - /// - public static bool ExecuteScriptHeadless(string script, string parameters) => ExecuteHeadless(script, parameters, true); - - private static string GetProcessArguments(string command, string parameters, bool isScript) - { - if (isScript) - { - return string.IsNullOrWhiteSpace(parameters) - ? $"-File \"{command}\"" - : $"-File \"{command}\" \"{parameters}\""; - } - else - { - return $@"& {{{command}}}"; //NOTE: place to fix any potential future issues with "command part of the command" - } - } - - private static bool ExecuteHeadless(string command, string parameters, bool isScript) - { - var descriptor = isScript ? "script" : "command"; - - try - { - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) - return false; - - var processInfo = new ProcessStartInfo - { - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = psExec, - WorkingDirectory = workingDir, - Arguments = GetProcessArguments(command, parameters, isScript) - }; - - using var process = new Process(); - process.StartInfo = processInfo; - var start = process.Start(); - - if (!start) - { - Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {command}", descriptor, command); - - return false; - } - - return true; - } - catch (Exception ex) - { - Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); - - return false; - } - } - - /// - /// Execute a Powershell command, logs the output if it fails - /// - /// - /// - /// - public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, string.Empty, false, timeout); - - /// - /// Executes a Powershell script, logs the output if it fails - /// - /// - /// - /// - public static bool ExecuteScript(string script, string parameters, TimeSpan timeout) => Execute(script, parameters, true, timeout); - - private static bool Execute(string command, string parameters, bool isScript, TimeSpan timeout) - { - var descriptor = isScript ? "script" : "command"; - - try - { - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) return false; - - var processInfo = new ProcessStartInfo - { - FileName = psExec, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - WorkingDirectory = workingDir, - Arguments = GetProcessArguments(command, parameters, isScript) - }; - - using var process = new Process(); - process.StartInfo = processInfo; - var start = process.Start(); - - if (!start) - { - Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {script}", descriptor, command); - - return false; - } - - process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); - - if (process.ExitCode == 0) - return true; - - // non-zero exitcode, process as failed - Log.Error("[POWERSHELL] The {descriptor} returned non-zero exitcode: {code}", descriptor, process.ExitCode); - - var errors = process.StandardError.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(errors)) - { - Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); - } - else - { - var console = process.StandardOutput.ReadToEnd().Trim(); - if (!string.IsNullOrEmpty(console)) - Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); - else - Log.Error("[POWERSHELL] No error and no console output"); - } - - return false; - } - catch (Exception ex) - { - Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); - - return false; - } - } - - private static Encoding TryParseCodePage(int codePage) - { - Encoding encoding = null; - try - { - encoding = Encoding.GetEncoding(codePage); - } - catch - { - // best effort - } - - return encoding; - } - - private static Encoding GetEncoding() - { - var encoding = TryParseCodePage(CultureInfo.InstalledUICulture.TextInfo.OEMCodePage); - if (encoding != null) - return encoding; - - encoding = TryParseCodePage(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); - if (encoding != null) - return encoding; - - encoding = TryParseCodePage(CultureInfo.CurrentUICulture.TextInfo.OEMCodePage); - if (encoding != null) - return encoding; - - encoding = TryParseCodePage(CultureInfo.InvariantCulture.TextInfo.OEMCodePage); - if (encoding != null) - return encoding; - - Log.Warning("[POWERSHELL] Cannot parse system text culture to encoding, returning UTF-8 as a fallback, please report this as a GitHub issue"); - - Log.Debug("[POWERSHELL] currentInstalledUICulture {c}", JsonConvert.SerializeObject(CultureInfo.InstalledUICulture.TextInfo)); - Log.Debug("[POWERSHELL] currentCulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentCulture.TextInfo)); - Log.Debug("[POWERSHELL] currentUICulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentUICulture.TextInfo)); - Log.Debug("[POWERSHELL] invariantCulture {c}", JsonConvert.SerializeObject(CultureInfo.InvariantCulture.TextInfo)); - - return Encoding.UTF8; - } - - /// - /// Executes the command or script, and returns the standard and error output - /// - /// - /// - /// - /// - /// - internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out string output, out string errors) - { - output = string.Empty; - errors = string.Empty; - - try - { - var isScript = command.ToLower().EndsWith(".ps1"); - - var workingDir = string.Empty; - if (isScript) - { - // try to get the script's startup path - var scriptDir = Path.GetDirectoryName(command); - workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; - } - - var psExec = GetPsExecutable(); - if (string.IsNullOrEmpty(psExec)) - return false; - - var encoding = GetEncoding(); - - var processInfo = new ProcessStartInfo - { - FileName = psExec, - RedirectStandardError = true, - RedirectStandardOutput = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = workingDir, - StandardOutputEncoding = encoding, - StandardErrorEncoding = encoding, - // set the right type of arguments - Arguments = isScript - ? $@"& '{command}'" - : $@"& {{{command}}}" - }; - - using var process = new Process(); - process.StartInfo = processInfo; - - var start = process.Start(); - if (!start) - { - Log.Error("[POWERSHELL] Unable to begin executing the {type}: {cmd}", isScript ? "script" : "command", command); - - return false; - } - - var completed = process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); - if (!completed) - Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); - - output = process.StandardOutput.ReadToEnd().Trim(); - errors = process.StandardError.ReadToEnd().Trim(); - - process.StandardOutput.Dispose(); - process.StandardError.Dispose(); - - process.Kill(); - - return completed; - } - catch (Exception ex) - { - Log.Fatal(ex, ex.Message); - - return false; - } - } - - /// - /// Attempt to locate powershell.exe - /// - /// - public static string GetPsExecutable() - { - // try regular location - var psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) - return psExec; - - // try specific - psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "WindowsPowerShell\\v1.0\\powershell.exe"); - if (File.Exists(psExec)) - return psExec; - - Log.Error("[POWERSHELL] PS executable not found, make sure you have powershell installed on your system"); - return string.Empty; - } - } + /// + /// Execute a Powershell command without waiting for or checking results + /// + /// + /// + public static bool ExecuteCommandHeadless(string command) => ExecuteHeadless(command, string.Empty, false); + + /// + /// Executes a Powershell script without waiting for or checking results + /// + /// + /// + /// + public static bool ExecuteScriptHeadless(string script, string parameters) => ExecuteHeadless(script, parameters, true); + + private static string GetProcessArguments(string command, string parameters, bool isScript) + { + if (isScript) + { + return string.IsNullOrWhiteSpace(parameters) + ? $"-File \"{command}\"" + : $"-File \"{command}\" {parameters}"; + } + else + { + return $@"& {{{command}}}"; //NOTE: place to fix any potential future issues with "command part of the command" + } + } + + private static bool ExecuteHeadless(string command, string parameters, bool isScript) + { + var descriptor = isScript ? "script" : "command"; + + try + { + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) + return false; + + var processInfo = new ProcessStartInfo + { + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = psExec, + WorkingDirectory = workingDir, + Arguments = GetProcessArguments(command, parameters, isScript) + }; + + using var process = new Process(); + process.StartInfo = processInfo; + var start = process.Start(); + + if (!start) + { + Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {command}", descriptor, command); + + return false; + } + + return true; + } + catch (Exception ex) + { + Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + + return false; + } + } + + /// + /// Execute a Powershell command, logs the output if it fails + /// + /// + /// + /// + public static bool ExecuteCommand(string command, TimeSpan timeout) => Execute(command, string.Empty, false, timeout); + + /// + /// Executes a Powershell script, logs the output if it fails + /// + /// + /// + /// + public static bool ExecuteScript(string script, string parameters, TimeSpan timeout) => Execute(script, parameters, true, timeout); + + private static bool Execute(string command, string parameters, bool isScript, TimeSpan timeout) + { + var descriptor = isScript ? "script" : "command"; + + try + { + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) return false; + + var processInfo = new ProcessStartInfo + { + FileName = psExec, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + WorkingDirectory = workingDir, + Arguments = GetProcessArguments(command, parameters, isScript) + }; + + using var process = new Process(); + process.StartInfo = processInfo; + var start = process.Start(); + + if (!start) + { + Log.Error("[POWERSHELL] Unable to start processing {descriptor}: {script}", descriptor, command); + + return false; + } + + process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); + + if (process.ExitCode == 0) + return true; + + // non-zero exitcode, process as failed + Log.Error("[POWERSHELL] The {descriptor} returned non-zero exitcode: {code}", descriptor, process.ExitCode); + + var errors = process.StandardError.ReadToEnd().Trim(); + if (!string.IsNullOrEmpty(errors)) + { + Log.Error("[POWERSHELL] Error output:\r\n{output}", errors); + } + else + { + var console = process.StandardOutput.ReadToEnd().Trim(); + if (!string.IsNullOrEmpty(console)) + Log.Error("[POWERSHELL] No error output, console output:\r\n{output}", errors); + else + Log.Error("[POWERSHELL] No error and no console output"); + } + + return false; + } + catch (Exception ex) + { + Log.Fatal(ex, "[POWERSHELL] Fatal error when executing {descriptor}: {command}", descriptor, command); + + return false; + } + } + + private static Encoding TryParseCodePage(int codePage) + { + Encoding encoding = null; + try + { + encoding = Encoding.GetEncoding(codePage); + } + catch + { + // best effort + } + + return encoding; + } + + private static Encoding GetEncoding() + { + var encoding = TryParseCodePage(CultureInfo.InstalledUICulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.CurrentCulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.CurrentUICulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + encoding = TryParseCodePage(CultureInfo.InvariantCulture.TextInfo.OEMCodePage); + if (encoding != null) + return encoding; + + Log.Warning("[POWERSHELL] Cannot parse system text culture to encoding, returning UTF-8 as a fallback, please report this as a GitHub issue"); + + Log.Debug("[POWERSHELL] currentInstalledUICulture {c}", JsonConvert.SerializeObject(CultureInfo.InstalledUICulture.TextInfo)); + Log.Debug("[POWERSHELL] currentCulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentCulture.TextInfo)); + Log.Debug("[POWERSHELL] currentUICulture {c}", JsonConvert.SerializeObject(CultureInfo.CurrentUICulture.TextInfo)); + Log.Debug("[POWERSHELL] invariantCulture {c}", JsonConvert.SerializeObject(CultureInfo.InvariantCulture.TextInfo)); + + return Encoding.UTF8; + } + + /// + /// Executes the command or script, and returns the standard and error output + /// + /// + /// + /// + /// + /// + internal static bool ExecuteWithOutput(string command, TimeSpan timeout, out string output, out string errors) + { + output = string.Empty; + errors = string.Empty; + + try + { + var isScript = command.ToLower().EndsWith(".ps1"); + + var workingDir = string.Empty; + if (isScript) + { + // try to get the script's startup path + var scriptDir = Path.GetDirectoryName(command); + workingDir = !string.IsNullOrEmpty(scriptDir) ? scriptDir : string.Empty; + } + + var psExec = GetPsExecutable(); + if (string.IsNullOrEmpty(psExec)) + return false; + + var encoding = GetEncoding(); + + var processInfo = new ProcessStartInfo + { + FileName = psExec, + RedirectStandardError = true, + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = workingDir, + StandardOutputEncoding = encoding, + StandardErrorEncoding = encoding, + // set the right type of arguments + Arguments = isScript + ? $@"& '{command}'" + : $@"& {{{command}}}" + }; + + using var process = new Process(); + process.StartInfo = processInfo; + + var start = process.Start(); + if (!start) + { + Log.Error("[POWERSHELL] Unable to begin executing the {type}: {cmd}", isScript ? "script" : "command", command); + + return false; + } + + var completed = process.WaitForExit(Convert.ToInt32(timeout.TotalMilliseconds)); + if (!completed) + Log.Error("[POWERSHELL] Timeout executing the {type}: {cmd}", isScript ? "script" : "command", command); + + output = process.StandardOutput.ReadToEnd().Trim(); + errors = process.StandardError.ReadToEnd().Trim(); + + process.StandardOutput.Dispose(); + process.StandardError.Dispose(); + + process.Kill(); + + return completed; + } + catch (Exception ex) + { + Log.Fatal(ex, ex.Message); + + return false; + } + } + + /// + /// Attempt to locate powershell.exe + /// + /// + public static string GetPsExecutable() + { + // try regular location + var psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell\\v1.0\\powershell.exe"); + if (File.Exists(psExec)) + return psExec; + + // try specific + psExec = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.SystemX86), "WindowsPowerShell\\v1.0\\powershell.exe"); + if (File.Exists(psExec)) + return psExec; + + Log.Error("[POWERSHELL] PS executable not found, make sure you have powershell installed on your system"); + return string.Empty; + } } diff --git a/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/AbstractSingleValueSensor.cs b/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/AbstractSingleValueSensor.cs index 63261023..25e41799 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/AbstractSingleValueSensor.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/AbstractSingleValueSensor.cs @@ -3,134 +3,144 @@ using MQTTnet; using Serilog; -namespace HASS.Agent.Shared.Models.HomeAssistant +namespace HASS.Agent.Shared.Models.HomeAssistant; + +/// +/// Abstract singlevalue-sensor from which all singlevalue-sensors are derived +/// +public abstract class AbstractSingleValueSensor : AbstractDiscoverable { - /// - /// Abstract singlevalue-sensor from which all singlevalue-sensors are derived - /// - public abstract class AbstractSingleValueSensor : AbstractDiscoverable - { - public int UpdateIntervalSeconds { get; protected set; } - public DateTime? LastUpdated { get; protected set; } + public int UpdateIntervalSeconds { get; protected set; } + public DateTime? LastUpdated { get; protected set; } - public string PreviousPublishedState { get; protected set; } = string.Empty; - public string PreviousPublishedAttributes { get; protected set; } = string.Empty; + public string PreviousPublishedState { get; protected set; } = string.Empty; + public string PreviousPublishedAttributes { get; protected set; } = string.Empty; - protected AbstractSingleValueSensor(string entityName, string name, int updateIntervalSeconds = 10, string id = default, bool useAttributes = false) - { - Id = id == null || id == Guid.Empty.ToString() ? Guid.NewGuid().ToString() : id; - EntityName = entityName; - Name = name; - UpdateIntervalSeconds = updateIntervalSeconds; - Domain = "sensor"; - UseAttributes = useAttributes; - } + protected AbstractSingleValueSensor(string entityName, string name, int updateIntervalSeconds = 10, string id = default, bool useAttributes = false) + { + Id = id == null || id == Guid.Empty.ToString() ? Guid.NewGuid().ToString() : id; + EntityName = entityName; + Name = name; + UpdateIntervalSeconds = updateIntervalSeconds; + Domain = "sensor"; + UseAttributes = useAttributes; + } - protected SensorDiscoveryConfigModel AutoDiscoveryConfigModel; - protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config) - { - AutoDiscoveryConfigModel = config; - return config; - } + protected SensorDiscoveryConfigModel AutoDiscoveryConfigModel; + protected SensorDiscoveryConfigModel SetAutoDiscoveryConfigModel(SensorDiscoveryConfigModel config) + { + AutoDiscoveryConfigModel = config; + return config; + } - public override void ClearAutoDiscoveryConfig() => AutoDiscoveryConfigModel = null; + public override void ClearAutoDiscoveryConfig() => AutoDiscoveryConfigModel = null; - // nullable in preparation for possible future "nullable enablement" - public abstract string? GetState(); + // nullable in preparation for possible future "nullable enablement" + public abstract string? GetState(); - // nullable in preparation for possible future "nullable enablement" - public abstract string? GetAttributes(); + // nullable in preparation for possible future "nullable enablement" + public abstract string? GetAttributes(); - public void ResetChecks() - { - LastUpdated = DateTime.MinValue; + public void ResetChecks() + { + LastUpdated = DateTime.MinValue; - PreviousPublishedState = string.Empty; - PreviousPublishedAttributes = string.Empty; - } + PreviousPublishedState = string.Empty; + PreviousPublishedAttributes = string.Empty; + } - public async Task PublishStateAsync(bool respectChecks = true) + public async Task PublishStateAsync(bool respectChecks = true) + { + try { - try - { - if (Variables.MqttManager == null) return; + if (Variables.MqttManager == null) + return; - // are we asked to check elapsed time? - if (respectChecks) - { - if (LastUpdated.HasValue && LastUpdated.Value.AddSeconds(UpdateIntervalSeconds) > DateTime.Now) return; - } - - // get the current state/attributes - var state = GetState(); - if (state == null) + // are we asked to check elapsed time? + if (respectChecks) + { + if (LastUpdated.HasValue && LastUpdated.Value.AddSeconds(UpdateIntervalSeconds) > DateTime.Now) return; + } - var attributes = GetAttributes(); + // get the current state/attributes + var state = GetState(); + if (state == null) + return; - // are we asked to check state changes? - if (respectChecks) + var attributes = GetAttributes(); + + // are we asked to check state changes? + if (respectChecks) + { + if (PreviousPublishedState == state && PreviousPublishedAttributes == attributes) { - if (PreviousPublishedState == state && PreviousPublishedAttributes == attributes) return; + LastUpdated = DateTime.Now; + return; } + } - // fetch the autodiscovery config - var autoDiscoConfig = (SensorDiscoveryConfigModel)GetAutoDiscoveryConfig(); - if (autoDiscoConfig == null) return; - - // prepare the state message - var message = new MqttApplicationMessageBuilder() - .WithTopic(autoDiscoConfig.State_topic) - .WithPayload(state) + // fetch the autodiscovery config + var autoDiscoConfig = (SensorDiscoveryConfigModel)GetAutoDiscoveryConfig(); + if (autoDiscoConfig == null) + return; + + // prepare the state message + var message = new MqttApplicationMessageBuilder() + .WithTopic(autoDiscoConfig.State_topic) + .WithPayload(state) + .WithRetainFlag(Variables.MqttManager.UseRetainFlag()) + .Build(); + + // send it + var published = await Variables.MqttManager.PublishAsync(message); + if (!published) + return; + + // optionally prepare and send attributes + if (UseAttributes && attributes != null) + { + message = new MqttApplicationMessageBuilder() + .WithTopic(autoDiscoConfig.Json_attributes_topic) + .WithPayload(attributes) .WithRetainFlag(Variables.MqttManager.UseRetainFlag()) .Build(); - // send it - var published = await Variables.MqttManager.PublishAsync(message); + published = await Variables.MqttManager.PublishAsync(message); if (!published) - return; // failed, don't store the state - - // optionally prepare and send attributes - if (UseAttributes && attributes != null) - { - message = new MqttApplicationMessageBuilder() - .WithTopic(autoDiscoConfig.Json_attributes_topic) - .WithPayload(attributes) - .WithRetainFlag(Variables.MqttManager.UseRetainFlag()) - .Build(); - - published = await Variables.MqttManager.PublishAsync(message); - if (!published) - return; // failed, don't store the state - } - - // only store the values if the checks are respected - // otherwise, we might stay in 'unknown' state until the value changes - if (!respectChecks) return; + } - PreviousPublishedState = state; - if (attributes != null) - PreviousPublishedAttributes = attributes; + // only store the values if the checks are respected + // otherwise, we might stay in 'unknown' state until the value changes + if (!respectChecks) + return; - LastUpdated = DateTime.Now; - } - catch (Exception ex) - { - Log.Fatal("[SENSOR] [{name}] Error publishing state: {err}", EntityName, ex.Message); - } - } + PreviousPublishedState = state; + if (attributes != null) + PreviousPublishedAttributes = attributes; - public async Task PublishAutoDiscoveryConfigAsync() - { - if (Variables.MqttManager == null) return; - await Variables.MqttManager.AnnounceAutoDiscoveryConfigAsync(this, Domain); + LastUpdated = DateTime.Now; } - - public async Task UnPublishAutoDiscoveryConfigAsync() + catch (Exception ex) { - if (Variables.MqttManager == null) return; - await Variables.MqttManager.AnnounceAutoDiscoveryConfigAsync(this, Domain, true); + Log.Fatal("[SENSOR] [{name}] Error publishing state: {err}", EntityName, ex.Message); } } + + public async Task PublishAutoDiscoveryConfigAsync() + { + if (Variables.MqttManager == null) + return; + + await Variables.MqttManager.AnnounceAutoDiscoveryConfigAsync(this, Domain); + } + + public async Task UnPublishAutoDiscoveryConfigAsync() + { + if (Variables.MqttManager == null) + return; + + await Variables.MqttManager.AnnounceAutoDiscoveryConfigAsync(this, Domain, true); + } } diff --git a/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/DiscoveryConfigModel.cs b/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/DiscoveryConfigModel.cs index 68c4d4a6..4a98233b 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/DiscoveryConfigModel.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Models/HomeAssistant/DiscoveryConfigModel.cs @@ -143,6 +143,20 @@ public string Object_id public string Value_template { get; set; } } + [SuppressMessage("ReSharper", "InconsistentNaming")] + public class CameraSensorDiscoveryConfigModel : SensorDiscoveryConfigModel + { + /// + /// Messages published to this topic need to contain full contents of an image + /// + public string Topic { get; set; } + + /// + /// (Optional) The encoding of the image payloads received. + /// + public string Image_encoding { get; set; } + } + [SuppressMessage("ReSharper", "InconsistentNaming")] public class CommandDiscoveryConfigModel : DiscoveryConfigModel { diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.Designer.cs b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.Designer.cs index b529d8a8..be913c63 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.Designer.cs +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.Designer.cs @@ -1284,6 +1284,15 @@ internal static string CommandType_SetApplicationVolumeCommand { } } + /// + /// Looks up a localized string similar to SetAudioInputCommand. + /// + internal static string CommandType_SetAudioInputCommand { + get { + return ResourceManager.GetString("CommandType_SetAudioInputCommand", resourceCulture); + } + } + /// /// Looks up a localized string similar to SetAudioOutputCommand. /// @@ -6735,6 +6744,15 @@ internal static string SensorType_ProcessActiveSensor { } } + /// + /// Looks up a localized string similar to Screenshot. + /// + internal static string SensorType_ScreenshotSensor { + get { + return ResourceManager.GetString("SensorType_ScreenshotSensor", resourceCulture); + } + } + /// /// Looks up a localized string similar to ServiceState. /// diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.de.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.de.resx index 883f905a..736f2957 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.de.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.de.resx @@ -3358,4 +3358,10 @@ Willst Du den Runtime Installer herunterladen? Legen Sie den Audioausgabebefehl fest + + Bildschirmfoto + + + Legen Sie den Audioeingabebefehl fest + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.en.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.en.resx index d635f2ae..decc96d4 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.en.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.en.resx @@ -3225,9 +3225,6 @@ Do you want to download the runtime installer? InternalDeviceSensor - - SwitchDesktop - ActiveDesktop @@ -3237,4 +3234,10 @@ Do you want to download the runtime installer? SetAudioOutputCommand + + Screenshot + + + SetAudioInputCommand + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.es.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.es.resx index b384718b..58a31529 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.es.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.es.resx @@ -3234,4 +3234,10 @@ Oculta, Maximizada, Minimizada, Normal y Desconocida. Establecer comando de salida de audio + + Captura de pantalla + + + Establecer comando de entrada de audio + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.fr.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.fr.resx index 677293f0..32f1ca61 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.fr.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.fr.resx @@ -3267,4 +3267,10 @@ Do you want to download the runtime installer? Définir la commande de sortie audio + + Capture d'écran + + + Définir la commande d'entrée audio + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.nl.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.nl.resx index f9248ab5..664534da 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.nl.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.nl.resx @@ -3256,4 +3256,10 @@ Wil je de runtime installatie downloaden? Stel het audio-uitvoercommando in + + Schermafbeelding + + + Stel het audio-invoercommando in + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pl.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pl.resx index 563325c4..c6ffb694 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pl.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pl.resx @@ -3344,4 +3344,10 @@ Czy chcesz pobrać plik instalacyjny? Ustaw wyjście audio + + Zrzut ekranu + + + Ustaw wejście audio + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pt-br.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pt-br.resx index ac97d593..76c7b10d 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pt-br.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.pt-br.resx @@ -3281,4 +3281,10 @@ Deseja baixar o Microsoft WebView2 runtime? Definir comando de saída de áudio + + Captura de tela + + + Definir comando de entrada de áudio + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.resx index 311f0b79..94dad3f7 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.resx @@ -3217,4 +3217,10 @@ Do you want to download the runtime installer? SetAudioOutputCommand + + Screenshot + + + SetAudioInputCommand + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.ru.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.ru.resx index 2d922706..758aa8bd 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.ru.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.ru.resx @@ -3302,4 +3302,10 @@ Home Assistant. Установить команду вывода звука + + Скриншот + + + Установить команду аудиовхода + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.sl.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.sl.resx index d9f52478..77cf6153 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.sl.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.sl.resx @@ -3383,4 +3383,10 @@ Ali želite prenesti runtime installer? Nastavite ukaz za avdio izhod + + Posnetek zaslona + + + Nastavite ukaz za zvočni vhod + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.tr.resx b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.tr.resx index 1b6e9a8c..e2c8d79d 100644 --- a/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.tr.resx +++ b/src/HASS.Agent/HASS.Agent.Shared/Resources/Localization/Languages.tr.resx @@ -2841,4 +2841,10 @@ Lütfen aracınız için credentialları sağlayın, HA Mosquitto eklentisini ku Ses Çıkışı Komutunu Ayarla + + Ekran görüntüsü + + + Ses Giriş Komutunu Ayarla + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Commands/CommandsManager.cs b/src/HASS.Agent/HASS.Agent/Commands/CommandsManager.cs index a0850e70..0d4dbc69 100644 --- a/src/HASS.Agent/HASS.Agent/Commands/CommandsManager.cs +++ b/src/HASS.Agent/HASS.Agent/Commands/CommandsManager.cs @@ -17,6 +17,8 @@ internal static class CommandsManager private static bool _active = true; private static bool _pause; + private static bool _discoveryPublished = false; + private static DateTime _lastAutoDiscoPublish = DateTime.MinValue; /// @@ -66,6 +68,8 @@ internal static async Task UnpublishAllCommands() await command.UnPublishAutoDiscoveryConfigAsync(); await Variables.MqttManager.UnsubscribeAsync(command); } + + _discoveryPublished = false; } /// @@ -103,18 +107,24 @@ private static async void Process() firstRun = false; + // publish availability & autodiscovery every 30 sec if ((DateTime.Now - _lastAutoDiscoPublish).TotalSeconds > 30) { await Variables.MqttManager.AnnounceAvailabilityAsync(); - foreach (var command in Variables.Commands - .TakeWhile(_ => !_pause) - .TakeWhile(_ => _active)) + if (!_discoveryPublished) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) - continue; + foreach (var command in Variables.Commands + .TakeWhile(_ => !_pause) + .TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) + continue; - await command.PublishAutoDiscoveryConfigAsync(); + await command.PublishAutoDiscoveryConfigAsync(); + } + + _discoveryPublished = true; } if (!subscribed) @@ -483,6 +493,14 @@ internal static void LoadCommandInfo() // ================================= + commandInfoCard = new CommandInfoCard(CommandType.SetAudioInputCommand, + Languages.CommandsManager_SetAudioInputCommandDescription, + true, false, true); + + CommandInfoCards.Add(commandInfoCard.CommandType, commandInfoCard); + + // ================================= + commandInfoCard = new CommandInfoCard(CommandType.ShutdownCommand, Languages.CommandsManager_ShutdownCommandDescription, true, true, false); diff --git a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.Designer.cs b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.Designer.cs index 48c9bfbb..670ddf22 100644 --- a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.Designer.cs +++ b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.Designer.cs @@ -56,6 +56,7 @@ private void InitializeComponent() this.LblClientId = new System.Windows.Forms.Label(); this.NumMqttPort = new Syncfusion.Windows.Forms.Tools.NumericUpDownExt(); this.PbShow = new System.Windows.Forms.PictureBox(); + this.CbIgnoreGracePeriod = new System.Windows.Forms.CheckBox(); this.CbEnableMqtt = new System.Windows.Forms.CheckBox(); this.LblMqttDisabledWarning = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)(this.NumMqttPort)).BeginInit(); @@ -460,51 +461,68 @@ private void InitializeComponent() this.LblMqttDisabledWarning.Text = Languages.ConfigMqtt_LblMqttDisabledWarning; this.LblMqttDisabledWarning.Visible = false; // + // CbIgnoreGracePeriod + // + this.CbIgnoreGracePeriod.AccessibleDescription = "Ignore grace period after waking up from hibernation."; + this.CbIgnoreGracePeriod.AccessibleName = "Ignore grace period"; + this.CbIgnoreGracePeriod.AccessibleRole = AccessibleRole.CheckButton; + this.CbIgnoreGracePeriod.AutoSize = true; + this.CbIgnoreGracePeriod.Checked = false; + this.CbIgnoreGracePeriod.CheckState = CheckState.Unchecked; + this.CbIgnoreGracePeriod.Font = new Font("Segoe UI", 10F); + this.CbIgnoreGracePeriod.Location = new Point(59, 612); + this.CbIgnoreGracePeriod.Name = "CbIgnoreGracePeriod"; + this.CbIgnoreGracePeriod.Size = new Size(354, 23); + this.CbIgnoreGracePeriod.TabIndex = 103; + this.CbIgnoreGracePeriod.Text = Languages.ConfigMqtt_CbIgnoreGracePeriod; + this.CbIgnoreGracePeriod.UseVisualStyleBackColor = true; + this.CbIgnoreGracePeriod.CheckedChanged += new System.EventHandler(this.CbIgnoreGracePeriod_CheckedChanged); + // // ConfigMqtt // this.AccessibleDescription = "Panel containing the MQTT client configuration."; this.AccessibleName = "MQTT"; - this.AccessibleRole = System.Windows.Forms.AccessibleRole.Pane; - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); - this.Controls.Add(this.LblMqttDisabledWarning); - this.Controls.Add(this.CbEnableMqtt); - this.Controls.Add(this.PbShow); - this.Controls.Add(this.NumMqttPort); - this.Controls.Add(this.LblTip2); - this.Controls.Add(this.TbMqttClientId); - this.Controls.Add(this.LblClientId); - this.Controls.Add(this.LblTip3); - this.Controls.Add(this.TbMqttClientCertificate); - this.Controls.Add(this.LblClientCert); - this.Controls.Add(this.TbMqttRootCertificate); - this.Controls.Add(this.LblRootCert); - this.Controls.Add(this.CbUseRetainFlag); - this.Controls.Add(this.CbAllowUntrustedCertificates); - this.Controls.Add(this.BtnMqttClearConfig); - this.Controls.Add(this.LblTip1); - this.Controls.Add(this.LblInfo1); - this.Controls.Add(this.TbMqttDiscoveryPrefix); - this.Controls.Add(this.LblDiscoPrefix); - this.Controls.Add(this.TbMqttPassword); - this.Controls.Add(this.TbMqttUsername); - this.Controls.Add(this.TbMqttAddress); - this.Controls.Add(this.CbMqttTls); - this.Controls.Add(this.LblBrokerPassword); - this.Controls.Add(this.LblBrokerUsername); - this.Controls.Add(this.LblBrokerPort); - this.Controls.Add(this.LblBrokerIp); - this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.Margin = new System.Windows.Forms.Padding(4); + this.AccessibleRole = AccessibleRole.Pane; + this.AutoScaleDimensions = new SizeF(96F, 96F); + this.AutoScaleMode = AutoScaleMode.Dpi; + this.BackColor = Color.FromArgb(45, 45, 48); + this.Controls.Add(CbIgnoreGracePeriod); + this.Controls.Add(LblMqttDisabledWarning); + this.Controls.Add(CbEnableMqtt); + this.Controls.Add(PbShow); + this.Controls.Add(NumMqttPort); + this.Controls.Add(LblTip2); + this.Controls.Add(TbMqttClientId); + this.Controls.Add(LblClientId); + this.Controls.Add(LblTip3); + this.Controls.Add(TbMqttClientCertificate); + this.Controls.Add(LblClientCert); + this.Controls.Add(TbMqttRootCertificate); + this.Controls.Add(LblRootCert); + this.Controls.Add(CbUseRetainFlag); + this.Controls.Add(CbAllowUntrustedCertificates); + this.Controls.Add(BtnMqttClearConfig); + this.Controls.Add(LblTip1); + this.Controls.Add(LblInfo1); + this.Controls.Add(TbMqttDiscoveryPrefix); + this.Controls.Add(LblDiscoPrefix); + this.Controls.Add(TbMqttPassword); + this.Controls.Add(TbMqttUsername); + this.Controls.Add(TbMqttAddress); + this.Controls.Add(CbMqttTls); + this.Controls.Add(LblBrokerPassword); + this.Controls.Add(LblBrokerUsername); + this.Controls.Add(LblBrokerPort); + this.Controls.Add(LblBrokerIp); + this.ForeColor = Color.FromArgb(241, 241, 241); + this.Margin = new Padding(4); this.Name = "ConfigMqtt"; - this.Size = new System.Drawing.Size(700, 614); - this.Load += new System.EventHandler(this.ConfigMqtt_Load); - ((System.ComponentModel.ISupportInitialize)(this.NumMqttPort)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.PbShow)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - + this.Size = new Size(700, 658); + this.Load += ConfigMqtt_Load; + ((System.ComponentModel.ISupportInitialize)NumMqttPort).EndInit(); + ((System.ComponentModel.ISupportInitialize)PbShow).EndInit(); + ResumeLayout(false); + PerformLayout(); } #endregion @@ -536,5 +554,6 @@ private void InitializeComponent() private PictureBox PbShow; internal CheckBox CbEnableMqtt; private Label LblMqttDisabledWarning; + internal CheckBox CbIgnoreGracePeriod; } } diff --git a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.cs b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.cs index c39635d7..83354e26 100644 --- a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.cs +++ b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.cs @@ -60,5 +60,10 @@ private void CbEnableMqtt_CheckedChanged(object sender, EventArgs e) { LblMqttDisabledWarning.Visible = CbEnableMqtt.CheckState != CheckState.Checked; } + + private void CbIgnoreGracePeriod_CheckedChanged(object sender, EventArgs e) + { + + } } } diff --git a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.resx b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.resx index c037b6da..1435c3aa 100644 --- a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.resx +++ b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigMqtt.resx @@ -1,17 +1,17 @@  - @@ -117,11 +117,18 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Commands and sensors use MQTT, as well as notifications and media player functions when using the new integration. + +Please provide credentials for your broker, if you're using the HA Mosquitto addon, you can probably use the preset address. + +Note: these settings (excluding the Client ID) will also be applied to the satellite service. + iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wAAADsABataJCQAAAZZJREFUSEvtk61TAlEUxTcQDAajwWAgGgwG/gCCgUAwGA0GgsFIZMZANBgMBgOR + vgAADr4B6kKxwAAAAZZJREFUSEvtk61TAlEUxTcQDAajwWAgGgwG/gCCgUAwGA0GgsFIZMZANBgMBgOR YDQYNliAhdkgMwQCwUAgGAgGgv7Oeh/zdmdRscqZObPv3Y9z37v3bbDBBgkGg0Gx1+uV+/3+mdjtdo/Z H5j7bwjDcAuxOozhRx6jKBrzvYrjeMfSfgdOd0rixBf7jhSawRqHKphEPnRtEp6yAmswgiWTSwNHlVPM XTDrB/js9qtIzAjeebYF+5rJfoGTX3oBUw1RdtahZ88lYmOLLWnt7DyMa9mz4nGn09lLHEBBnm8V2xYe diff --git a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.Designer.cs b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.Designer.cs index 53fdff2f..8fec92e9 100644 --- a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.Designer.cs +++ b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.Designer.cs @@ -30,320 +30,318 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - this.LblInfo1 = new System.Windows.Forms.Label(); - this.CbDefaultMenu = new System.Windows.Forms.CheckBox(); - this.CbShowWebView = new System.Windows.Forms.CheckBox(); - this.TbWebViewUrl = new System.Windows.Forms.TextBox(); - this.LblWebViewUrl = new System.Windows.Forms.Label(); - this.LblX = new System.Windows.Forms.Label(); - this.LblWebViewSize = new System.Windows.Forms.Label(); - this.BtnShowWebViewPreview = new Syncfusion.WinForms.Controls.SfButton(); - this.NumWebViewWidth = new Syncfusion.Windows.Forms.Tools.NumericUpDownExt(); - this.NumWebViewHeight = new Syncfusion.Windows.Forms.Tools.NumericUpDownExt(); - this.BtnWebViewReset = new Syncfusion.WinForms.Controls.SfButton(); - this.CbWebViewKeepLoaded = new System.Windows.Forms.CheckBox(); - this.LblInfo2 = new System.Windows.Forms.Label(); - this.CbWebViewShowMenuOnLeftClick = new System.Windows.Forms.CheckBox(); - ((System.ComponentModel.ISupportInitialize)(this.NumWebViewWidth)).BeginInit(); - ((System.ComponentModel.ISupportInitialize)(this.NumWebViewHeight)).BeginInit(); - this.SuspendLayout(); + LblInfo1 = new Label(); + CbUseModernIcon = new CheckBox(); + CbDefaultMenu = new CheckBox(); + CbShowWebView = new CheckBox(); + TbWebViewUrl = new TextBox(); + LblWebViewUrl = new Label(); + LblX = new Label(); + LblWebViewSize = new Label(); + BtnShowWebViewPreview = new Syncfusion.WinForms.Controls.SfButton(); + NumWebViewWidth = new Syncfusion.Windows.Forms.Tools.NumericUpDownExt(); + NumWebViewHeight = new Syncfusion.Windows.Forms.Tools.NumericUpDownExt(); + BtnWebViewReset = new Syncfusion.WinForms.Controls.SfButton(); + CbWebViewKeepLoaded = new CheckBox(); + LblInfo2 = new Label(); + CbWebViewShowMenuOnLeftClick = new CheckBox(); + ((System.ComponentModel.ISupportInitialize)NumWebViewWidth).BeginInit(); + ((System.ComponentModel.ISupportInitialize)NumWebViewHeight).BeginInit(); + SuspendLayout(); // // LblInfo1 // - this.LblInfo1.AccessibleDescription = "Tray icon information."; - this.LblInfo1.AccessibleName = "Information"; - this.LblInfo1.AccessibleRole = System.Windows.Forms.AccessibleRole.StaticText; - this.LblInfo1.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.LblInfo1.Location = new System.Drawing.Point(70, 36); - this.LblInfo1.Name = "LblInfo1"; - this.LblInfo1.Size = new System.Drawing.Size(541, 78); - this.LblInfo1.TabIndex = 31; - this.LblInfo1.Text = Languages.ConfigTrayIcon_LblInfo1; + LblInfo1.AccessibleDescription = "Tray icon information."; + LblInfo1.AccessibleName = "Information"; + LblInfo1.AccessibleRole = AccessibleRole.StaticText; + LblInfo1.Font = new Font("Segoe UI", 10F); + LblInfo1.Location = new Point(70, 102); + LblInfo1.Name = "LblInfo1"; + LblInfo1.Size = new Size(541, 36); + LblInfo1.TabIndex = 31; + LblInfo1.Text = "Control the behaviour of the tray icon when it is right-clicked."; + // + // CbUseModernIcon + // + CbUseModernIcon.AccessibleDescription = "If enabled, modern white and transparent icon will be used"; + CbUseModernIcon.AccessibleName = "Use modern icon"; + CbUseModernIcon.AccessibleRole = AccessibleRole.CheckButton; + CbUseModernIcon.AutoSize = true; + CbUseModernIcon.Font = new Font("Segoe UI", 10F); + CbUseModernIcon.Location = new Point(70, 42); + CbUseModernIcon.Name = "CbUseModernIcon"; + CbUseModernIcon.Size = new Size(160, 23); + CbUseModernIcon.TabIndex = 50; + CbUseModernIcon.Text = Languages.ConfigTrayIcon_CbUseModernIcon; + CbUseModernIcon.UseVisualStyleBackColor = true; // // CbDefaultMenu // - this.CbDefaultMenu.AccessibleDescription = "If enabled, right clicking the system tray icon will show the default menu."; - this.CbDefaultMenu.AccessibleName = "Show default menu"; - this.CbDefaultMenu.AccessibleRole = System.Windows.Forms.AccessibleRole.CheckButton; - this.CbDefaultMenu.AutoSize = true; - this.CbDefaultMenu.Checked = true; - this.CbDefaultMenu.CheckState = System.Windows.Forms.CheckState.Checked; - this.CbDefaultMenu.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.CbDefaultMenu.Location = new System.Drawing.Point(70, 117); - this.CbDefaultMenu.Name = "CbDefaultMenu"; - this.CbDefaultMenu.Size = new System.Drawing.Size(145, 23); - this.CbDefaultMenu.TabIndex = 0; - this.CbDefaultMenu.Text = global::HASS.Agent.Resources.Localization.Languages.ConfigTrayIcon_CbDefaultMenu; - this.CbDefaultMenu.UseVisualStyleBackColor = true; - this.CbDefaultMenu.CheckedChanged += new System.EventHandler(this.CbDefaultMenu_CheckedChanged); + CbDefaultMenu.AccessibleDescription = "If enabled, right clicking the system tray icon will show the default menu."; + CbDefaultMenu.AccessibleName = "Show default menu"; + CbDefaultMenu.AccessibleRole = AccessibleRole.CheckButton; + CbDefaultMenu.AutoSize = true; + CbDefaultMenu.Checked = true; + CbDefaultMenu.CheckState = CheckState.Checked; + CbDefaultMenu.Font = new Font("Segoe UI", 10F); + CbDefaultMenu.Location = new Point(70, 141); + CbDefaultMenu.Name = "CbDefaultMenu"; + CbDefaultMenu.Size = new Size(149, 23); + CbDefaultMenu.TabIndex = 0; + CbDefaultMenu.Text = Languages.ConfigTrayIcon_CbDefaultMenu; + CbDefaultMenu.UseVisualStyleBackColor = true; + CbDefaultMenu.CheckedChanged += CbDefaultMenu_CheckedChanged; // // CbShowWebView // - this.CbShowWebView.AccessibleDescription = "If enabled, right clicking the system tray icon will show a webview with the url " + - "you provide."; - this.CbShowWebView.AccessibleName = "Show webview"; - this.CbShowWebView.AccessibleRole = System.Windows.Forms.AccessibleRole.CheckButton; - this.CbShowWebView.AutoSize = true; - this.CbShowWebView.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.CbShowWebView.Location = new System.Drawing.Point(70, 209); - this.CbShowWebView.Name = "CbShowWebView"; - this.CbShowWebView.Size = new System.Drawing.Size(116, 23); - this.CbShowWebView.TabIndex = 1; - this.CbShowWebView.Text = global::HASS.Agent.Resources.Localization.Languages.ConfigTrayIcon_CbShowWebView; - this.CbShowWebView.UseVisualStyleBackColor = true; - this.CbShowWebView.CheckedChanged += new System.EventHandler(this.CbShowWebView_CheckedChanged); + CbShowWebView.AccessibleDescription = "If enabled, right clicking the system tray icon will show a webview with the url you provide."; + CbShowWebView.AccessibleName = "Show webview"; + CbShowWebView.AccessibleRole = AccessibleRole.CheckButton; + CbShowWebView.AutoSize = true; + CbShowWebView.Font = new Font("Segoe UI", 10F); + CbShowWebView.Location = new Point(70, 209); + CbShowWebView.Name = "CbShowWebView"; + CbShowWebView.Size = new Size(121, 23); + CbShowWebView.TabIndex = 1; + CbShowWebView.Text = Languages.ConfigTrayIcon_CbShowWebView; + CbShowWebView.UseVisualStyleBackColor = true; + CbShowWebView.CheckedChanged += CbShowWebView_CheckedChanged; // // TbWebViewUrl // - this.TbWebViewUrl.AccessibleDescription = "The URL to show. Defaults to the Home Assistant API\'s URL."; - this.TbWebViewUrl.AccessibleName = "URL"; - this.TbWebViewUrl.AccessibleRole = System.Windows.Forms.AccessibleRole.Text; - this.TbWebViewUrl.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.TbWebViewUrl.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.TbWebViewUrl.Enabled = false; - this.TbWebViewUrl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.TbWebViewUrl.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.TbWebViewUrl.Location = new System.Drawing.Point(90, 280); - this.TbWebViewUrl.Name = "TbWebViewUrl"; - this.TbWebViewUrl.Size = new System.Drawing.Size(521, 25); - this.TbWebViewUrl.TabIndex = 2; + TbWebViewUrl.AccessibleDescription = "The URL to show. Defaults to the Home Assistant API's URL."; + TbWebViewUrl.AccessibleName = "URL"; + TbWebViewUrl.AccessibleRole = AccessibleRole.Text; + TbWebViewUrl.BackColor = Color.FromArgb(63, 63, 70); + TbWebViewUrl.BorderStyle = BorderStyle.FixedSingle; + TbWebViewUrl.Enabled = false; + TbWebViewUrl.Font = new Font("Segoe UI", 10F); + TbWebViewUrl.ForeColor = Color.FromArgb(241, 241, 241); + TbWebViewUrl.Location = new Point(90, 280); + TbWebViewUrl.Name = "TbWebViewUrl"; + TbWebViewUrl.Size = new Size(521, 25); + TbWebViewUrl.TabIndex = 2; // // LblWebViewUrl // - this.LblWebViewUrl.AccessibleDescription = "URL textbox description"; - this.LblWebViewUrl.AccessibleName = "URL info"; - this.LblWebViewUrl.AccessibleRole = System.Windows.Forms.AccessibleRole.StaticText; - this.LblWebViewUrl.AutoSize = true; - this.LblWebViewUrl.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.LblWebViewUrl.Location = new System.Drawing.Point(87, 258); - this.LblWebViewUrl.Name = "LblWebViewUrl"; - this.LblWebViewUrl.Size = new System.Drawing.Size(349, 19); - this.LblWebViewUrl.TabIndex = 49; - this.LblWebViewUrl.Text = Languages.ConfigTrayIcon_LblWebViewUrl; + LblWebViewUrl.AccessibleDescription = "URL textbox description"; + LblWebViewUrl.AccessibleName = "URL info"; + LblWebViewUrl.AccessibleRole = AccessibleRole.StaticText; + LblWebViewUrl.AutoSize = true; + LblWebViewUrl.Font = new Font("Segoe UI", 10F); + LblWebViewUrl.Location = new Point(87, 258); + LblWebViewUrl.Name = "LblWebViewUrl"; + LblWebViewUrl.Size = new Size(415, 19); + LblWebViewUrl.TabIndex = 49; + LblWebViewUrl.Text = "&WebView URL (For instance, your Home Assistant Dashboard URL)"; // // LblX // - this.LblX.AccessibleDescription = "Shows X, meaning \'by\' in this context."; - this.LblX.AccessibleName = "X info"; - this.LblX.AccessibleRole = System.Windows.Forms.AccessibleRole.StaticText; - this.LblX.AutoSize = true; - this.LblX.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.LblX.Location = new System.Drawing.Point(184, 350); - this.LblX.Name = "LblX"; - this.LblX.Size = new System.Drawing.Size(17, 19); - this.LblX.TabIndex = 53; - this.LblX.Text = "X"; + LblX.AccessibleDescription = "Shows X, meaning 'by' in this context."; + LblX.AccessibleName = "X info"; + LblX.AccessibleRole = AccessibleRole.StaticText; + LblX.AutoSize = true; + LblX.Font = new Font("Segoe UI", 10F); + LblX.Location = new Point(184, 350); + LblX.Name = "LblX"; + LblX.Size = new Size(17, 19); + LblX.TabIndex = 53; + LblX.Text = "X"; // // LblWebViewSize // - this.LblWebViewSize.AccessibleDescription = "Size description."; - this.LblWebViewSize.AccessibleName = "Size info"; - this.LblWebViewSize.AccessibleRole = System.Windows.Forms.AccessibleRole.StaticText; - this.LblWebViewSize.AutoSize = true; - this.LblWebViewSize.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.LblWebViewSize.Location = new System.Drawing.Point(85, 326); - this.LblWebViewSize.Name = "LblWebViewSize"; - this.LblWebViewSize.Size = new System.Drawing.Size(31, 19); - this.LblWebViewSize.TabIndex = 51; - this.LblWebViewSize.Text = Languages.ConfigTrayIcon_LblWebViewSize; + LblWebViewSize.AccessibleDescription = "Size description."; + LblWebViewSize.AccessibleName = "Size info"; + LblWebViewSize.AccessibleRole = AccessibleRole.StaticText; + LblWebViewSize.AutoSize = true; + LblWebViewSize.Font = new Font("Segoe UI", 10F); + LblWebViewSize.Location = new Point(85, 326); + LblWebViewSize.Name = "LblWebViewSize"; + LblWebViewSize.Size = new Size(58, 19); + LblWebViewSize.TabIndex = 51; + LblWebViewSize.Text = "Size (px)"; // // BtnShowWebViewPreview // - this.BtnShowWebViewPreview.AccessibleDescription = "Shows the webview, using the currently configured values."; - this.BtnShowWebViewPreview.AccessibleName = "Webview preview"; - this.BtnShowWebViewPreview.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; - this.BtnShowWebViewPreview.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnShowWebViewPreview.Enabled = false; - this.BtnShowWebViewPreview.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.BtnShowWebViewPreview.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnShowWebViewPreview.Location = new System.Drawing.Point(405, 348); - this.BtnShowWebViewPreview.Name = "BtnShowWebViewPreview"; - this.BtnShowWebViewPreview.Size = new System.Drawing.Size(206, 26); - this.BtnShowWebViewPreview.Style.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnShowWebViewPreview.Style.FocusedBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnShowWebViewPreview.Style.FocusedForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnShowWebViewPreview.Style.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnShowWebViewPreview.Style.HoverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnShowWebViewPreview.Style.HoverForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnShowWebViewPreview.Style.PressedForeColor = System.Drawing.Color.Black; - this.BtnShowWebViewPreview.TabIndex = 6; - this.BtnShowWebViewPreview.Text = global::HASS.Agent.Resources.Localization.Languages.ConfigTrayIcon_BtnShowWebViewPreview; - this.BtnShowWebViewPreview.UseVisualStyleBackColor = false; - this.BtnShowWebViewPreview.Click += new System.EventHandler(this.BtnShowWebViewPreview_Click); + BtnShowWebViewPreview.AccessibleDescription = "Shows the webview, using the currently configured values."; + BtnShowWebViewPreview.AccessibleName = "Webview preview"; + BtnShowWebViewPreview.AccessibleRole = AccessibleRole.PushButton; + BtnShowWebViewPreview.BackColor = Color.FromArgb(63, 63, 70); + BtnShowWebViewPreview.Enabled = false; + BtnShowWebViewPreview.Font = new Font("Segoe UI", 10F); + BtnShowWebViewPreview.ForeColor = Color.FromArgb(241, 241, 241); + BtnShowWebViewPreview.Location = new Point(405, 348); + BtnShowWebViewPreview.Name = "BtnShowWebViewPreview"; + BtnShowWebViewPreview.Size = new Size(206, 26); + BtnShowWebViewPreview.Style.BackColor = Color.FromArgb(63, 63, 70); + BtnShowWebViewPreview.Style.FocusedBackColor = Color.FromArgb(63, 63, 70); + BtnShowWebViewPreview.Style.FocusedForeColor = Color.FromArgb(241, 241, 241); + BtnShowWebViewPreview.Style.ForeColor = Color.FromArgb(241, 241, 241); + BtnShowWebViewPreview.Style.HoverBackColor = Color.FromArgb(63, 63, 70); + BtnShowWebViewPreview.Style.HoverForeColor = Color.FromArgb(241, 241, 241); + BtnShowWebViewPreview.Style.PressedForeColor = Color.Black; + BtnShowWebViewPreview.TabIndex = 6; + BtnShowWebViewPreview.Text = Languages.ConfigTrayIcon_BtnShowWebViewPreview; + BtnShowWebViewPreview.UseVisualStyleBackColor = false; + BtnShowWebViewPreview.Click += BtnShowWebViewPreview_Click; // // NumWebViewWidth // - this.NumWebViewWidth.AccessibleDescription = "The width of the webview. Only accepts numeric values."; - this.NumWebViewWidth.AccessibleName = "Width"; - this.NumWebViewWidth.AccessibleRole = System.Windows.Forms.AccessibleRole.Text; - this.NumWebViewWidth.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.NumWebViewWidth.BeforeTouchSize = new System.Drawing.Size(83, 25); - this.NumWebViewWidth.Border3DStyle = System.Windows.Forms.Border3DStyle.Flat; - this.NumWebViewWidth.BorderColor = System.Drawing.SystemColors.WindowFrame; - this.NumWebViewWidth.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.NumWebViewWidth.Enabled = false; - this.NumWebViewWidth.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.NumWebViewWidth.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.NumWebViewWidth.Location = new System.Drawing.Point(87, 348); - this.NumWebViewWidth.Maximum = new decimal(new int[] { - 65535, - 0, - 0, - 0}); - this.NumWebViewWidth.MaxLength = 10; - this.NumWebViewWidth.MetroColor = System.Drawing.SystemColors.WindowFrame; - this.NumWebViewWidth.Name = "NumWebViewWidth"; - this.NumWebViewWidth.Size = new System.Drawing.Size(83, 25); - this.NumWebViewWidth.TabIndex = 3; - this.NumWebViewWidth.ThemeName = "Metro"; - this.NumWebViewWidth.Value = new decimal(new int[] { - 700, - 0, - 0, - 0}); - this.NumWebViewWidth.VisualStyle = Syncfusion.Windows.Forms.VisualStyle.Metro; + NumWebViewWidth.AccessibleDescription = "The width of the webview. Only accepts numeric values."; + NumWebViewWidth.AccessibleName = "Width"; + NumWebViewWidth.AccessibleRole = AccessibleRole.Text; + NumWebViewWidth.BackColor = Color.FromArgb(63, 63, 70); + NumWebViewWidth.BeforeTouchSize = new Size(83, 25); + NumWebViewWidth.Border3DStyle = Border3DStyle.Flat; + NumWebViewWidth.BorderColor = SystemColors.WindowFrame; + NumWebViewWidth.BorderStyle = BorderStyle.FixedSingle; + NumWebViewWidth.Enabled = false; + NumWebViewWidth.Font = new Font("Segoe UI", 10F); + NumWebViewWidth.ForeColor = Color.FromArgb(241, 241, 241); + NumWebViewWidth.Location = new Point(87, 348); + NumWebViewWidth.Maximum = new decimal(new int[] { 65535, 0, 0, 0 }); + NumWebViewWidth.MaxLength = 10; + NumWebViewWidth.MetroColor = SystemColors.WindowFrame; + NumWebViewWidth.Name = "NumWebViewWidth"; + NumWebViewWidth.Size = new Size(83, 25); + NumWebViewWidth.TabIndex = 3; + NumWebViewWidth.ThemeName = "Metro"; + NumWebViewWidth.Value = new decimal(new int[] { 700, 0, 0, 0 }); + NumWebViewWidth.VisualStyle = Syncfusion.Windows.Forms.VisualStyle.Metro; // // NumWebViewHeight // - this.NumWebViewHeight.AccessibleDescription = "The height of the webview. Only accepts numeric values."; - this.NumWebViewHeight.AccessibleName = "Height"; - this.NumWebViewHeight.AccessibleRole = System.Windows.Forms.AccessibleRole.Text; - this.NumWebViewHeight.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.NumWebViewHeight.BeforeTouchSize = new System.Drawing.Size(83, 25); - this.NumWebViewHeight.Border3DStyle = System.Windows.Forms.Border3DStyle.Flat; - this.NumWebViewHeight.BorderColor = System.Drawing.SystemColors.WindowFrame; - this.NumWebViewHeight.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; - this.NumWebViewHeight.Enabled = false; - this.NumWebViewHeight.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.NumWebViewHeight.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.NumWebViewHeight.Location = new System.Drawing.Point(218, 348); - this.NumWebViewHeight.Maximum = new decimal(new int[] { - 65535, - 0, - 0, - 0}); - this.NumWebViewHeight.MaxLength = 10; - this.NumWebViewHeight.MetroColor = System.Drawing.SystemColors.WindowFrame; - this.NumWebViewHeight.Name = "NumWebViewHeight"; - this.NumWebViewHeight.Size = new System.Drawing.Size(83, 25); - this.NumWebViewHeight.TabIndex = 4; - this.NumWebViewHeight.ThemeName = "Metro"; - this.NumWebViewHeight.Value = new decimal(new int[] { - 560, - 0, - 0, - 0}); - this.NumWebViewHeight.VisualStyle = Syncfusion.Windows.Forms.VisualStyle.Metro; + NumWebViewHeight.AccessibleDescription = "The height of the webview. Only accepts numeric values."; + NumWebViewHeight.AccessibleName = "Height"; + NumWebViewHeight.AccessibleRole = AccessibleRole.Text; + NumWebViewHeight.BackColor = Color.FromArgb(63, 63, 70); + NumWebViewHeight.BeforeTouchSize = new Size(83, 25); + NumWebViewHeight.Border3DStyle = Border3DStyle.Flat; + NumWebViewHeight.BorderColor = SystemColors.WindowFrame; + NumWebViewHeight.BorderStyle = BorderStyle.FixedSingle; + NumWebViewHeight.Enabled = false; + NumWebViewHeight.Font = new Font("Segoe UI", 10F); + NumWebViewHeight.ForeColor = Color.FromArgb(241, 241, 241); + NumWebViewHeight.Location = new Point(218, 348); + NumWebViewHeight.Maximum = new decimal(new int[] { 65535, 0, 0, 0 }); + NumWebViewHeight.MaxLength = 10; + NumWebViewHeight.MetroColor = SystemColors.WindowFrame; + NumWebViewHeight.Name = "NumWebViewHeight"; + NumWebViewHeight.Size = new Size(83, 25); + NumWebViewHeight.TabIndex = 4; + NumWebViewHeight.ThemeName = "Metro"; + NumWebViewHeight.Value = new decimal(new int[] { 560, 0, 0, 0 }); + NumWebViewHeight.VisualStyle = Syncfusion.Windows.Forms.VisualStyle.Metro; // // BtnWebViewReset // - this.BtnWebViewReset.AccessibleDescription = "Resets the width and height values to their defaults."; - this.BtnWebViewReset.AccessibleName = "Reset webview"; - this.BtnWebViewReset.AccessibleRole = System.Windows.Forms.AccessibleRole.PushButton; - this.BtnWebViewReset.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnWebViewReset.Enabled = false; - this.BtnWebViewReset.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.BtnWebViewReset.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnWebViewReset.ImageSize = new System.Drawing.Size(24, 24); - this.BtnWebViewReset.Location = new System.Drawing.Point(317, 348); - this.BtnWebViewReset.Name = "BtnWebViewReset"; - this.BtnWebViewReset.Size = new System.Drawing.Size(51, 26); - this.BtnWebViewReset.Style.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnWebViewReset.Style.FocusedBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnWebViewReset.Style.FocusedForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnWebViewReset.Style.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnWebViewReset.Style.HoverBackColor = System.Drawing.Color.FromArgb(((int)(((byte)(63)))), ((int)(((byte)(63)))), ((int)(((byte)(70))))); - this.BtnWebViewReset.Style.HoverForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.BtnWebViewReset.Style.Image = global::HASS.Agent.Properties.Resources.reset_24; - this.BtnWebViewReset.Style.PressedForeColor = System.Drawing.Color.Black; - this.BtnWebViewReset.TabIndex = 5; - this.BtnWebViewReset.TextImageRelation = System.Windows.Forms.TextImageRelation.Overlay; - this.BtnWebViewReset.UseVisualStyleBackColor = false; - this.BtnWebViewReset.Click += new System.EventHandler(this.BtnWebViewReset_Click); + BtnWebViewReset.AccessibleDescription = "Resets the width and height values to their defaults."; + BtnWebViewReset.AccessibleName = "Reset webview"; + BtnWebViewReset.AccessibleRole = AccessibleRole.PushButton; + BtnWebViewReset.BackColor = Color.FromArgb(63, 63, 70); + BtnWebViewReset.Enabled = false; + BtnWebViewReset.Font = new Font("Segoe UI", 10F); + BtnWebViewReset.ForeColor = Color.FromArgb(241, 241, 241); + BtnWebViewReset.ImageSize = new Size(24, 24); + BtnWebViewReset.Location = new Point(317, 348); + BtnWebViewReset.Name = "BtnWebViewReset"; + BtnWebViewReset.Size = new Size(51, 26); + BtnWebViewReset.Style.BackColor = Color.FromArgb(63, 63, 70); + BtnWebViewReset.Style.FocusedBackColor = Color.FromArgb(63, 63, 70); + BtnWebViewReset.Style.FocusedForeColor = Color.FromArgb(241, 241, 241); + BtnWebViewReset.Style.ForeColor = Color.FromArgb(241, 241, 241); + BtnWebViewReset.Style.HoverBackColor = Color.FromArgb(63, 63, 70); + BtnWebViewReset.Style.HoverForeColor = Color.FromArgb(241, 241, 241); + BtnWebViewReset.Style.Image = Properties.Resources.reset_24; + BtnWebViewReset.Style.PressedForeColor = Color.Black; + BtnWebViewReset.TabIndex = 5; + BtnWebViewReset.TextImageRelation = TextImageRelation.Overlay; + BtnWebViewReset.UseVisualStyleBackColor = false; + BtnWebViewReset.Click += BtnWebViewReset_Click; // // CbWebViewKeepLoaded // - this.CbWebViewKeepLoaded.AccessibleDescription = "Keeps the webview loaded in the background, resulting in faster loading when invo" + - "ked."; - this.CbWebViewKeepLoaded.AccessibleName = "Background loading"; - this.CbWebViewKeepLoaded.AccessibleRole = System.Windows.Forms.AccessibleRole.CheckButton; - this.CbWebViewKeepLoaded.AutoSize = true; - this.CbWebViewKeepLoaded.Checked = true; - this.CbWebViewKeepLoaded.CheckState = System.Windows.Forms.CheckState.Checked; - this.CbWebViewKeepLoaded.Enabled = false; - this.CbWebViewKeepLoaded.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.CbWebViewKeepLoaded.Location = new System.Drawing.Point(90, 405); - this.CbWebViewKeepLoaded.Name = "CbWebViewKeepLoaded"; - this.CbWebViewKeepLoaded.Size = new System.Drawing.Size(252, 23); - this.CbWebViewKeepLoaded.TabIndex = 7; - this.CbWebViewKeepLoaded.Text = global::HASS.Agent.Resources.Localization.Languages.ConfigTrayIcon_CbWebViewKeepLoaded; - this.CbWebViewKeepLoaded.UseVisualStyleBackColor = true; + CbWebViewKeepLoaded.AccessibleDescription = "Keeps the webview loaded in the background, resulting in faster loading when invoked."; + CbWebViewKeepLoaded.AccessibleName = "Background loading"; + CbWebViewKeepLoaded.AccessibleRole = AccessibleRole.CheckButton; + CbWebViewKeepLoaded.AutoSize = true; + CbWebViewKeepLoaded.Checked = true; + CbWebViewKeepLoaded.CheckState = CheckState.Checked; + CbWebViewKeepLoaded.Enabled = false; + CbWebViewKeepLoaded.Font = new Font("Segoe UI", 10F); + CbWebViewKeepLoaded.Location = new Point(90, 405); + CbWebViewKeepLoaded.Name = "CbWebViewKeepLoaded"; + CbWebViewKeepLoaded.Size = new Size(253, 23); + CbWebViewKeepLoaded.TabIndex = 7; + CbWebViewKeepLoaded.Text = Languages.ConfigTrayIcon_CbWebViewKeepLoaded; + CbWebViewKeepLoaded.UseVisualStyleBackColor = true; // // LblInfo2 // - this.LblInfo2.AccessibleDescription = "Background loading information."; - this.LblInfo2.AccessibleName = "Background loading info"; - this.LblInfo2.AccessibleRole = System.Windows.Forms.AccessibleRole.StaticText; - this.LblInfo2.AutoSize = true; - this.LblInfo2.Enabled = false; - this.LblInfo2.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.LblInfo2.Location = new System.Drawing.Point(107, 435); - this.LblInfo2.Name = "LblInfo2"; - this.LblInfo2.Size = new System.Drawing.Size(320, 19); - this.LblInfo2.TabIndex = 76; - this.LblInfo2.Text = Languages.ConfigTrayIcon_LblInfo2; + LblInfo2.AccessibleDescription = "Background loading information."; + LblInfo2.AccessibleName = "Background loading info"; + LblInfo2.AccessibleRole = AccessibleRole.StaticText; + LblInfo2.AutoSize = true; + LblInfo2.Enabled = false; + LblInfo2.Font = new Font("Segoe UI", 10F); + LblInfo2.Location = new Point(107, 435); + LblInfo2.Name = "LblInfo2"; + LblInfo2.Size = new Size(330, 19); + LblInfo2.TabIndex = 76; + LblInfo2.Text = "(This uses extra resources, but reduces loading time.)"; // // CbWebViewShowMenuOnLeftClick // - this.CbWebViewShowMenuOnLeftClick.AccessibleDescription = "If enabled, left clicking the system tray icon will show the default menu."; - this.CbWebViewShowMenuOnLeftClick.AccessibleName = "Show default menu on left click"; - this.CbWebViewShowMenuOnLeftClick.AccessibleRole = System.Windows.Forms.AccessibleRole.CheckButton; - this.CbWebViewShowMenuOnLeftClick.AutoSize = true; - this.CbWebViewShowMenuOnLeftClick.Enabled = false; - this.CbWebViewShowMenuOnLeftClick.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); - this.CbWebViewShowMenuOnLeftClick.Location = new System.Drawing.Point(90, 487); - this.CbWebViewShowMenuOnLeftClick.Name = "CbWebViewShowMenuOnLeftClick"; - this.CbWebViewShowMenuOnLeftClick.Size = new System.Drawing.Size(304, 23); - this.CbWebViewShowMenuOnLeftClick.TabIndex = 77; - this.CbWebViewShowMenuOnLeftClick.Text = Languages.ConfigTrayIcon_CbWebViewShowMenuOnLeftClick; - this.CbWebViewShowMenuOnLeftClick.UseVisualStyleBackColor = true; + CbWebViewShowMenuOnLeftClick.AccessibleDescription = "If enabled, left clicking the system tray icon will show the default menu."; + CbWebViewShowMenuOnLeftClick.AccessibleName = "Show default menu on left click"; + CbWebViewShowMenuOnLeftClick.AccessibleRole = AccessibleRole.CheckButton; + CbWebViewShowMenuOnLeftClick.AutoSize = true; + CbWebViewShowMenuOnLeftClick.Enabled = false; + CbWebViewShowMenuOnLeftClick.Font = new Font("Segoe UI", 10F); + CbWebViewShowMenuOnLeftClick.Location = new Point(90, 487); + CbWebViewShowMenuOnLeftClick.Name = "CbWebViewShowMenuOnLeftClick"; + CbWebViewShowMenuOnLeftClick.Size = new Size(265, 23); + CbWebViewShowMenuOnLeftClick.TabIndex = 77; + CbWebViewShowMenuOnLeftClick.Text = Languages.ConfigTrayIcon_CbWebViewShowMenuOnLeftClick; + CbWebViewShowMenuOnLeftClick.UseVisualStyleBackColor = true; // // ConfigTrayIcon // - this.AccessibleDescription = "Panel containing the tray icon configuration."; - this.AccessibleName = "Tray icon"; - this.AccessibleRole = System.Windows.Forms.AccessibleRole.Pane; - this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(45)))), ((int)(((byte)(45)))), ((int)(((byte)(48))))); - this.Controls.Add(this.CbWebViewShowMenuOnLeftClick); - this.Controls.Add(this.LblInfo2); - this.Controls.Add(this.CbWebViewKeepLoaded); - this.Controls.Add(this.BtnWebViewReset); - this.Controls.Add(this.NumWebViewHeight); - this.Controls.Add(this.NumWebViewWidth); - this.Controls.Add(this.BtnShowWebViewPreview); - this.Controls.Add(this.LblX); - this.Controls.Add(this.LblWebViewSize); - this.Controls.Add(this.TbWebViewUrl); - this.Controls.Add(this.LblWebViewUrl); - this.Controls.Add(this.CbShowWebView); - this.Controls.Add(this.LblInfo1); - this.Controls.Add(this.CbDefaultMenu); - this.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(241)))), ((int)(((byte)(241)))), ((int)(((byte)(241))))); - this.Margin = new System.Windows.Forms.Padding(4); - this.Name = "ConfigTrayIcon"; - this.Size = new System.Drawing.Size(700, 544); - this.Load += new System.EventHandler(this.ConfigTrayIcon_Load); - ((System.ComponentModel.ISupportInitialize)(this.NumWebViewWidth)).EndInit(); - ((System.ComponentModel.ISupportInitialize)(this.NumWebViewHeight)).EndInit(); - this.ResumeLayout(false); - this.PerformLayout(); - + AccessibleDescription = "Panel containing the tray icon configuration."; + AccessibleName = "Tray icon"; + AccessibleRole = AccessibleRole.Pane; + AutoScaleDimensions = new SizeF(96F, 96F); + AutoScaleMode = AutoScaleMode.Dpi; + BackColor = Color.FromArgb(45, 45, 48); + Controls.Add(CbWebViewShowMenuOnLeftClick); + Controls.Add(LblInfo2); + Controls.Add(CbWebViewKeepLoaded); + Controls.Add(BtnWebViewReset); + Controls.Add(NumWebViewHeight); + Controls.Add(NumWebViewWidth); + Controls.Add(BtnShowWebViewPreview); + Controls.Add(LblX); + Controls.Add(LblWebViewSize); + Controls.Add(TbWebViewUrl); + Controls.Add(LblWebViewUrl); + Controls.Add(CbShowWebView); + Controls.Add(LblInfo1); + Controls.Add(CbDefaultMenu); + Controls.Add(CbUseModernIcon); + ForeColor = Color.FromArgb(241, 241, 241); + Margin = new Padding(4); + Name = "ConfigTrayIcon"; + Size = new Size(700, 544); + Load += ConfigTrayIcon_Load; + ((System.ComponentModel.ISupportInitialize)NumWebViewWidth).EndInit(); + ((System.ComponentModel.ISupportInitialize)NumWebViewHeight).EndInit(); + ResumeLayout(false); + PerformLayout(); } #endregion private System.Windows.Forms.Label LblInfo1; + internal System.Windows.Forms.CheckBox CbUseModernIcon; internal System.Windows.Forms.CheckBox CbDefaultMenu; internal CheckBox CbShowWebView; internal TextBox TbWebViewUrl; diff --git a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.resx b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.resx index f298a7be..af32865e 100644 --- a/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.resx +++ b/src/HASS.Agent/HASS.Agent/Controls/Configuration/ConfigTrayIcon.resx @@ -1,4 +1,64 @@ - + + + diff --git a/src/HASS.Agent/HASS.Agent/Forms/Commands/CommandsMod.cs b/src/HASS.Agent/HASS.Agent/Forms/Commands/CommandsMod.cs index 525bcc87..983b41b8 100644 --- a/src/HASS.Agent/HASS.Agent/Forms/Commands/CommandsMod.cs +++ b/src/HASS.Agent/HASS.Agent/Forms/Commands/CommandsMod.cs @@ -206,6 +206,7 @@ private void LoadCommand() case CommandType.SetVolumeCommand: case CommandType.SetApplicationVolumeCommand: case CommandType.SetAudioOutputCommand: + case CommandType.SetAudioInputCommand: TbSetting.Text = Command.Command; break; } @@ -457,6 +458,7 @@ private void BtnStore_Click(object sender, EventArgs e) break; case CommandType.SetAudioOutputCommand: + case CommandType.SetAudioInputCommand: var audioDeviceName = TbSetting.Text.Trim(); if (string.IsNullOrEmpty(audioDeviceName)) { @@ -630,6 +632,7 @@ private bool SetType(bool setDefaultValues = true) break; case CommandType.SetAudioOutputCommand: + case CommandType.SetAudioInputCommand: SetAudioOutputUi(); break; diff --git a/src/HASS.Agent/HASS.Agent/Forms/Configuration.cs b/src/HASS.Agent/HASS.Agent/Forms/Configuration.cs index dedf1cf2..6c25679f 100644 --- a/src/HASS.Agent/HASS.Agent/Forms/Configuration.cs +++ b/src/HASS.Agent/HASS.Agent/Forms/Configuration.cs @@ -323,6 +323,7 @@ private void LoadSettings() _mqtt.TbMqttClientCertificate.Text = Variables.AppSettings.MqttClientCertificate; _mqtt.CbAllowUntrustedCertificates.CheckState = Variables.AppSettings.MqttAllowUntrustedCertificates ? CheckState.Checked : CheckState.Unchecked; _mqtt.CbUseRetainFlag.CheckState = Variables.AppSettings.MqttUseRetainFlag ? CheckState.Checked : CheckState.Unchecked; + _mqtt.CbIgnoreGracePeriod.CheckState = Variables.AppSettings.MqttIgnoreGracePeriod ? CheckState.Checked : CheckState.Unchecked; // updates _updates.CbUpdates.CheckState = Variables.AppSettings.CheckForUpdates ? CheckState.Checked : CheckState.Unchecked; @@ -351,6 +352,7 @@ private void LoadSettings() _mediaPlayer.CbEnableMediaPlayer.CheckState = Variables.AppSettings.MediaPlayerEnabled ? CheckState.Checked : CheckState.Unchecked; // tray icon + _trayIcon.CbUseModernIcon.CheckState = Variables.AppSettings.TrayIconUseModern ? CheckState.Checked : CheckState.Unchecked; _trayIcon.CbDefaultMenu.CheckState = Variables.AppSettings.TrayIconShowDefaultMenu ? CheckState.Checked : CheckState.Unchecked; _trayIcon.CbShowWebView.CheckState = Variables.AppSettings.TrayIconShowWebView ? CheckState.Checked : CheckState.Unchecked; _trayIcon.NumWebViewWidth.Value = Variables.AppSettings.TrayIconWebViewWidth; @@ -425,6 +427,7 @@ private async Task StoreSettingsAsync() Variables.AppSettings.MqttClientCertificate = _mqtt.TbMqttClientCertificate.Text; Variables.AppSettings.MqttAllowUntrustedCertificates = _mqtt.CbAllowUntrustedCertificates.CheckState == CheckState.Checked; Variables.AppSettings.MqttUseRetainFlag = _mqtt.CbUseRetainFlag.CheckState == CheckState.Checked; + Variables.AppSettings.MqttIgnoreGracePeriod = _mqtt.CbIgnoreGracePeriod.CheckState == CheckState.Checked; // mqtt -> service await SettingsManager.SendMqttSettingsToServiceAsync(); @@ -456,6 +459,7 @@ private async Task StoreSettingsAsync() Variables.AppSettings.MediaPlayerEnabled = _mediaPlayer.CbEnableMediaPlayer.CheckState == CheckState.Checked; // tray icon + Variables.AppSettings.TrayIconUseModern = _trayIcon.CbUseModernIcon.CheckState == CheckState.Checked; Variables.AppSettings.TrayIconShowDefaultMenu = _trayIcon.CbDefaultMenu.CheckState == CheckState.Checked; Variables.AppSettings.TrayIconShowWebView = _trayIcon.CbShowWebView.CheckState == CheckState.Checked; Variables.AppSettings.TrayIconWebViewWidth = (int)_trayIcon.NumWebViewWidth.Value; diff --git a/src/HASS.Agent/HASS.Agent/Forms/Main.cs b/src/HASS.Agent/HASS.Agent/Forms/Main.cs index 3eb10a0f..7e971535 100644 --- a/src/HASS.Agent/HASS.Agent/Forms/Main.cs +++ b/src/HASS.Agent/HASS.Agent/Forms/Main.cs @@ -89,11 +89,11 @@ private async void Main_Load(object sender, EventArgs e) // core components initialization - required for loading the entities await RadioManager.Initialize(); await InternalDeviceSensorsManager.Initialize(); - InitializeHardwareManager(); - InitializeVirtualDesktopManager(); + InitializeHardwareManager(); + InitializeVirtualDesktopManager(); - // load entities - var loaded = await SettingsManager.LoadEntitiesAsync(); + // load entities + var loaded = await SettingsManager.LoadEntitiesAsync(); if (!loaded) { MessageBoxAdv.Show(this, Languages.Main_Load_MessageBox1, Variables.MessageBoxTitle, MessageBoxButtons.OK, MessageBoxIcon.Error); @@ -245,8 +245,15 @@ private void CheckDpiScalingFactor() MessageBoxAdv.Show(this, Languages.Main_CheckDpiScalingFactor_MessageBox1, Variables.MessageBoxTitle, MessageBoxButtons.OK, MessageBoxIcon.Exclamation); } - private static void ProcessTrayIcon() + private void ProcessTrayIcon() { + if (Variables.AppSettings.TrayIconUseModern) + { + var icon = (Icon)new System.Resources.ResourceManager(typeof(Main)).GetObject("ModernNotifyIcon"); + if (icon != null) + NotifyIcon.Icon = icon; + } + // are we set to show the webview and keep it loaded? if (!Variables.AppSettings.TrayIconShowWebView) return; diff --git a/src/HASS.Agent/HASS.Agent/Forms/Main.resx b/src/HASS.Agent/HASS.Agent/Forms/Main.resx index 3676f237..df700854 100644 --- a/src/HASS.Agent/HASS.Agent/Forms/Main.resx +++ b/src/HASS.Agent/HASS.Agent/Forms/Main.resx @@ -1,4 +1,64 @@ - + + + @@ -57,17 +117,17 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + + 17, 17 - - + + 125, 17 - - + iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAnRJREFUWEftlt+HVVEUx/vXIiIi5ikiIiKGISKip+glIiIiehoieomYmhpNv9TU + wAAADsABataJCQAAAnRJREFUWEftlt+HVVEUx/vXIiIi5ikiIiKGISKip+glIiIiehoieomYmhpNv9TU ZDI1PzI1mn7o53TPvefcc85qPvtY2nvffebuY+41ZBZf99599t7rs9daZ6+7a/dES7ZTOwD/D8C+yZbc +tiVuV+FjD5rB+eENBAAnM/+KCQvRdbapSS5RENsGcB2fuZVR0amEllpFQbi2JP+EFsC2HvXda7jQKx3 S5n6kjvzQ2oMwIn33Km+n51LBTv3OnXmXH/fNeMX3rjjITUCYMO0EPmWlibHhx+2zemJAmDMUed8+utD @@ -84,7 +144,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAiNJREFUWEftlc1L3FAUxfuvCV0VBKFQKLgtFISuCkKhK/eCIBQEQSh0JbgpCIJi + wAAADsABataJCQAAAiNJREFUWEftlc1L3FAUxfuvCV0VBKFQKLgtFISuCkKhK/eCIBQEQSh0JbgpCIJi nSqoFT+oYLVa60et0qlfnSSTSebW35tcTKZp5mWy0MUcOMy8l5d3z7vv3JsHXdMVuUt2BHQE3H8BL1c9 2bwMZfY0SH1elJkCni26EtRFJn/UpH/Nk545J3VdEWYKGNvzZfV3IH3Lrpy4dRn56ieeP5xJrm+HLTPw YsWVxV+BPF9yE88elxwjjuzE5/PSyoQI4ffpvCPv9n15te7JmXdzNxGGt6v/vGNLKwHKNzt+FDIJfNKc @@ -99,7 +159,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAnlJREFUWEftlu1LFUEUxv3XAiEIAkEQhCAIAiEQBEEQgsBPgV+CIAiCIAgEQYhA + wAAADsABataJCQAAAnlJREFUWEftlu1LFUEUxv3XAiEIAkEQhCAIAiEQBEEQgsBPgV+CIAiCIAgEQYhA CDQrM+3N0lDKtCxT8jWQfd97T/1m73inZXbv3BvRh+4DD3d3Z2bnmXOec/Z2nJry5F+yLaAtwFnA1ZVI wJZftY63ylIBV5ZDub0Rq2t+wU6QCTj/LJAHO4mcmbGvdWWhgL7ngaRVtacs7KdynGQ3PJvbS8VP1a3M fE+t611ZKKBz2pOXB7VdCoCY/leBdb0rG6ZAY2wzlp5ZX0behSoNYOmoYl3XDK0Cbn6Mldl0mPMbaUOC @@ -116,7 +176,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAiNJREFUWEftlc1L3FAUxfuvCV0VBKFQKLgtFISuCkKhK/eCIBQEQSh0JbgpCIJi + wAAADsABataJCQAAAiNJREFUWEftlc1L3FAUxfuvCV0VBKFQKLgtFISuCkKhK/eCIBQEQSh0JbgpCIJi nSqoFT+oYLVa60et0qlfnSSTSebW35tcTKZp5mWy0MUcOMy8l5d3z7vv3JsHXdMVuUt2BHQE3H8BL1c9 2bwMZfY0SH1elJkCni26EtRFJn/UpH/Nk545J3VdEWYKGNvzZfV3IH3Lrpy4dRn56ieeP5xJrm+HLTPw YsWVxV+BPF9yE88elxwjjuzE5/PSyoQI4ffpvCPv9n15te7JmXdzNxGGt6v/vGNLKwHKNzt+FDIJfNKc @@ -131,7 +191,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAe9JREFUWEftlttHRFEUxvvXIiIiIiIiIiIiIiIiIhE9RfTUU0QiopruN92bmm6j + wAAADsABataJCQAAAe9JREFUWEftlttHRFEUxvvXIiIiIiIiIiIiIiIiIhE9RfTUU0QiopruN92bmm6j y9Qo0mU6tzkzs/PtzjTn7LP2zJ4xGTEf38vZa9b57b3WrH0qKhc0VkqXAcoA/w+gY8/gFp/Xreisddv/ PJeVAZA89Jlk0KuV4i90r0/exvlaRE+x3mPTs5bNSgB9JyZLpHj+Xx2/JVn7rsEa13U2dh1nesJZcDR+ EydziVYCGAhaTlp1TYSLCFC1qDHr5/SVVbvsLZHMygDvcaEGOYTSULlEKwGgqUTNPdm8NOiPqXvbd0LD @@ -145,7 +205,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAwJJREFUWEftlttrE1EQxv3XBEEQBEHwqVAoCIIgCAXBJ0EoCH0RBEEQBF8FwRdB + wAAADsABataJCQAAAwJJREFUWEftlttrE1EQxv3XBEEQBEHwqVAoCIIgCAXBJ0EoCH0RBEEQBF8FwRdB UKsttbZqvdW7FVQsrdRbm2Q3u8m0v92OTs6Z3cQHKUIHPpKdnD3zzTcz52TX7psN2U7sEPi/COybaMjY Qio3ljJ5+r0jS61ugYWfHZlYzmX8ZSoH7jbdd6swEAECX1psS9qRHvvR7spq2t16Ki3ffLz6KRuYSF8C R2ZbspKUQSBw5WMmxx62onWH77fk8oe2rGXlWj5PPE6idSFqCZx+nvzOGpmH78WBQxyaasr017x4BzXO @@ -164,7 +224,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAkVJREFUWEftlt9LFUEUx/vXhCDwKQiCQOgpCIJAEIKeBJ+CEAKfAkEIAkEQfAkC + wAAADsABataJCQAAAkVJREFUWEftlt9LFUEUx/vXhCDwKQiCQOgpCIJAEIKeBJ+CEAKfAkEIAkEQfAkC S/wVZv7IpMwfpSVZlhbK7t79cXe6n7NMzO6Ou7qtSeCBL5d77pk7n5k5c85caBt11FnqHOD/Bbg66aon X0K15TTVykFT9a366uIze2yRKgEw+a8gVtjMj0ggsKc7oTW+SJUAWDl2Y8b743u8GYivc76Rii1TJYBt N1bT36OU78qEKwAP14OUv0yVANYOm7Ltpu/mS08AyAXTX6ZKAKwSe/QxUJfHXZn81V6k/FYqXJtyrWOO @@ -180,7 +240,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAbxJREFUWEftlu9HBEEYx/vXIiIiehUR/QHR24heRfQyIiJ6Fb2MiKur01X0Q5eK + wAAADsABataJCQAAAbxJREFUWEftlu9HBEEYx/vXIiIiehUR/QHR24heRfQyIiJ6Fb2MiKur01X0Q5eK ri6lurq6iuv25+w+9Z01uRuzu7PuUrTLhzMzZj7P7LPPcx2dmTr9JqlAKpAKJBbo3TRotuhQ5pFRseZR 2fRp74XR0o1DQztm09qu9ToNbBtNYzLaAths+tymqu1T4c2juUuHxgsWjR5afHyt7BLzicv054JDcTge ea9GtAVWH1we7dixpZwHODhXYVSxfH4bbRNYvHbozvC/I4tj+TaQHd41WxcYOTDp3QkiUs2HcfTKKP/M @@ -221,7 +281,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAXxJREFUWEftlUFLw0AQhf1rgidPglevnvwHXr2K4NWrIAiC4MWTUiwUigdRClUQ + wAAADsABataJCQAAAXxJREFUWEftlUFLw0AQhf1rgidPglevnvwHXr2K4NWrIAiC4MWTUiwUigdRClUQ tVoUq6igSdMkzdiXbMgmHc10Gy2FPHhQkunMt5Od3ZnZI4sm6RKgBBgZYLnepb17j2ovPrXtgFpWP/yN Z3jH/ec3iwEWqzZVOz7l6fDRC2O5HJxFAHPHFl2891WJfNVffTYPZxHAzp2rUif69AI6aHth6/EZstq8 6rG5shYBnL2lW//UDWjhJGnzfMWim680BDqm5/jJIoAPN1BpI21du0MxG5c99TaSPWDOxnAWb8I8r547 @@ -233,7 +293,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAJtJREFUWEdjcNz79f9A4lEHjDpg6Dog5ti3/yff/Pl//t2f/6knv2NVQwwm2wFr + wAAADsABataJCQAAAJtJREFUWEdjcNz79f9A4lEHjDpg6Dog5ti3/yff/Pl//t2f/6knv2NVQwwm2wFr Hv3+DwN7X/zBqoYYTLYDJt78CbX+//9lD35hVUMMJtsB7vu//m++8uN/57WfYDY2NcTgoZsIqYXJdgAo 4T3//g+MByQRgrIfDIDY2NQQg0cdMOqAUQeMOmDUAaMOINsB1MKjDhh1wAA74Ot/AF92BBjcG70TAAAA AElFTkSuQmCC @@ -242,7 +302,7 @@ iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAg5JREFUWEftls8rBVEUx/1ryspKKStlpaxsla2yslRWSikrZakUCT1RChGR3+Vn + wAAADsABataJCQAAAg5JREFUWEftls8rBVEUx/1ryspKKStlpaxsla2yslRWSikrZakUCT1RChGR3+Vn Epr33ng/Dh+6GveeMzPPj6R867uZuXfO55577rnT1DwbyW/6H+BvAbTORzK4HcvMRUU272pyUaq/evu+ JvNXVRnaiaVtoajOtZwLgMBjh08S1yRT1brI1FklN0gmQM9qSa7LL19tUI+VuvStl9VvJp0KMLBVzrVq S2RjeC9Wv+1sArDyrwRPqn/DzoQKwJ5/Ju2WilWRjiW9JlQACs4SFX8Sham5jetSuHmJZGj6vBLEwQEA @@ -283,7 +343,7 @@ iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAmFJREFUSEutlStwU1EQhiMQCAQCgUAgEBHIioqKCiQCUYmojEAgEBERnYlAIBCR + wAAADsABataJCQAAAmFJREFUSEutlStwU1EQhiMQCAQCgUAgEBHIioqKCiQCUYmojEAgEBERnYlAIBCR yMqKiIoKRERF3hNBZioqIiIqKioiKiLa7zvsCW24hEC6M/+ce/bx7949r9K6MhgMdvr9/jHjiPHzeDx+ EqbNBdJtMAcXoAVuTBbmzQWyb2CWqyZB3SSdTudFcthUIDsC01ar9cg5ySom6PV6r5PDpgLhnoSMTXAA Lpn/CPPDCIRViGeRqM1YDtP6QmANXEmyhDn64263uzUajZ6G+78JJPtBdgiZbVgAXZ3xHPgHOxGyvrgb @@ -299,7 +359,7 @@ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAA9xJREFUaEPVmi1Q3FAUhREIBAKBqKhAICoQCAQCgahAIBAVCAQCgahAICoQzCAq + wAAADsABataJCQAAA9xJREFUaEPVmi1Q3FAUhREIBAKBqKhAICoQCAQCgahAIBAVCAQCgahAICoQzCAq KhAIRAUCgahAIBAIBKKiAoGoQCAQCAQCgWBm6Xf2HXZnySZ5+WGy/WbuJPvevSd33ybvLztUF61WaxZb wbawn9gZdo3dYH+xc+wI23x5efmMjTh0MCCxe5KKBv9FhzYP+YyHtOLhC2w4vHlIZtZ5RUPMvsObh2TW nFc0xJw7vHlI5ofzioaYO4c3B3mMkMgm9hDSKgZxB9ik5eoF/TnETzguYcMu7kDdMnatRKqAxjN2wOkH @@ -322,7 +382,7 @@ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAABTdJREFUaEPVmS1QHTEQxxEIBKICUVGBqKhAIBAVFQgEEoFAIBAViIoKZAUzFQgE + wAAADsABataJCQAABTdJREFUaEPVmS1QHTEQxxEIBKICUVGBqKhAIBAVFQgEEoFAIBAViIoKZAUzFQgE ogKBQCAQCERFBQKBqKioQCAQiIqKioqKis7Q/v65vZBccveO93Hv9T+zk2R3k93cy8du3tRjcX9/f/k3 AO0/Jvo/gMO35nuIGRNPDnD0DY7NWtNDX7zwOcK8iT3q+ncCjB/IK8rPFE+NPaW6+FWgt2Qq0pmmfWb8 K4puJ1E6X4L2DfSc6gy065gVIN+neALNUv/omAba3U0CY/rZE8D/Af20ZhbIf0O5/SHZmZkYLbD1FGPf @@ -350,7 +410,7 @@ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAABH1JREFUaEPNmS9Q3UAQhxEIBAKBQFQgEAgEAoFAIBAIBAKJQCAqKxAIBDMIBAJR + wAAADsABataJCQAABH1JREFUaEPNmS9Q3UAQhxEIBAKBQFQgEAgEAoFAIBAIBAKJQCAqKxAIBDMIBAJR gUBUICoQCAQCgahAVCAqKioQiIoKBKKCGej3u+w7Eu4uSSEvL9/MTpLdzd6+vNv7kwyleH5+Hn16etpC LpBfyB9k3czdhuSnlTTHAujmzaW7kOcEid5mKQeMm1t3IfkzS1ZPXN1mT0+ey0lz6S4kOpWlniXPYdpM riaQDWQXWTV1tyDpHZJzcL5launXkN9mkm3NTN2CxI4sR7EgHcdV9I9ScLxBviAf3A0VcMuonbaDkrNE @@ -375,7 +435,7 @@ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAAuVJREFUaEPVma9zE0EUxyMqEMiKigpERUUlojKiAlFRgUAgIpEV/QOYQVZUVCAQ + wAAADsABataJCQAAAuVJREFUaEPVma9zE0EUxyMqEMiKigpERUUlojKiAlFRgUAgIpEV/QOYQVZUVCAQ CGRFRAWiAhGBQCIQFREREUgkMwmf9/aba0M2vST3YzefmTd3+31vd793c7m92XSaYjqddieTySVxRwyJ AXGFfqaSPMHgc4x+4rgU8n0Ou+qSD5gy87/cZQnUjYl9dc2D/+887c/EKacviBPOr4m/ngTOv6prevDT DbYCmHur1Byk7LdRXAT0lEoLpi5lyO+85CjkP6jUam8kpwUjd/Jkpk4lRyF/pFKrvZecFoyM5MlMlf44 @@ -393,7 +453,7 @@ iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAABpVJREFUaEPFmi9QHTsUxhEIBKICUYGoQCAqEE9UVCAQCASiAlGBqKh4oqICUcEM + wAAADsABataJCQAABpVJREFUaEPFmi9QHTsUxhEIBKICUYGoQCAqEE9UVCAQCASiAlGBqKh4oqICUcEM ouIJBAKBQCAqKhAIBAKBqEAgKioQFYgnKioQCGagvy97NpvsbrJ7917e+2bObHLOl7PJ5t9J7p2aFB4e Hl49Pj5u8dxFviIXyE+Tb8gxsod8greMTFvR/wdUYIbKrCIHyA35kUCZX8gR8obsrLn9b8ALN4dUOgV8 /UY+kHzaXuEFy7zoyr01Azh3yDVybvIDuTVzElbmjb1ucsD3LI6/FK9pAtsNsktyBXlmxRrANou8hruD @@ -427,7 +487,7 @@ iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAO - wQAADsEBuJFr7QAAARtJREFUSEvdlK8SAVEUxjcIgiCKgigIHkUQBEEQPMQ2QfAIoiCIHkAQBA8gegRB + wAAADsABataJCQAAARtJREFUSEvdlK8SAVEUxjcIgiCKgigIHkUQBEEQPMQ2QfAIoiCIHkAQBA8gegRB MLPrd/d8u2OMsbhrBr+ZM+d+97vn8++u4P+J47gWRdGEWlF7lVtPqYaOvQfhISEn+l2cR411/DUYHCkn F84ONfYczJQYOtu4gV5TA5Z9+sZ2DfSRVtZ4Pgw0bdRA72kl2Qnsbc010G1Z+XC4wUyYFrorK4O9iQtO QfdkFQOBS2UnoDuy/CGvQmB2u1i736su2x8C5xZtoGey/CFsqtwEtLsAVdnvQ4i7urfvfEf5Pc0pd8JX @@ -437,28 +497,28 @@ - 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 + gut54Qr+Bs8w4136NFAbAAAAAElFTkSuQmCC @@ -487,4 +547,8 @@ NLGJMOvFeKEqY0ecbYgf8dDTDERBTzMQBX0DvaZvoNdMcQMi/wKHssFyJvHDuwAAAABJRU5ErkJggg== + + + ..\Resources\modern-tray-icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Forms/Sensors/SensorsMod.cs b/src/HASS.Agent/HASS.Agent/Forms/Sensors/SensorsMod.cs index ba5c019d..987cbad3 100644 --- a/src/HASS.Agent/HASS.Agent/Forms/Sensors/SensorsMod.cs +++ b/src/HASS.Agent/HASS.Agent/Forms/Sensors/SensorsMod.cs @@ -228,7 +228,11 @@ private void LoadSensor() CbApplyRounding.Checked = Sensor.ApplyRounding; NumRound.Text = Sensor.Round?.ToString() ?? LastActiveSensor.DefaultTimeWindow.ToString(); ; break; - } + + case SensorType.ScreenshotSensor: + TbSetting1.Text = Sensor.Query; + break; + } } /// @@ -327,6 +331,10 @@ private bool SetType(bool setDefaultValues = true) SetLastActiveGui(); break; + case SensorType.ScreenshotSensor: + SetScreenshotGui(); + break; + default: SetEmptyGui(); break; @@ -528,10 +536,24 @@ private void SetLastActiveGui() })); } - /// - /// Change the UI to a general type - /// - private void SetEmptyGui() + private void SetScreenshotGui() + { + Invoke(new MethodInvoker(delegate + { + SetEmptyGui(); + + LblSetting1.Text = Languages.SensorsMod_LblSetting1_ScreenNumber; + LblSetting1.Visible = true; + TbSetting1.Visible = true; + + BtnTest.Visible = false; + })); + } + + /// + /// Change the UI to a general type + /// + private void SetEmptyGui() { Invoke(new MethodInvoker(delegate { @@ -799,6 +821,17 @@ private void BtnStore_Click(object sender, EventArgs e) return; } break; + + case SensorType.ScreenshotSensor: + var screenIndex = TbSetting1.Text.Trim(); + if (string.IsNullOrEmpty(screenIndex)) + { + MessageBoxAdv.Show(this, Languages.SensorsMod_BtnStore_MessageBox10, Variables.MessageBoxTitle, MessageBoxButtons.OK, MessageBoxIcon.Error); + ActiveControl = TbSetting1; + return; + } + Sensor.Query = screenIndex; + break; } // set values diff --git a/src/HASS.Agent/HASS.Agent/HASS.Agent.csproj b/src/HASS.Agent/HASS.Agent/HASS.Agent.csproj index 04598222..d5bd7361 100644 --- a/src/HASS.Agent/HASS.Agent/HASS.Agent.csproj +++ b/src/HASS.Agent/HASS.Agent/HASS.Agent.csproj @@ -12,7 +12,7 @@ x64 x64;x86 full - 2.0.2-beta2 + 2.1.0-beta1 HASS.Agent Team HASS.Agent Team Windows-based client for Home Assistant. Provides notifications, quick actions, commands, sensors and more. @@ -22,8 +22,8 @@ https://github.com/hass-agent/HASS.Agent MIT app.manifest - 2.0.2 - 2.0.2 + 2.1.0 + 2.1.0 HASS.Agent None win10-x64;win10-x86 @@ -49,27 +49,27 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - + WV2 - - - + + + - + - + - + diff --git a/src/HASS.Agent/HASS.Agent/MQTT/MqttManager.cs b/src/HASS.Agent/HASS.Agent/MQTT/MqttManager.cs index ff56e52f..3e99155b 100644 --- a/src/HASS.Agent/HASS.Agent/MQTT/MqttManager.cs +++ b/src/HASS.Agent/HASS.Agent/MQTT/MqttManager.cs @@ -14,6 +14,7 @@ using HASS.Agent.Resources.Localization; using HASS.Agent.Settings; using HASS.Agent.Shared.Enums; +using HASS.Agent.Shared.Managers; using HASS.Agent.Shared.Models.HomeAssistant; using HASS.Agent.Shared.Mqtt; using MQTTnet; @@ -130,10 +131,14 @@ private async Task OnMqttDisconnected(MqttClientDisconnectedEventArgs arg) Variables.MainForm?.SetMqttStatus(ComponentStatus.Connecting); + var gracePeriod = Variables.AppSettings.DisconnectedGracePeriodSeconds; + // give the connection the grace period to recover var runningTimer = Stopwatch.StartNew(); - while (runningTimer.Elapsed.TotalSeconds < Variables.AppSettings.DisconnectedGracePeriodSeconds) + while (runningTimer.Elapsed.TotalSeconds < gracePeriod) { + await Task.Delay(TimeSpan.FromSeconds(5)); + if (IsConnected()) { _isReady = true; @@ -143,12 +148,20 @@ private async Task OnMqttDisconnected(MqttClientDisconnectedEventArgs arg) _status = MqttStatus.Connected; Variables.MainForm?.SetMqttStatus(ComponentStatus.Ok); - Log.Information("[MQTT] Connected"); + Log.Information("[MQTT] Reconnected from disconnection"); return; } - await Task.Delay(TimeSpan.FromSeconds(5)); + if (Variables.AppSettings.MqttIgnoreGracePeriod) + { + var lastResumed = SystemStateManager.LastEventOccurrence.TryGetValue(SystemStateEvent.Resume, out var lastResumeEventDate); + if (lastResumed && DateTime.Now < lastResumeEventDate.AddSeconds(gracePeriod)) + { + Log.Information("[MQTT] System resumed less than {gracePeriod} seconds ago, ignoring grace period on disconnection"); + break; + } + } } // nope, call it @@ -172,10 +185,14 @@ private async Task OnMqttConnectionFailed(ConnectingFailedEventArgs arg) { Variables.MainForm?.SetMqttStatus(ComponentStatus.Connecting); + var gracePeriod = Variables.AppSettings.DisconnectedGracePeriodSeconds; + // give the connection the grace period to recover var runningTimer = Stopwatch.StartNew(); - while (runningTimer.Elapsed.TotalSeconds < Variables.AppSettings.DisconnectedGracePeriodSeconds) + while (runningTimer.Elapsed.TotalSeconds < gracePeriod) { + await Task.Delay(TimeSpan.FromSeconds(5)); + if (IsConnected()) { // recovered @@ -184,12 +201,20 @@ private async Task OnMqttConnectionFailed(ConnectingFailedEventArgs arg) _status = MqttStatus.Connected; Variables.MainForm?.SetMqttStatus(ComponentStatus.Ok); - Log.Information("[MQTT] Connected"); + Log.Information("[MQTT] Reconnected from failed connection"); return; } - await Task.Delay(TimeSpan.FromSeconds(5)); + if (Variables.AppSettings.MqttIgnoreGracePeriod) + { + var lastResumed = SystemStateManager.LastEventOccurrence.TryGetValue(SystemStateEvent.Resume, out var lastResumeEventDate); + if (lastResumed && DateTime.Now < lastResumeEventDate.AddSeconds(gracePeriod)) + { + Log.Information("[MQTT] System resumed more than {gracePeriod} seconds ago, ignoring grace period on connection failed"); + break; + } + } } // nope, call it diff --git a/src/HASS.Agent/HASS.Agent/Managers/SystemStateManager.cs b/src/HASS.Agent/HASS.Agent/Managers/SystemStateManager.cs index ca4c4ded..22fbfb1e 100644 --- a/src/HASS.Agent/HASS.Agent/Managers/SystemStateManager.cs +++ b/src/HASS.Agent/HASS.Agent/Managers/SystemStateManager.cs @@ -12,7 +12,7 @@ internal static class SystemStateManager /// The pointer to unregister the monitor power notifications /// internal static IntPtr UnRegPowerNotify { get; set; } = IntPtr.Zero; - + /// /// Notes the last time something happened to the system's state, eg. user logged on, session locked, etc /// @@ -21,7 +21,21 @@ internal static class SystemStateManager /// /// Contains the last event that happened to the system, eg. user logged on, session locked, etc /// - internal static SystemStateEvent LastSystemStateEvent { get; private set; } = SystemStateEvent.ApplicationStarted; + private static SystemStateEvent s_lastSystemStateEvent = SystemStateEvent.ApplicationStarted; + internal static SystemStateEvent LastSystemStateEvent + { + get => s_lastSystemStateEvent; + private set + { + s_lastSystemStateEvent = value; + LastEventOccurrence[s_lastSystemStateEvent] = DateTime.Now; + } + } + + /// + /// Contains the key value pair with SystemStateEvent and the last time it occurred + /// + public static Dictionary LastEventOccurrence = new(); /// /// Contains the last event that happened to the monitors, eg. power on @@ -33,6 +47,7 @@ internal static class SystemStateManager /// internal static async void Initialize() { + LastSystemStateEvent = SystemStateEvent.ApplicationStarted; await Task.Run(Monitor); } @@ -67,7 +82,7 @@ internal static void ProcessSessionEnd() Log.Information("[SYSTEMSTATE] Session ending: system shutting down"); Task.Run(() => HelperFunctions.ShutdownAsync(TimeSpan.Zero)); LastSystemStateEvent = SystemStateEvent.SystemShutdown; - + } else { diff --git a/src/HASS.Agent/HASS.Agent/Models/Config/AppSettings.cs b/src/HASS.Agent/HASS.Agent/Models/Config/AppSettings.cs index caccd39e..ee404285 100644 --- a/src/HASS.Agent/HASS.Agent/Models/Config/AppSettings.cs +++ b/src/HASS.Agent/HASS.Agent/Models/Config/AppSettings.cs @@ -37,6 +37,7 @@ public AppSettings() public string TrayIconWebViewUrl { get; set; } = string.Empty; public bool TrayIconWebViewBackgroundLoading { get; set; } = false; public bool TrayIconWebViewShowMenuOnLeftClick { get; set; } = false; + public bool TrayIconUseModern { get; set; } = false; public string ServiceAuthId { get; set; } = string.Empty; @@ -78,5 +79,7 @@ public AppSettings() public bool MqttUseRetainFlag { get; set; } = true; public string MqttRootCertificate { get; set; } = string.Empty; public string MqttClientCertificate { get; set; } = string.Empty; + + public bool MqttIgnoreGracePeriod { get; set; } = false; } } diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.Designer.cs b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.Designer.cs index 779cfe72..ffd2a2ed 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.Designer.cs +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.Designer.cs @@ -608,6 +608,16 @@ internal static string CommandsManager_SetApplicationVolumeCommandDescription { } } + /// + /// Looks up a localized string similar to Sets the default audio input for the system (including default communication device). + ///Requires audio device name as a payload.. + /// + internal static string CommandsManager_SetAudioInputCommandDescription { + get { + return ResourceManager.GetString("CommandsManager_SetAudioInputCommandDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Sets the default audio output for the system. ///Requires audio device name as a payload.. @@ -2352,6 +2362,15 @@ internal static string ConfigMqtt_CbEnableMqtt { } } + /// + /// Looks up a localized string similar to Ignore grace period after waking up from hibernation. + /// + internal static string ConfigMqtt_CbIgnoreGracePeriod { + get { + return ResourceManager.GetString("ConfigMqtt_CbIgnoreGracePeriod", resourceCulture); + } + } + /// /// Looks up a localized string similar to &TLS. /// @@ -3036,6 +3055,15 @@ internal static string ConfigTrayIcon_CbShowWebView { } } + /// + /// Looks up a localized string similar to Use modern tray icon. + /// + internal static string ConfigTrayIcon_CbUseModernIcon { + get { + return ResourceManager.GetString("ConfigTrayIcon_CbUseModernIcon", resourceCulture); + } + } + /// /// Looks up a localized string similar to &Keep page loaded in the background. /// @@ -6006,7 +6034,7 @@ internal static string SensorsManager_MicrophoneActiveSensorDescription { } /// - /// Looks up a localized string similar to Provides the name of the process that's currently using the microphone. + /// Looks up a localized string similar to Provides the number of the processes that currently use the microphone - additionally provides name of the processes in the sensor's attributes. /// ///Note: if used in the satellite service, it won't detect userspace applications.. /// @@ -6066,6 +6094,7 @@ internal static string SensorsManager_PerformanceCounterSensorDescription { /// /// Looks up a localized string similar to Returns the result of the provided Powershell command or script. + ///Note: please keep in mind that Home Assistant accepts payload up to 255 characters. /// ///Converts the outcome to text.. /// @@ -6095,6 +6124,16 @@ internal static string SensorsManager_ProcessActiveSensorDescription { } } + /// + /// Looks up a localized string similar to Provides a screenshot sensor in form of a camera entity. + ///Screen number depends on system configuration - starts at 0.. + /// + internal static string SensorsManager_ScreenshotSensorDescription { + get { + return ResourceManager.GetString("SensorsManager_ScreenshotSensorDescription", resourceCulture); + } + } + /// /// Looks up a localized string similar to Returns the state of the provided service: NotFound, Stopped, StartPending, StopPending, Running, ContinuePending, PausePending or Paused. /// @@ -6520,6 +6559,15 @@ internal static string SensorsMod_LblSetting1_Process { } } + /// + /// Looks up a localized string similar to Screen number. + /// + internal static string SensorsMod_LblSetting1_ScreenNumber { + get { + return ResourceManager.GetString("SensorsMod_LblSetting1_ScreenNumber", resourceCulture); + } + } + /// /// Looks up a localized string similar to Service. /// diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.de.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.de.resx index d378941c..ef8af3e2 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.de.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.de.resx @@ -3252,11 +3252,6 @@ Zeigt nur Geräte an, die seit dem letzten Bericht gesehen wurden, d. h. Wenn de Stelle sicher, dass die Ortungsdienste von Windows aktiviert sind! Je nach Windows-Version finden Sie diese in der neuen Systemsteuerung -> „Datenschutz und Sicherheit“ -> „Standort“. - - - Stellt den Namen des Prozesses bereit, der das Mikrofon derzeit verwendet. - -Hinweis: Bei Verwendung im Satellitenservice werden keine Userspace-Anwendungen erkannt. Zeigt die letzte Änderung des Monitor-Energiezustands: @@ -3265,6 +3260,7 @@ Gedimmt, PowerOff, PowerOn und Unbekannt. Gibt das Ergebnis des bereitgestellten Powershell-Befehls oder -Skripts zurück. +Hinweis: Bitte beachten Sie, dass Home Assistant Nutzdaten von bis zu 255 Zeichen akzeptiert. Konvertiert das Ergebnis in Text. @@ -3477,4 +3473,26 @@ Erfordert den Namen des Audiogeräts als Nutzlast. Lautstärke (zwischen 0 und 100) + + Sie stellen einen Screenshot-Sensor in Form einer Kameraeinheit bereit. +Die Bildschirmnummer hängt von der Systemkonfiguration ab – beginnt bei 0. + + + Bildschirmnummer + + + Legt den Standard-Audioeingang für das System fest (einschließlich des Standard-Kommunikationsgeräts). +Als Payload benötigen Sie den Namen des Audiogeräts. + + + Ignorieren Sie die Schonfrist nach dem Aufwachen aus dem Ruhezustand + + + Verwenden Sie ein modernes Taskleistensymbol + + + Gibt die Anzahl der Prozesse an, die derzeit das Mikrofon verwenden. Zusätzlich werden die Namen der Prozesse in den Attributen des Sensors angegeben. + +Hinweis: Bei Verwendung im Satellitendienst werden keine Userspace-Anwendungen erkannt. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.en.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.en.resx index 23c49a32..2d827e9e 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.en.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.en.resx @@ -3130,11 +3130,6 @@ Only shows devices that were seen since the last report, ie. when the sensor pub Make sure Windows' location services are enabled! Depending on your Windows version, this can be found in the new control panel -> 'privacy and security' -> 'location'. - - - Provides the name of the process that's currently using the microphone. - -Note: if used in the satellite service, it won't detect userspace applications. Provides the last monitor power state change: @@ -3143,6 +3138,7 @@ Dimmed, PowerOff, PowerOn and Unkown. Returns the result of the provided Powershell command or script. +Note: please keep in mind that Home Assistant accepts payload up to 255 characters. Converts the outcome to text. @@ -3354,4 +3350,26 @@ Requires audio device name as a payload. Volume (between 0 and 100) + + Screen number + + + Provides a screenshot sensor in form of a camera entity. +Screen number depends on system configuration - starts at 0. + + + Sets the default audio input for the system (including default communication device). +Requires audio device name as a payload. + + + Ignore grace period after waking up from hibernation + + + Use modern tray icon + + + Provides the number of the processes that currently use the microphone - additionally provides name of the processes in the sensor's attributes. + +Note: if used in the satellite service, it won't detect userspace applications. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.es.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.es.resx index ff26c4b5..88070530 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.es.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.es.resx @@ -3129,11 +3129,6 @@ Sólo muestra los dispositivos que fueron vistos desde el último informe, es de Asegúrese de que los servicios de localización de Windows están activados. Dependiendo de su versión de Windows, esto se puede encontrar en el nuevo panel de control -> 'privacidad y seguridad' -> 'ubicación'. - - - Proporciona el nombre del proceso que está usando actualmente el micrófono. - -Nota: si se usa en el servicio de satélite, no detectará las aplicaciones del espacio de usuario. Proporciona el último cambio de estado de energía del monitor: @@ -3142,6 +3137,7 @@ Atenuado, Apagado, Encendido y Desconocido. Devuelve el resultado del comando o script de Powershell proporcionado. +Nota: tenga en cuenta que Home Assistant acepta una carga útil de hasta 255 caracteres. Convierte el resultado en texto. @@ -3353,4 +3349,26 @@ Requiere el nombre del dispositivo de audio como carga útil. Nombre del dispositivo de audio + + Proporciona un sensor de captura de pantalla en forma de entidad de cámara. +El número de pantalla depende de la configuración del sistema: comienza en 0. + + + Número de pantalla + + + Establece la entrada de audio predeterminada para el sistema (incluido el dispositivo de comunicación predeterminado). +Necesita el nombre del dispositivo de audio como carga útil. + + + Ignorar el período de gracia después de despertar de la hibernación + + + Utilice el icono de bandeja moderno + + + Proporciona el número de procesos que utilizan actualmente el micrófono; además, proporciona el nombre de los procesos en los atributos del sensor. + +Nota: si se usa en el servicio satelital, no detectará aplicaciones del espacio de usuario. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.fr.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.fr.resx index 5e3fde18..f8295066 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.fr.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.fr.resx @@ -3162,11 +3162,6 @@ Affiche uniquement les appareils qui ont été vus depuis le dernier rapport, c' Assurez-vous que les services de localisation de Windows sont activés ! Selon votre version de Windows, cela peut être trouvé dans le nouveau panneau de configuration -> 'confidentialité et sécurité' -> 'emplacement'. - - - Provides the name of the process that's currently using the microphone. - -Note: if used in the satellite service, it won't detect userspace applications. Provides the last monitor power state change: @@ -3174,9 +3169,10 @@ Note: if used in the satellite service, it won't detect userspace applications.< Dimmed, PowerOff, PowerOn and Unkown. - Returns the result of the provided Powershell command or script. + Renvoie le résultat de la commande ou du script Powershell fourni. +Remarque : veuillez garder à l'esprit que Home Assistant accepte des charges utiles allant jusqu'à 255 caractères. -Converts the outcome to text. +Convertit le résultat en texte. Fournit des informations sur toutes les imprimantes installées et leurs files d'attente. @@ -3386,4 +3382,26 @@ Nécessite le nom du périphérique audio comme charge utile. Nom du périphérique audio + + Vous fournissez un capteur de capture d’écran sous la forme d’une entité caméra. +Le numéro d'écran dépend de la configuration du système - commence à 0. + + + Numéro d'écran + + + Définit l'entrée audio par défaut du système (y compris le périphérique de communication par défaut). +Vous avez besoin du nom du périphérique audio comme charge utile. + + + Ignorer la période de grâce après la sortie de l'hibernation + + + Utiliser l'icône de la barre d'état moderne + + + Fournit le nombre de processus qui utilisent actuellement le microphone - fournit en outre le nom des processus dans les attributs du capteur. + +Remarque : s'il est utilisé dans le service satellite, il ne détectera pas les applications de l'espace utilisateur. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.nl.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.nl.resx index fc9c643e..f810730d 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.nl.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.nl.resx @@ -3151,11 +3151,6 @@ Toont alleen apparaten die zijn gezien sinds het laatste rapport, oftewel, zodra Verzeker dat Windows' localisatieservices ingeschakeld zijn! Afhankelijk van je Windows versie, kan dit gevonden worden in het nieuwe configuratiescherm -> 'privacy en beveiliging' -> 'locatie'. - - - Geeft de naam van het proces dat momenteel de microfoon gebruikt. - -Notitie: als hij in de satellietservice gebruikt wordt, zal hij geen gebruikerapplicaties detecteren. Geeft de laatste beeldscherm energiemodus status verandering: @@ -3163,9 +3158,10 @@ Notitie: als hij in de satellietservice gebruikt wordt, zal hij geen gebruikerap Gedimmed, Uitgeschakeld, Ingeschakeld en Onbekend - Geeft het resultaat van het opgegeven Powershell commando of script. + Retourneert het resultaat van de opgegeven Powershell-opdracht of -script. +Opmerking: houd er rekening mee dat Home Assistant een payload van maximaal 255 tekens accepteert. -Converteert de uitkomst naar text. +Converteert de uitkomst naar tekst. Geeft informatie over alle geïnstalleerde printers en hun wachtrijen. @@ -3374,4 +3370,26 @@ Vereist de naam van het audioapparaat als payload. Volume (tussen 0 en 100) + + U levert een screenshot-sensor in de vorm van een camera-entiteit. +Schermnummer is afhankelijk van de systeemconfiguratie - begint bij 0. + + + Schermnummer + + + Stelt de standaardaudio-invoer in voor het systeem (inclusief het standaardcommunicatieapparaat). +U hebt de naam van het audioapparaat nodig als payload. + + + Negeer de respijtperiode na het ontwaken uit de winterslaap + + + Gebruik het moderne ladepictogram + + + Geeft het aantal processen weer die momenteel de microfoon gebruiken - geeft bovendien de naam van de processen in de attributen van de sensor. + +Opmerking: indien gebruikt in de satellietdienst, zal het geen gebruikersruimtetoepassingen detecteren. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pl.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pl.resx index c2a0e768..8adea682 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pl.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pl.resx @@ -3239,11 +3239,6 @@ Pokazuje tylko te urządzenia które zgłaszały się w okresie od ostatniego ra Upewnij się że lokalizacja jest włączona w systemie Windows! W zalezności od Twojej wersji Windows'a opcje te możesz znaleźć w Ustawienia -> Prywatność i Zabezpieczenia -> Lokalizacja - - - Zwraca nazwę procesu który obecnie używa mikrofonu. - -Wskazówka: jeżeli korzystasz z usługi Satellite, nie będziesz miał informacji o aplikacji w przestrzeni użytkownika. Zwraca ostatnią zmianę stanu ekranu: @@ -3251,9 +3246,10 @@ Wskazówka: jeżeli korzystasz z usługi Satellite, nie będziesz miał informac Przyciemnienie, Wyłączenie, Włączenie, Nieznany - Zwraca wynik poprzedniej komendy Powershell lub skryptu. + Zwraca wynik podanego polecenia lub skryptu programu PowerShell. +Uwaga: pamiętaj, że Home Assistant akceptuje ładunek o długości do 255 znaków. -Konwertuje wynik do tekstu. +Konwertuje wynik na tekst. Zwraca informacje na temat wszystkich zainstalowanych drukarek i ich kolejek. @@ -3463,4 +3459,26 @@ Wymaga nazwy urządzenia audio jako ładunku. Nazwa urządzenia audio + + Czujnik zrzutu ekranu w postaci kamery. +Numer ekranu zależy od konfiguracji systemu – zaczyna się od 0. + + + Numer ekranu + + + Ustawia domyślne wejście audio dla systemu (w tym domyślne urządzenie komunikacyjne). +Wymaga nazwy urządzenia audio jako ładunku. + + + Ignoruj okres karencji po przebudzeniu ze stanu hibernacji + + + Użyj nowoczesnej ikony w zasobniku + + + Podaje liczbę procesów aktualnie korzystających z mikrofonu - dodatkowo podaje nazwę procesów w atrybutach czujnika. + +Uwaga: jeśli jest używany w usłudze satelitarnej, nie wykryje aplikacji przestrzeni użytkownika. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pt-br.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pt-br.resx index 16e45e80..92c42b50 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pt-br.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.pt-br.resx @@ -2250,11 +2250,6 @@ HassAgentStarted, Logoff, SystemShutdown, Resume, Suspend, ConsoleConnect, Conso Fornece um valor bool com base em se o microfone está sendo usado no momento. Fuzzy - - Fornece o nome do processo que está usando o microfone no momento. - -Observação: se usado no serviço satélite, ele não detectará aplicativos de espaço de usuário. - Fornece a última alteração do estado de energia do monitor: @@ -2281,6 +2276,7 @@ Você pode explorar os contadores através da ferramenta 'perfmon.exe' do Window Retorna o resultado do comando ou script do Powershell fornecido. +Observação: lembre-se de que o Home Assistant aceita carga útil de até 255 caracteres. Converte o resultado em texto. @@ -3399,4 +3395,26 @@ Requer o nome do dispositivo de áudio como carga útil. Volume (entre 0 e 100) + + Você fornece um sensor de captura de tela na forma de uma entidade de câmera. +O número da tela depende da configuração do sistema – começa em 0. + + + Número da tela + + + Define a entrada de áudio padrão para o sistema (incluindo o dispositivo de comunicação padrão). +Você precisa do nome do dispositivo de áudio como carga útil. + + + Ignorar o período de carência após sair da hibernação + + + Use o ícone moderno da bandeja + + + Fornece o número de processos que utilizam atualmente o microfone - fornece adicionalmente o nome dos processos nos atributos do sensor. + +Nota: se usado no serviço de satélite, não detectará aplicações no espaço do usuário. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.resx index 72abfe05..69340051 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.resx @@ -2925,6 +2925,7 @@ Dimmed, PowerOff, PowerOn and Unkown. Returns the result of the provided Powershell command or script. +Note: please keep in mind that Home Assistant accepts payload up to 255 characters. Converts the outcome to text. @@ -2951,7 +2952,7 @@ Hidden, Maximized, Minimized, Normal and Unknown. Note: if used in the satellite service, it won't detect userspace applications. - Provides the name of the process that's currently using the microphone. + Provides the number of the processes that currently use the microphone - additionally provides name of the processes in the sensor's attributes. Note: if used in the satellite service, it won't detect userspace applications. @@ -3366,4 +3367,21 @@ Requires audio device name as a payload. Volume (between 0 and 100) + + Screen number + + + Provides a screenshot sensor in form of a camera entity. +Screen number depends on system configuration - starts at 0. + + + Sets the default audio input for the system (including default communication device). +Requires audio device name as a payload. + + + Ignore grace period after waking up from hibernation + + + Use modern tray icon + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.ru.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.ru.resx index 7aecfda6..5ecfc113 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.ru.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.ru.resx @@ -3197,11 +3197,6 @@ Home Assistant. Убедитесь, что службы определения местоположения Windows включены! В зависимости от вашей версии Windows, это можно найти в новой панели управления -> 'конфиденциальность и безопасность' -> 'местоположение'. - - - Указывает имя процесса, который в данный момент использует микрофон. - -Примечание: если он используется в спутниковой службе, он не будет обнаруживать приложения пользовательского пространства. Обеспечивает последнее изменение состояния питания монитора: @@ -3209,7 +3204,8 @@ Home Assistant. Затемненный, Выключенный, Включенный и Неизвестный. - Возвращает результат предоставленной команды Powershell или сценария. + Возвращает результат предоставленной команды или сценария Powershell. +Примечание. Имейте в виду, что Home Assistant принимает полезную нагрузку длиной до 255 символов. Преобразует результат в текст. @@ -3422,4 +3418,26 @@ Home Assistant. Имя аудиоустройства + + Вы предоставляете датчик скриншота в виде объекта камеры. +Номер экрана зависит от конфигурации системы — начинается с 0. + + + Номер экрана + + + Устанавливает аудиовход по умолчанию для системы (включая устройство связи по умолчанию). +В качестве полезной нагрузки требуется имя аудиоустройства. + + + Игнорировать льготный период после выхода из спящего режима + + + Использовать современный значок в трее + + + Предоставляет количество процессов, которые в данный момент используют микрофон. Дополнительно указывается имя процесса в атрибутах датчика. + +Примечание. При использовании в спутниковой службе он не обнаружит приложения пользовательского пространства. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.sl.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.sl.resx index 0a7b3781..b50418be 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.sl.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.sl.resx @@ -2328,11 +2328,6 @@ HassAgentStarted, Odjava, SystemShutdown, Resume, Suspend, ConsoleConnect, Conso Zagotavlja bool vrednost glede na to, ali se mikrofon trenutno uporablja. Fuzzy - - Vrne ime procesa, ki trenutno uporablja mikrofon. - -Opomba: če se uporablja v satelitski storitvi - Vrne zadnjo spremembo načina monitorja: @@ -2358,9 +2353,10 @@ Primer: _Skupaj Številke lahko raziščete z orodjem Windows 'perfmon.exe'. - Vrne rezultat Powershell ukaza ali skripta. + Vrne rezultat podanega ukaza ali skripta Powershell. +Opomba: ne pozabite, da Home Assistant sprejme vsebino do 255 znakov. -Pretvori rezultat v tekst. +Pretvori rezultat v besedilo. Vrne informacijo o nameščenih tiskalnikih in čakalni vrsti za njih. @@ -3502,4 +3498,26 @@ Zahteva ime zvočne naprave kot tovor. Glasnost (med 0 in 100) + + Zagotovite senzor posnetka zaslona v obliki entitete kamere. +Številka zaslona je odvisna od konfiguracije sistema - začne se pri 0. + + + Številka zaslona + + + Nastavi privzeti zvočni vhod za sistem (vključno s privzeto komunikacijsko napravo). +Ime zvočne naprave potrebujete kot obremenitev. + + + Po prebujanju iz mirovanja prezrite obdobje odloga + + + Uporabi sodobno ikono pladnja + + + Zagotavlja število procesov, ki trenutno uporabljajo mikrofon - dodatno podaja ime procesov v atributih senzorja. + +Opomba: če se uporablja v satelitski storitvi, ne bo zaznal aplikacij uporabniškega prostora. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.tr.resx b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.tr.resx index 064d8465..140a75ac 100644 --- a/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.tr.resx +++ b/src/HASS.Agent/HASS.Agent/Resources/Localization/Languages.tr.resx @@ -2764,14 +2764,14 @@ Daha fazla tuşa ve/veya CTRL gibi değiştiricilere ihtiyacınız varsa, Multip Geçerli enlem, boylam ve yüksekliğinizi virgülle ayrılmış bir değer olarak döndürür. Windows'un konum hizmetlerinin etkinleştirildiğinden emin olun! Windows sürümünüze bağlı olarak bu, yeni kontrol panelinde -> 'gizlilik ve güvenlik' -> 'konum'da bulunabilir. - - Şu anda mikrofonu kullanan işlemin adını sağlar. Not: uydu hizmetinde kullanılırsa, kullanıcı alanı uygulamalarını algılamaz. - Son monitör güç durumu değişikliğini sağlar: Soluk, Güç Kapalı, Güç Açık ve Bilinmiyor. - Sağlanan Powershell komutunun veya komut dosyasının sonucunu döndürür. Sonucu metne dönüştürür. + Sağlanan Powershell komutunun veya betiğinin sonucunu döndürür. +Not: Home Assistant'ın 255 karaktere kadar yükü kabul ettiğini lütfen unutmayın. + +Sonucu metne dönüştürür. Yüklü tüm yazıcılar ve sıraları hakkında bilgi sağlar. @@ -2965,4 +2965,26 @@ Yük olarak ses cihazı adını gerektirir. Hacim (0 ile 100 arasında) + + Bir kamera varlığı biçiminde bir ekran görüntüsü sensörü sağlarsınız. +Ekran numarası sistem konfigürasyonuna bağlıdır - 0'dan başlar. + + + Ekran numarası + + + Sistem için varsayılan ses girişini ayarlar (varsayılan iletişim cihazı dahil). +Yük olarak ses cihazı adına ihtiyacınız vardır. + + + Hazırda bekletme modundan uyandıktan sonra ek süreyi göz ardı et + + + Modern tepsi simgesini kullan + + + Halihazırda mikrofonu kullanan işlemlerin sayısını sağlar; ayrıca sensörün özniteliklerinde süreçlerin adını da sağlar. + +Not: Uydu hizmetinde kullanılırsa kullanıcı alanı uygulamalarını algılamaz. + \ No newline at end of file diff --git a/src/HASS.Agent/HASS.Agent/Resources/modern-tray-icon.ico b/src/HASS.Agent/HASS.Agent/Resources/modern-tray-icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4bc498569af998717d123e342e879a1b7957a362 GIT binary patch literal 52111 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 tq-AD@c>$ @@ -96,32 +98,34 @@ private static async void Process() // optionally flag as the first real run if (!firstRunDone) firstRunDone = true; - // publish availability & sensor autodisco's every 30 sec + // publish availability & autodiscovery every 30 sec if ((DateTime.Now - _lastAutoDiscoPublish).TotalSeconds > 30) { - // let hass know we're still here await Variables.MqttManager.AnnounceAvailabilityAsync(); - // publish the autodisco's - if (SingleValueSensorsPresent()) + if (!_discoveryPublished) { - foreach (var sensor in Variables.SingleValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + if (SingleValueSensorsPresent()) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; - await sensor.PublishAutoDiscoveryConfigAsync(); + foreach (var sensor in Variables.SingleValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; + await sensor.PublishAutoDiscoveryConfigAsync(); + } } - } - if (MultiValueSensorsPresent()) - { - foreach (var sensor in Variables.MultiValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + if (MultiValueSensorsPresent()) { - if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; - await sensor.PublishAutoDiscoveryConfigAsync(); + foreach (var sensor in Variables.MultiValueSensors.TakeWhile(_ => !_pause).TakeWhile(_ => _active)) + { + if (_pause || Variables.MqttManager.GetStatus() != MqttStatus.Connected) continue; + await sensor.PublishAutoDiscoveryConfigAsync(); + } } + + _discoveryPublished = true; } - // log moment _lastAutoDiscoPublish = DateTime.Now; } @@ -661,6 +665,14 @@ internal static void LoadSensorInfo() SensorInfoCards.Add(sensorInfoCard.SensorType, sensorInfoCard); // ================================= + + sensorInfoCard = new SensorInfoCard(SensorType.ScreenshotSensor, + Languages.SensorsManager_ScreenshotSensorDescription, + 10, false, true, false); + + SensorInfoCards.Add(sensorInfoCard.SensorType, sensorInfoCard); + + // ================================= } } } diff --git a/src/HASS.Agent/HASS.Agent/Settings/StoredCommands.cs b/src/HASS.Agent/HASS.Agent/Settings/StoredCommands.cs index c277e876..9ddb228a 100644 --- a/src/HASS.Agent/HASS.Agent/Settings/StoredCommands.cs +++ b/src/HASS.Agent/HASS.Agent/Settings/StoredCommands.cs @@ -178,6 +178,9 @@ internal static AbstractCommand ConvertConfiguredToAbstract(ConfiguredCommand co case CommandType.SetAudioOutputCommand: abstractCommand = new SetAudioOutputCommand(command.EntityName, command.Name, command.Command, command.EntityType, command.Id.ToString()); break; + case CommandType.SetAudioInputCommand: + abstractCommand = new SetAudioInputCommand(command.EntityName, command.Name, command.Command, command.EntityType, command.Id.ToString()); + break; default: Log.Error("[SETTINGS_COMMANDS] [{name}] Unknown configured command type: {type}", command.EntityName, command.Type.ToString()); break; diff --git a/src/HASS.Agent/HASS.Agent/Settings/StoredSensors.cs b/src/HASS.Agent/HASS.Agent/Settings/StoredSensors.cs index 6dce967d..8c908fd2 100644 --- a/src/HASS.Agent/HASS.Agent/Settings/StoredSensors.cs +++ b/src/HASS.Agent/HASS.Agent/Settings/StoredSensors.cs @@ -3,6 +3,7 @@ using HASS.Agent.Extensions; using HASS.Agent.HomeAssistant.Sensors.GeneralSensors.MultiValue; using HASS.Agent.HomeAssistant.Sensors.GeneralSensors.SingleValue; +using HASS.Agent.Managers.DeviceSensors; using HASS.Agent.Resources.Localization; using HASS.Agent.Shared.Enums; using HASS.Agent.Shared.Extensions; @@ -196,7 +197,10 @@ internal static AbstractSingleValueSensor ConvertConfiguredToAbstractSingleValue abstractSensor = new BluetoothLeDevicesSensor(sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString()); break; case SensorType.InternalDeviceSensor: - abstractSensor = new InternalDeviceSensor(sensor.Query, sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString()); + abstractSensor = new HomeAssistant.Sensors.GeneralSensors.SingleValue.InternalDeviceSensor(sensor.Query, sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString()); + break; + case SensorType.ScreenshotSensor: + abstractSensor = new ScreenshotSensor(sensor.Query, sensor.UpdateInterval, sensor.EntityName, sensor.Name, sensor.Id.ToString()); break; default: Log.Error("[SETTINGS_SENSORS] [{name}] Unknown configured single-value sensor type: {type}", sensor.EntityName, sensor.Type.ToString()); @@ -389,7 +393,7 @@ internal static ConfiguredSensor ConvertAbstractSingleValueToConfigured(Abstract }; } - case InternalDeviceSensor internalDeviceSensor: + case HomeAssistant.Sensors.GeneralSensors.SingleValue.InternalDeviceSensor internalDeviceSensor: { _ = Enum.TryParse(internalDeviceSensor.GetType().Name, out var type); return new ConfiguredSensor @@ -404,6 +408,21 @@ internal static ConfiguredSensor ConvertAbstractSingleValueToConfigured(Abstract }; } + case ScreenshotSensor screenshotSensor: + { + _ = Enum.TryParse(screenshotSensor.GetType().Name, out var type); + return new ConfiguredSensor + { + Id = Guid.Parse(screenshotSensor.Id), + EntityName = screenshotSensor.EntityName, + Name = screenshotSensor.Name, + Type = type, + UpdateInterval = screenshotSensor.UpdateIntervalSeconds, + IgnoreAvailability = screenshotSensor.IgnoreAvailability, + Query = screenshotSensor.ScreenIndex.ToString() + }; + } + default: { _ = Enum.TryParse(sensor.GetType().Name, out var type);