Skip to content

Commit

Permalink
Merge pull request #6 from AmadeusW/fixToggling
Browse files Browse the repository at this point in the history
Improve toggling UX: keyboard shortcuts, on app shutdown
  • Loading branch information
AmadeusW authored Sep 16, 2020
2 parents 62fd18a + 7ae0ed6 commit 7779bbe
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 67 deletions.
16 changes: 4 additions & 12 deletions ConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -36,18 +35,17 @@ 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;
}
}

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)
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down
3 changes: 1 addition & 2 deletions MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
mc:Ignorable="d"
Title="Media sensor"
Width="260"
Closing="Window_Closing"
SizeToContent="Height"
ResizeMode="CanMinimize"
>
Expand All @@ -16,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
</Button>
<StackPanel>
Expand Down
113 changes: 65 additions & 48 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -26,6 +21,9 @@ 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;
this.PreviewKeyDown += OnPreviewKeyDown;

Task.Run(async () =>
{
Expand All @@ -49,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;
});
}
}

/// <summary>
/// Reacts to new state by updating the UI
/// </summary>
private void OnStateUpdated(object? sender, UpdateArgs e)
{
if (e.Exception == null)
Expand All @@ -91,6 +62,10 @@ private void OnStateUpdated(object? sender, UpdateArgs e)
HandleException(e.Exception);
}

/// <summary>
/// Updates the text labels in the UI. This method may be called from any thread.
/// </summary>
/// <param name="args"><see cref="UpdateArgs"/> which contain the updated state.</param>
private void UpdateUi(UpdateArgs args)
{
if (System.Windows.Threading.Dispatcher.CurrentDispatcher != this.Dispatcher)
Expand All @@ -115,33 +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;
}

/// <summary>
/// Handle keyboard gestures, whether the button is focused or not
/// </summary>
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;
}
}

private void OverrideButton_Click(object sender, RoutedEventArgs e)
/// <summary>
/// Handle mouse gesture for the button
/// </summary>
private void OnOverrideButtonClick(object sender, RoutedEventArgs e)
{
RunAsyncSafely(async () => await this.Core.ToggleOverrideAsync());
}

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
/// <summary>
/// Turn off the light after delay when user closes the app
/// </summary>
private void OnWindowClosing(object sender, System.ComponentModel.CancelEventArgs e)
{
RunAsyncSafely(async () => await this.Core.ShutdownAsync())
.Wait();
}

private async Task AnimateAndMinimize()
/// <summary>
/// Turn off the light after delay when OS closes the app
/// </summary>
private void OnSessionEnding(object sender, SessionEndingCancelEventArgs e)
{
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;
RunAsyncSafely(async () => await this.Core.ShutdownAsync())
.Wait();
}

/// <summary>
/// Dispatches async method to run. Returns immediately. Handles exceptions.
/// </summary>
private Task RunAsyncSafely(Func<Task> action)
{
return Task.Run(async () =>
Expand All @@ -157,6 +171,9 @@ private Task RunAsyncSafely(Func<Task> action)
});
}

/// <summary>
/// Handle exception by permanently disabling the sensor and displaying the message.
/// </summary>
private void HandleException(Exception ex)
{
// Immediately stop further updates
Expand Down
2 changes: 1 addition & 1 deletion MediaSensor.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<UseWPF>true</UseWPF>
<Version>1.4.0</Version>
<Version>1.5.0</Version>
<Authors>Amadeusz Wieczorek</Authors>
<Company>Amadeusz Wieczorek</Company>
<RepositoryUrl>https://github.com/AmadeusW/homeautomation-mediasensor</RepositoryUrl>
Expand Down
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down

0 comments on commit 7779bbe

Please sign in to comment.