From f22c7bb00596f932fa89f9d0b683092fe85a699d Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 13 Sep 2020 22:01:12 -0700 Subject: [PATCH 1/5] ensure the light turns off when app shutdown --- MainWindow.xaml | 1 - MainWindow.xaml.cs | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index f59bb82..a1a0e8b 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -7,7 +7,6 @@ mc:Ignorable="d" Title="Media sensor" Width="260" - Closing="Window_Closing" SizeToContent="Height" ResizeMode="CanMinimize" > diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 01b7c57..e980468 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -26,6 +26,8 @@ public MainWindow() var apiEndpoint = new ApiEndpoint(this.Sensor, this.Configuration); this.Core = new Core(this.Configuration, apiEndpoint, this.Sensor); this.Core.StateUpdated += OnStateUpdated; + Application.Current.SessionEnding += OnSessionEnding; + this.Closing += OnWindowClosing; Task.Run(async () => { @@ -123,7 +125,13 @@ private void OverrideButton_Click(object sender, RoutedEventArgs e) RunAsyncSafely(async () => await this.Core.ToggleOverrideAsync()); } - private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) + private void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e) + { + RunAsyncSafely(async () => await this.Core.ShutdownAsync()) + .Wait(); + } + + private void OnSessionEnding(object sender, SessionEndingCancelEventArgs e) { RunAsyncSafely(async () => await this.Core.ShutdownAsync()) .Wait(); From 011ce40ce1f2978563cffa6469c7d0b71d560870 Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 13 Sep 2020 22:19:16 -0700 Subject: [PATCH 2/5] use keyboard gestures instead of minimizing hack --- MainWindow.xaml | 2 +- MainWindow.xaml.cs | 109 ++++++++++++++++++++++++--------------------- 2 files changed, 60 insertions(+), 51 deletions(-) diff --git a/MainWindow.xaml b/MainWindow.xaml index a1a0e8b..771523f 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -15,7 +15,7 @@ Margin="5 0 0 0" DockPanel.Dock="Right" Width="60" - Padding="4 1" Click="OverrideButton_Click"> + Padding="4 1" Click="OnOverrideButtonClick"> Toggle diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index e980468..ad09aaf 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -13,11 +13,6 @@ public partial class MainWindow : Window private ConfigurationReader Configuration { get; } private Sensor Sensor { get; } - private bool ShowMediaState { get; set; } - private WindowState LastWindowState { get; set; } - private bool CanToggleOnRestore { get; set; } - private static TimeSpan ToggleOnRestoreDelay { get; } = TimeSpan.FromSeconds(10); - public MainWindow() { InitializeComponent(); @@ -28,6 +23,7 @@ public MainWindow() this.Core.StateUpdated += OnStateUpdated; Application.Current.SessionEnding += OnSessionEnding; this.Closing += OnWindowClosing; + this.PreviewKeyDown += OnPreviewKeyDown; Task.Run(async () => { @@ -51,40 +47,13 @@ private async Task InitializeAsync() throw new InvalidOperationException($"Please update {ConfigurationReader.ConfigurationFileName}"); } - ShowMediaState = this.Configuration.SoundSensor; - await this.Core.InitializeAsync(); await this.Core.UpdateEndpointAsync(); - - this.StateChanged += OnStateChanged; - this.CanToggleOnRestore = true; - } - - private void OnStateChanged(object? sender, EventArgs e) - { - if (this.LastWindowState == WindowState.Minimized - && this.WindowState != WindowState.Minimized) - { - OnWindowRestored(); - } - this.LastWindowState = this.WindowState; - } - - private void OnWindowRestored() - { - if (this.Configuration.ToggleOnRestore && this.CanToggleOnRestore) - { - RunAsyncSafely(async () => - { - this.CanToggleOnRestore = false; - await this.Core.ToggleOverrideAsync(); - _ = this.Dispatcher.BeginInvoke((Action)(async () => await this.AnimateAndMinimize())); - await Task.Delay(ToggleOnRestoreDelay); - this.CanToggleOnRestore = true; - }); - } } + /// + /// Reacts to new state by updating the UI + /// private void OnStateUpdated(object? sender, UpdateArgs e) { if (e.Exception == null) @@ -93,6 +62,10 @@ private void OnStateUpdated(object? sender, UpdateArgs e) HandleException(e.Exception); } + /// + /// Updates the text labels in the UI. This method may be called from any thread. + /// + /// which contain the updated state. private void UpdateUi(UpdateArgs args) { if (System.Windows.Threading.Dispatcher.CurrentDispatcher != this.Dispatcher) @@ -117,39 +90,72 @@ private void UpdateUi(UpdateArgs args) _ => "Switch: error", }; - this.StatusText.Visibility = this.ShowMediaState ? Visibility.Visible : Visibility.Collapsed; + this.StatusText.Visibility = this.Configuration.SoundSensor + ? Visibility.Visible + : Visibility.Collapsed; } - private void OverrideButton_Click(object sender, RoutedEventArgs e) + /// + /// Handle keyboard gestures, whether the button is focused or not + /// + private void OnPreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e) + { + switch (e.Key) + { + case System.Windows.Input.Key.Space: + // Space toggles the button + RunAsyncSafely(async () => await this.Core.ToggleOverrideAsync()); + e.Handled = true; + break; + + case System.Windows.Input.Key.Enter: + // Enter toggles the button and minimizes + RunAsyncSafely(async () => await this.Core.ToggleOverrideAsync()); + this.WindowState = WindowState.Minimized; + e.Handled = true; + break; + + case System.Windows.Input.Key.Escape: + // Escape minimizes + e.Handled = true; + this.WindowState = WindowState.Minimized; + break; + + default: + e.Handled = false; + break; + } + } + + /// + /// Handle mouse gesture for the button + /// + private void OnOverrideButtonClick(object sender, RoutedEventArgs e) { RunAsyncSafely(async () => await this.Core.ToggleOverrideAsync()); } + /// + /// Turn off the light after delay when user closes the app + /// private void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e) { RunAsyncSafely(async () => await this.Core.ShutdownAsync()) .Wait(); } + /// + /// Turn off the light after delay when OS closes the app + /// private void OnSessionEnding(object sender, SessionEndingCancelEventArgs e) { RunAsyncSafely(async () => await this.Core.ShutdownAsync()) .Wait(); } - private async Task AnimateAndMinimize() - { - var textToRestore = this.OverridingText.Text; - this.OverridingText.Text = "Gotcha! "; - await Task.Delay(300).ConfigureAwait(true); - this.OverridingText.Text = "Gotcha!! "; - await Task.Delay(300).ConfigureAwait(true); - this.OverridingText.Text = "Gotcha!!!"; - await Task.Delay(300).ConfigureAwait(true); - this.OverridingText.Text = textToRestore; - this.WindowState = WindowState.Minimized; - } - + /// + /// Dispatches async method to run. Returns immediately. Handles exceptions. + /// private Task RunAsyncSafely(Func action) { return Task.Run(async () => @@ -165,6 +171,9 @@ private Task RunAsyncSafely(Func action) }); } + /// + /// Handle exception by permanently disabling the sensor and displaying the message. + /// private void HandleException(Exception ex) { // Immediately stop further updates From 4990aabe785368615664dab56ef07a99242700f0 Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 13 Sep 2020 22:24:18 -0700 Subject: [PATCH 3/5] remove unused configuration --- ConfigurationReader.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/ConfigurationReader.cs b/ConfigurationReader.cs index 86d7468..902ecdc 100644 --- a/ConfigurationReader.cs +++ b/ConfigurationReader.cs @@ -13,7 +13,6 @@ class ConfigurationReader internal int Latch { get; private set; } internal bool Initialized { get; private set; } internal bool SoundSensor { get; private set; } - internal bool ToggleOnRestore { get; private set; } internal ConfigurationReader() { @@ -36,9 +35,8 @@ internal void Initialize() @"url: http://host:8123/api/states/sensor.media # URL of the API endpoint. See https://developers.home-assistant.io/docs/en/external_api_rest.html token: InsertLongTermTokenHere # Home Assistant long term token poll: 250 # Polling delay in milliseconds. This represents delay between calls to the OS. -latch: 1000 # Latching delay in milliseconds. This represents duration of how long media state must be steady before making API call +latch: 1000 # Latching delay in milliseconds. This represents duration of how long media state must be steady before making API call soundsensor: true # true to use sound sensor. false to use the app as on-off switch -onrestore: true # true to toggle on restore. false to not react to window restore "); Initialized = false; } @@ -46,8 +44,8 @@ internal void Initialize() private bool ReadConfiguration() { - bool gotUrl, gotToken, gotPoll, gotLatch, gotSoundSensor, gotRestore; - gotUrl = gotToken = gotPoll = gotLatch = gotSoundSensor = gotRestore = false; + bool gotUrl, gotToken, gotPoll, gotLatch, gotSoundSensor; + gotUrl = gotToken = gotPoll = gotLatch = gotSoundSensor = false; var lines = File.ReadAllLines(ConfigurationFileName); foreach (var line in lines) @@ -76,13 +74,9 @@ private bool ReadConfiguration() Latch = Int32.Parse(value); gotLatch = true; break; - case "onrestore": - ToggleOnRestore = Boolean.Parse(value); - gotRestore = true; - break; } - if (gotUrl && gotToken && gotPoll && gotLatch && gotSoundSensor && gotRestore) + if (gotUrl && gotToken && gotPoll && gotLatch && gotSoundSensor) return true; } @@ -96,8 +90,6 @@ private bool ReadConfiguration() throw new ApplicationException("Configuration did not contain key with integer value: latch"); if (!gotSoundSensor) throw new ApplicationException("Configuration did not contain key with Boolean value: soundsensor"); - if (!gotRestore) - throw new ApplicationException("Configuration did not contain key with Boolean value: onrestore"); return false; } From ab4cf4d7e36933703bcc31a40d918e1eafe7baea Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 13 Sep 2020 22:24:23 -0700 Subject: [PATCH 4/5] bump version --- MediaSensor.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaSensor.csproj b/MediaSensor.csproj index dbb4aa9..d4ea46b 100644 --- a/MediaSensor.csproj +++ b/MediaSensor.csproj @@ -4,7 +4,7 @@ WinExe netcoreapp3.1 true - 1.4.0 + 1.5.0 Amadeusz Wieczorek Amadeusz Wieczorek https://github.com/AmadeusW/homeautomation-mediasensor From 7ae0ed6092f51184fa408dbba4c468d973626cc2 Mon Sep 17 00:00:00 2001 From: Amadeusz Wieczorek Date: Sun, 13 Sep 2020 22:27:21 -0700 Subject: [PATCH 5/5] update readme --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 68b94dc..8798320 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ and turn the light on when the media is stopped. * Override the sound sensor and manually control the light * Option to not use the sound sensor at all * Mouse free operation - * Control the light when minimized app restores (e.g. with alt tab) + * Space, Enter and Escape shortcuts to operate and/or minimize the app. * Don't worry about turning the PC off - * Turn the light off after a delay when app closes + * The light turns off a minute after the app closes ## Prerequisites @@ -36,8 +36,7 @@ url: http://hass-server:8123/api/states/sensor.tvroommedia # URL of the API endp token: redacted # Home Assistant long term token poll: 250 # Polling delay in milliseconds. This represents delay between calls to the OS. latch: 1000 # Latching delay in milliseconds. This represents duration of how long media state must be steady before making API call -soundsensor: true # true to use sound sensor. false to use the app as on-off switch -onrestore: true # true to toggle on restore. false to not react to window restore +soundsensor: true # true to enable the sound sensor. false to use the app as just an on-off switch ``` `automations.yaml` on the Home Assistant server