├── .github ├── FUNDING.yml └── workflows │ └── automerge.yaml ├── .gitignore ├── Home Assistant Taskbar Menu.sln ├── Home Assistant Taskbar Menu ├── App.config ├── App.xaml ├── App.xaml.cs ├── Connection │ ├── ApiConsumer.cs │ ├── HaClientContext.cs │ ├── HomeAssistantRestClient.cs │ ├── HomeAssistantServiceCallData.cs │ └── HomeAssistantWebsocketsClient.cs ├── Entities │ ├── AutomationEntity.cs │ ├── ButtonEntity.cs │ ├── ClimateEntity.cs │ ├── CoverEntity.cs │ ├── Entity.cs │ ├── FanEntity.cs │ ├── InputBooleanEntity.cs │ ├── InputButton.cs │ ├── InputNumberEntity.cs │ ├── InputSelectEntity.cs │ ├── LightEntity.cs │ ├── LockEntity.cs │ ├── MediaPlayerEntity.cs │ ├── NotificationEvent.cs │ ├── NumberEntity.cs │ ├── SceneEntity.cs │ ├── ScriptEntity.cs │ ├── SelectEntity.cs │ ├── SirenEntity.cs │ ├── SwitchEntity.cs │ └── VacuumEntity.cs ├── Home Assistant Taskbar Menu.csproj ├── Images │ └── small.ico ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ ├── Settings.settings │ └── app.manifest ├── Utils │ ├── Configuration.cs │ ├── ConsoleWriter.cs │ ├── EntityCreator.cs │ ├── ResourceProvider.cs │ ├── Storage.cs │ └── ViewConfiguration.cs ├── Views │ ├── AboutWindow.xaml │ ├── AboutWindow.xaml.cs │ ├── AuthWindow.xaml │ ├── AuthWindow.xaml.cs │ ├── BrowserWindow.xaml │ ├── BrowserWindow.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── SearchWindow.xaml │ ├── SearchWindow.xaml.cs │ ├── ViewConfigurationDialog.xaml │ ├── ViewConfigurationDialog.xaml.cs │ ├── ViewConfigurationWindow.xaml │ └── ViewConfigurationWindow.xaml.cs └── packages.config ├── Images ├── add_entry_1.png ├── add_entry_2.png ├── auth_1.png ├── auth_2.png ├── browser.png ├── icon.png ├── menu_1.png ├── menu_2.png ├── menu_3.png ├── menu_4.png ├── notification.png ├── search.png ├── shortcut_1.png ├── shortcut_2.png ├── view_1.png ├── view_2.png └── view_3.png ├── Installer_script.iss ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: piotrmachowski 2 | custom: ["buycoffee.to/piotrmachowski", "paypal.me/PiMachowski", "revolut.me/314ma"] 3 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yaml: -------------------------------------------------------------------------------- 1 | name: 'Automatically merge master -> dev' 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Automatically merge master to dev 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | name: Git checkout 15 | with: 16 | fetch-depth: 0 17 | - name: Merge master -> dev 18 | run: | 19 | git config user.name "GitHub Actions" 20 | git config user.email "PiotrMachowski@users.noreply.github.com" 21 | if (git checkout dev) 22 | then 23 | git merge --ff-only master || git merge --no-commit master 24 | git commit -m "Automatically merge master -> dev" || echo "No commit needed" 25 | git push origin dev 26 | else 27 | echo "No dev branch" 28 | fi -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | Installer/ 25 | nppBackup/ 26 | 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.1169 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Home Assistant Taskbar Menu", "Home Assistant Taskbar Menu\Home Assistant Taskbar Menu.csproj", "{B49305F6-8A11-43D6-AD36-864C95D4F015}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|x64.ActiveCfg = Debug|x64 21 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|x64.Build.0 = Debug|x64 22 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|x86.ActiveCfg = Debug|x86 23 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Debug|x86.Build.0 = Debug|x86 24 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|x64.ActiveCfg = Release|x64 27 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|x64.Build.0 = Release|x64 28 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|x86.ActiveCfg = Release|x86 29 | {B49305F6-8A11-43D6-AD36-864C95D4F015}.Release|x86.Build.0 = Release|x86 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {4840496F-2F22-4CDB-BCAD-CDB9F36F3313} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/App.xaml: -------------------------------------------------------------------------------- 1 |  7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Windows; 4 | using Home_Assistant_Taskbar_Menu.Connection; 5 | using Home_Assistant_Taskbar_Menu.Utils; 6 | using MaterialDesignThemes.Wpf; 7 | 8 | namespace Home_Assistant_Taskbar_Menu 9 | { 10 | /// 11 | /// Interaction logic for App.xaml 12 | /// 13 | public partial class App : Application 14 | { 15 | private void OnStartup(object sender, StartupEventArgs e) 16 | { 17 | bool enableLogging = e.Args.Length == 0; 18 | Storage.InitConfigDirectory(enableLogging); 19 | Configuration configuration = Storage.RestoreConfiguration(); 20 | ViewConfiguration viewConfiguration = Storage.RestoreViewConfiguration(); 21 | if (configuration != null && e.Args.Length > 0) 22 | { 23 | if (e.Args[0] == "call_service" && e.Args.Length > 2) 24 | { 25 | var service = e.Args[1]; 26 | var serviceData = string.Join(" ", e.Args.Skip(2).ToList()).Replace("\\\"", "\""); 27 | CallService(configuration, service, serviceData); 28 | Shutdown(); 29 | return; 30 | } 31 | } 32 | 33 | StartUi(viewConfiguration, configuration); 34 | } 35 | 36 | private static void StartUi(ViewConfiguration viewConfiguration, Configuration configuration) 37 | { 38 | var paletteHelper = new PaletteHelper(); 39 | var theme = paletteHelper.GetTheme(); 40 | theme.SetBaseTheme(viewConfiguration.GetProperty(ViewConfiguration.ThemeKey) == ViewConfiguration.LightTheme 41 | ? new MaterialDesignLightTheme() 42 | : (IBaseTheme) new MaterialDesignDarkTheme()); 43 | paletteHelper.SetTheme(theme); 44 | 45 | if (configuration == null) 46 | { 47 | ConsoleWriter.WriteLine("NO CONFIGURATION", ConsoleColor.Red); 48 | new AuthWindow().Show(); 49 | } 50 | else 51 | { 52 | ConsoleWriter.WriteLine($"configuration.Url = {configuration.Url}", ConsoleColor.Green); 53 | new MainWindow(configuration, viewConfiguration).Show(); 54 | } 55 | } 56 | 57 | private static void CallService(Configuration configuration, string service, string data) 58 | { 59 | var restClient = new HomeAssistantRestClient(configuration); 60 | var serviceSplit = service.Split('.'); 61 | if (serviceSplit.Length == 2) 62 | { 63 | restClient.CallService(serviceSplit[0], serviceSplit[1], data); 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Connection/ApiConsumer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Websocket.Client; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Connection 5 | { 6 | public class ApiConsumer 7 | { 8 | private Action Action { get; } 9 | private Predicate Condition { get; } 10 | private bool DeleteAfterRun { get; } 11 | 12 | public ApiConsumer(Predicate condition, Action action, 13 | bool deleteAfterRun = false) 14 | { 15 | Action = action; 16 | Condition = condition; 17 | DeleteAfterRun = deleteAfterRun; 18 | } 19 | 20 | public bool Consume(ResponseMessage message) 21 | { 22 | var invoke = Condition.Invoke(message); 23 | if (invoke) 24 | { 25 | Action.Invoke(message); 26 | } 27 | 28 | return invoke && DeleteAfterRun; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Connection/HaClientContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using System.Windows; 5 | using System.Windows.Threading; 6 | using Home_Assistant_Taskbar_Menu.Entities; 7 | using Home_Assistant_Taskbar_Menu.Utils; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Connection 10 | { 11 | public static class HaClientContext 12 | { 13 | private static Configuration Configuration { get; set; } 14 | private static HomeAssistantWebsocketsClient HomeAssistantWebsocketClient { get; set; } 15 | 16 | public static void Initialize(Configuration configuration) 17 | { 18 | Configuration = configuration; 19 | HomeAssistantWebsocketClient = new HomeAssistantWebsocketsClient(Configuration); 20 | } 21 | 22 | public static void Recreate() 23 | { 24 | System.Diagnostics.Process.Start(Application.ResourceAssembly.Location); 25 | Application.Current.Shutdown(); 26 | } 27 | 28 | public static async Task Start() 29 | { 30 | await HomeAssistantWebsocketClient.Start(); 31 | } 32 | 33 | public static void AddStateChangeListener(object identifier, Action listener) 34 | { 35 | HomeAssistantWebsocketClient.AddStateChangeListener(identifier, listener); 36 | } 37 | 38 | public static void RemoveStateChangeListener(object identifier) 39 | { 40 | HomeAssistantWebsocketClient.RemoveStateChangeListener(identifier); 41 | } 42 | 43 | public static void AddAuthenticationStateListener(Action listener) 44 | { 45 | HomeAssistantWebsocketClient.AddAuthenticationStateListener(listener); 46 | } 47 | 48 | public static void AddNotificationListener(Action listener) 49 | { 50 | HomeAssistantWebsocketClient.AddNotificationListener(listener); 51 | } 52 | 53 | public static void AddEntitiesListListener(Action> listener) 54 | { 55 | HomeAssistantWebsocketClient.AddEntitiesListListener(listener); 56 | } 57 | 58 | public static void CallService(Dispatcher dispatcher, Entity stateObject, string service, 59 | params Tuple[] data) 60 | { 61 | dispatcher.Invoke(() => 62 | HomeAssistantWebsocketClient.CallService( 63 | new HomeAssistantServiceCallData(service, stateObject, data))); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Connection/HomeAssistantRestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Text; 5 | using Home_Assistant_Taskbar_Menu.Utils; 6 | 7 | namespace Home_Assistant_Taskbar_Menu.Connection 8 | { 9 | public class HomeAssistantRestClient 10 | { 11 | private readonly HttpClient _httpClient; 12 | 13 | public HomeAssistantRestClient(Configuration configuration) 14 | { 15 | _httpClient = new HttpClient {BaseAddress = new Uri(configuration.HttpUrl())}; 16 | _httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {configuration.Token}"); 17 | ConsoleWriter.WriteLine($"HTTP URL: {configuration.HttpUrl()}", ConsoleColor.Green); 18 | } 19 | 20 | public void CallService(string domain, string service, string data) 21 | { 22 | ConsoleWriter.WriteLine("Calling service: ", ConsoleColor.Yellow); 23 | ConsoleWriter.Write(" Service: ", ConsoleColor.Yellow); 24 | ConsoleWriter.WriteLine($"{domain}.{service}", ConsoleColor.Blue, false); 25 | ConsoleWriter.Write(" Data: ", ConsoleColor.Yellow); 26 | ConsoleWriter.WriteLine(data, ConsoleColor.Gray, false); 27 | 28 | HttpContent content = new StringContent(data, Encoding.UTF8, "application/json"); 29 | var response = _httpClient.PostAsync($"/api/services/{domain}/{service}", 30 | content).Result; 31 | 32 | ConsoleWriter.WriteLine("Response:", ConsoleColor.Green); 33 | ConsoleWriter.Write($" Status code: ", ConsoleColor.Blue); 34 | ConsoleWriter.WriteLine(response.StatusCode.ToString(), 35 | response.StatusCode == HttpStatusCode.OK ? ConsoleColor.Green : ConsoleColor.Red, false); 36 | ConsoleWriter.Write($" Body: ", ConsoleColor.Cyan); 37 | ConsoleWriter.WriteLine(response.Content.ReadAsStringAsync().Result, ConsoleColor.Gray, false); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Connection/HomeAssistantServiceCallData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Home_Assistant_Taskbar_Menu.Entities; 4 | using Newtonsoft.Json; 5 | 6 | namespace Home_Assistant_Taskbar_Menu.Connection 7 | { 8 | public class HomeAssistantServiceCallData 9 | { 10 | [JsonProperty("domain")] public string Domain { get; } 11 | 12 | [JsonProperty("service")] public string Service { get; } 13 | 14 | [JsonProperty("service_data")] public Dictionary ServiceData { get; } 15 | 16 | public HomeAssistantServiceCallData(string domain, string service, Dictionary serviceData) 17 | { 18 | Domain = domain; 19 | Service = service; 20 | ServiceData = serviceData; 21 | } 22 | 23 | public HomeAssistantServiceCallData(string service, Entity stateObject, 24 | params Tuple[] data) 25 | { 26 | var serviceData = new Dictionary 27 | { 28 | {"entity_id", stateObject.EntityId} 29 | }; 30 | foreach (var (parameter, value) in data) 31 | { 32 | serviceData[parameter] = value; 33 | } 34 | 35 | Domain = stateObject.Domain(); 36 | Service = service; 37 | ServiceData = serviceData; 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Connection/HomeAssistantWebsocketsClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using System.Timers; 6 | using Home_Assistant_Taskbar_Menu.Entities; 7 | using Home_Assistant_Taskbar_Menu.Utils; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using Websocket.Client; 11 | 12 | namespace Home_Assistant_Taskbar_Menu.Connection 13 | { 14 | public class HomeAssistantWebsocketsClient 15 | { 16 | private readonly bool _debug = false; 17 | private readonly Timer _timer; 18 | private readonly WebsocketClient _websocketClient; 19 | private readonly string _token; 20 | private readonly List _consumers; 21 | private long _counter; 22 | private bool _authenticated; 23 | 24 | private bool Authenticated 25 | { 26 | get => _authenticated; 27 | set 28 | { 29 | _authenticatedListener?.Invoke(value); 30 | _authenticated = value; 31 | } 32 | } 33 | 34 | private Action _authenticatedListener; 35 | private readonly Dictionary> _stateChangeListeners; 36 | private readonly List> _notificationListeners; 37 | private readonly List>> _entitiesListListeners; 38 | 39 | public HomeAssistantWebsocketsClient(Configuration configuration) 40 | { 41 | _token = configuration.Token; 42 | _counter = 1; 43 | _websocketClient = new WebsocketClient(new Uri(configuration.Url)); 44 | _timer = new Timer(60 * 1000); 45 | _consumers = new List(); 46 | _stateChangeListeners = new Dictionary>(); 47 | _entitiesListListeners = new List>>(); 48 | _notificationListeners = new List>(); 49 | if (_debug) 50 | { 51 | _consumers.Add( 52 | new ApiConsumer(msg => true, 53 | msg => ConsoleWriter.WriteLine("RECEIVED: " + msg.Text, ConsoleColor.Gray))); 54 | } 55 | 56 | AuthFlow(); 57 | _websocketClient.MessageReceived.Subscribe(msg => 58 | { 59 | var toRemove = _consumers.Where(c => c.Consume(msg)).ToList(); 60 | toRemove.ForEach(c => _consumers.Remove(c)); 61 | }); 62 | _consumers.Add( 63 | new ApiConsumer( 64 | msg => 65 | { 66 | var jsonObject = JObject.Parse(msg.Text); 67 | return (string) jsonObject["type"] == "event" && 68 | (string) jsonObject["event"]["event_type"] == "state_changed"; 69 | }, 70 | msg => 71 | { 72 | Entity entity = EntityCreator.CreateFromChangedState(msg.Text); 73 | if (entity != null) 74 | { 75 | _stateChangeListeners.Values.ToList().ForEach(a => Task.Run(() => a.Invoke(entity))); 76 | } 77 | })); 78 | _consumers.Add( 79 | new ApiConsumer( 80 | msg => 81 | { 82 | var jsonObject = JObject.Parse(msg.Text); 83 | return (string) jsonObject["type"] == "event" && 84 | (string) jsonObject["event"]["event_type"] == "state_changed" && 85 | ((string) jsonObject["event"]["data"]["entity_id"]).Contains(NotificationEvent.Domain); 86 | }, 87 | msg => 88 | { 89 | NotificationEvent notificationEvent = NotificationEvent.FromJson(msg.Text); 90 | if (notificationEvent != null) 91 | { 92 | _notificationListeners.ForEach(n => Task.Run(() => n.Invoke(notificationEvent))); 93 | } 94 | })); 95 | _websocketClient.ReconnectionHappened.Subscribe(recInfo => 96 | { 97 | { 98 | ConsoleWriter.WriteLine($"RECONNECTION HAPPENED: {recInfo.Type}", ConsoleColor.Yellow); 99 | if (recInfo.Type == ReconnectionType.NoMessageReceived) 100 | { 101 | HaClientContext.Recreate(); 102 | } 103 | 104 | if (recInfo.Type != ReconnectionType.Initial) 105 | { 106 | Authenticated = false; 107 | Authenticate(); 108 | } 109 | } 110 | }); 111 | } 112 | 113 | public void AddAuthenticationStateListener(Action handler) 114 | { 115 | _authenticatedListener = handler; 116 | } 117 | 118 | public void AddStateChangeListener(object identifier, Action handler) 119 | { 120 | _stateChangeListeners.Add(identifier, handler); 121 | } 122 | 123 | public void RemoveStateChangeListener(object identifier) 124 | { 125 | _stateChangeListeners.Remove(identifier); 126 | } 127 | 128 | public void AddEntitiesListListener(Action> handler) 129 | { 130 | _entitiesListListeners.Add(handler); 131 | } 132 | 133 | public async Task Start() 134 | { 135 | ConsoleWriter.WriteLine("STARTING", ConsoleColor.Blue); 136 | await _websocketClient.Start(); 137 | _timer.Elapsed += (sender, args) => Ping(); 138 | _timer.AutoReset = true; 139 | _timer.Enabled = true; 140 | } 141 | 142 | public void Disconnect() 143 | { 144 | _websocketClient.Dispose(); 145 | } 146 | 147 | private void AuthFlow() 148 | { 149 | _consumers.Add( 150 | new ApiConsumer( 151 | msg => (string) JObject.Parse(msg.Text)["type"] == "auth_required", 152 | msg => 153 | { 154 | ConsoleWriter.WriteLine("AUTH REQUIRED", ConsoleColor.Yellow); 155 | Authenticated = false; 156 | Authenticate(); 157 | })); 158 | _consumers.Add( 159 | new ApiConsumer( 160 | msg => (string) JObject.Parse(msg.Text)["type"] == "auth_ok", 161 | msg => 162 | { 163 | ConsoleWriter.WriteLine("AUTH OK", ConsoleColor.Green); 164 | Authenticated = true; 165 | Task.Run(GetStates); 166 | SubscribeStateChange(); 167 | })); 168 | } 169 | 170 | private void Authenticate() 171 | { 172 | var authMessage = $"{{\"type\": \"auth\",\"access_token\": \"{_token}\"}}"; 173 | CallApi(authMessage); 174 | } 175 | 176 | private void SubscribeStateChange() 177 | { 178 | ConsoleWriter.WriteLine("SUBSCRIBE STATE CHANGES", ConsoleColor.Blue); 179 | var id = _counter++; 180 | var subscribeMsg = $"{{\"id\": {id},\"type\": \"subscribe_events\",\"event_type\": \"state_changed\"}}"; 181 | CallApi(subscribeMsg); 182 | } 183 | 184 | private void Ping() 185 | { 186 | if (Authenticated) 187 | { 188 | ConsoleWriter.WriteLine("PING", ConsoleColor.Blue); 189 | var id = _counter++; 190 | var subscribeMsg = $"{{\"id\": {id},\"type\": \"ping\"}}"; 191 | CallApi(subscribeMsg); 192 | } 193 | } 194 | 195 | private async Task GetStates() 196 | { 197 | ConsoleWriter.WriteLine("GETTING STATES", ConsoleColor.Blue); 198 | var id = _counter++; 199 | var subscribeMsg = $"{{\"id\": {id},\"type\": \"get_states\"}}"; 200 | await CallApi(id, subscribeMsg, msg => 201 | { 202 | List states = EntityCreator.CreateFromStateList(msg.Text); 203 | states.Sort((o1, o2) => string.Compare(o1.EntityId, o2.EntityId, StringComparison.Ordinal)); 204 | Task.Run(() => _entitiesListListeners.ForEach(l => l.Invoke(states))); 205 | }); 206 | } 207 | 208 | private async Task CallApi(long id, string message, Action resultHandler = null, 209 | bool waitFotAuth = true) 210 | { 211 | while (!Authenticated & waitFotAuth) 212 | { 213 | await Task.Delay(10); 214 | } 215 | 216 | if (!Authenticated) 217 | { 218 | return; 219 | } 220 | 221 | await Task.Run(() => CallApi(message)); 222 | if (resultHandler != null) 223 | { 224 | var tuple = new ApiConsumer( 225 | msg => 226 | { 227 | var jsonObject = JObject.Parse(msg.Text); 228 | return (string) jsonObject["type"] == "result" && 229 | (int) jsonObject["id"] == id; 230 | }, 231 | resultHandler, true); 232 | _consumers.Add(tuple); 233 | } 234 | } 235 | 236 | private void CallApi(string message) 237 | { 238 | if (_debug) 239 | { 240 | ConsoleWriter.WriteLine("SENT: " + message, ConsoleColor.Gray); 241 | } 242 | 243 | _websocketClient.Send(message); 244 | } 245 | 246 | public void CallService(HomeAssistantServiceCallData serviceCallData) 247 | { 248 | var id = _counter++; 249 | JObject serviceCallObject = JObject.FromObject(serviceCallData); 250 | serviceCallObject["id"] = id; 251 | serviceCallObject["type"] = "call_service"; 252 | var json = JsonConvert.SerializeObject(serviceCallObject); 253 | ConsoleWriter.WriteLine($"CALLING SERVICE: {json}", ConsoleColor.Blue); 254 | CallApi(json); 255 | } 256 | 257 | public void AddNotificationListener(Action listener) 258 | { 259 | _notificationListeners.Add(listener); 260 | } 261 | } 262 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/AutomationEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Threading; 6 | using Home_Assistant_Taskbar_Menu.Connection; 7 | using MaterialDesignThemes.Wpf; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Entities 10 | { 11 | public class AutomationEntity : Entity 12 | { 13 | public const string DomainName = "automation"; 14 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 15 | 16 | public override string Domain() 17 | { 18 | return DomainName; 19 | } 20 | 21 | protected override List OffStates() 22 | { 23 | return OffStatesList; 24 | } 25 | 26 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 27 | { 28 | var root = new MenuItem 29 | { 30 | Header = GetName(name), 31 | StaysOpenOnClick = true, 32 | IsEnabled = IsAvailable() 33 | }; 34 | if (IsOn()) 35 | { 36 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 37 | } 38 | 39 | root.PreviewMouseDown += (sender, args) => 40 | { 41 | if (args.ChangedButton == MouseButton.Right) 42 | { 43 | args.Handled = ToggleIfPossible(dispatcher); 44 | } 45 | }; 46 | 47 | new List> 48 | { 49 | Tuple.Create("turn_on", "Turn On"), 50 | Tuple.Create("turn_off", "Turn Off"), 51 | Tuple.Create("trigger", "Trigger") 52 | }.ForEach(t => root.Items.Add(CreateMenuItem(dispatcher, t.Item1, t.Item2))); 53 | return root; 54 | } 55 | 56 | public override bool ToggleIfPossible(Dispatcher dispatcher) 57 | { 58 | HaClientContext.CallService(dispatcher, this, "toggle"); 59 | return true; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/ButtonEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Threading; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class ButtonEntity : Entity 7 | { 8 | public const string DomainName = "button"; 9 | 10 | public override string Domain() 11 | { 12 | return DomainName; 13 | } 14 | 15 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 16 | { 17 | return CreateMenuItem(dispatcher, "press", GetName(name), isEnabled: IsAvailable()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/ClimateEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class ClimateEntity : Entity 8 | { 9 | public const string DomainName = "climate"; 10 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 11 | 12 | public override string Domain() 13 | { 14 | return DomainName; 15 | } 16 | 17 | protected override List OffStates() 18 | { 19 | return OffStatesList; 20 | } 21 | 22 | protected override List AllSupportedFeatures() 23 | { 24 | return SupportedFeatures.All; 25 | } 26 | 27 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 28 | { 29 | var isOn = IsOn(); 30 | return CreateMenuItem(dispatcher, isOn ? "turn_off" : "turn_on", GetName(name), isOn, 31 | IsAvailable()); 32 | } 33 | 34 | private static class SupportedFeatures 35 | { 36 | private const int TargetTemperature = 1; 37 | private const int TargetTemperatureRange = 2; 38 | private const int TargetHumidity = 4; 39 | private const int FanMode = 8; 40 | private const int PresetMode = 16; 41 | private const int SwingMode = 32; 42 | private const int AuxHeat = 64; 43 | 44 | public static readonly List All = new List 45 | { 46 | TargetTemperature, TargetTemperatureRange, TargetHumidity, FanMode, PresetMode, SwingMode, AuxHeat 47 | }; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/CoverEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Input; 4 | using System.Windows.Threading; 5 | using Home_Assistant_Taskbar_Menu.Connection; 6 | using MaterialDesignThemes.Wpf; 7 | 8 | namespace Home_Assistant_Taskbar_Menu.Entities 9 | { 10 | public class CoverEntity : Entity 11 | { 12 | public const string DomainName = "cover"; 13 | private static readonly List OffStatesList = new List {States.Closed, States.Unavailable}; 14 | 15 | public override string Domain() 16 | { 17 | return DomainName; 18 | } 19 | 20 | protected override List OffStates() 21 | { 22 | return OffStatesList; 23 | } 24 | 25 | protected override List AllSupportedFeatures() 26 | { 27 | return SupportedFeatures.All; 28 | } 29 | 30 | protected override Dictionary FeatureToServiceMap() 31 | { 32 | return SupportedFeatures.ServiceMap; 33 | } 34 | 35 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 36 | { 37 | var root = new MenuItem 38 | { 39 | Header = GetName(name), 40 | StaysOpenOnClick = true, 41 | IsEnabled = IsAvailable() 42 | }; 43 | if (IsOn()) 44 | { 45 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 46 | } 47 | 48 | var features = GetSupportedFeatures(); 49 | if (new HashSet {SupportedFeatures.Open, SupportedFeatures.Close}.SetEquals(features)) 50 | { 51 | root.Click += (sender, args) => { HaClientContext.CallService(dispatcher, this, "toggle"); }; 52 | } 53 | else if (new HashSet {SupportedFeatures.OpenTilt, SupportedFeatures.CloseTilt}.SetEquals(features)) 54 | { 55 | root.Click += (sender, args) => { HaClientContext.CallService(dispatcher, this, "toggle_cover_tilt"); }; 56 | } 57 | else 58 | { 59 | root.PreviewMouseDown += (sender, args) => 60 | { 61 | if (args.ChangedButton == MouseButton.Right) 62 | { 63 | args.Handled = ToggleIfPossible(dispatcher); 64 | } 65 | }; 66 | 67 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Open); 68 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Close); 69 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Stop); 70 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.SetPosition, 0, 100, 71 | GetDoubleAttribute("current_position"), "position"); 72 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.OpenTilt); 73 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.CloseTilt); 74 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.StopTilt); 75 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.SetTiltPosition, 0, 100, 76 | GetDoubleAttribute("current_tilt_position"), "tilt_position"); 77 | } 78 | 79 | return root; 80 | } 81 | 82 | private static class SupportedFeatures 83 | { 84 | public const int Open = 1; 85 | public const int Close = 2; 86 | public const int SetPosition = 4; 87 | public const int Stop = 8; 88 | public const int OpenTilt = 16; 89 | public const int CloseTilt = 32; 90 | public const int StopTilt = 64; 91 | public const int SetTiltPosition = 128; 92 | 93 | public static readonly List All = new List 94 | { 95 | Open, Close, Stop, SetPosition, OpenTilt, CloseTilt, StopTilt, SetTiltPosition 96 | }; 97 | 98 | public static readonly Dictionary ServiceMap = 99 | new Dictionary 100 | { 101 | {Open, (service: "open_cover", header: "Open")}, 102 | {Close, (service: "close_cover", header: "Close")}, 103 | {Stop, (service: "stop_cover", header: "Stop")}, 104 | {SetPosition, (service: "set_cover_position", header: "Position")}, 105 | {OpenTilt, (service: "open_cover_tilt", header: "Open Tilt")}, 106 | {CloseTilt, (service: "close_cover_tilt", header: "Close Tilt")}, 107 | {StopTilt, (service: "stop_cover_tilt", header: "Stop Tilt")}, 108 | {SetTiltPosition, (service: "set_cover_tilt_position", header: "Tilt Position")} 109 | }; 110 | } 111 | 112 | public override bool ToggleIfPossible(Dispatcher dispatcher) 113 | { 114 | if (IsSupported(SupportedFeatures.Open, SupportedFeatures.Close)) 115 | { 116 | HaClientContext.CallService(dispatcher, this, "toggle"); 117 | return true; 118 | } 119 | 120 | if (IsSupported(SupportedFeatures.OpenTilt, SupportedFeatures.CloseTilt)) 121 | { 122 | HaClientContext.CallService(dispatcher, this, "toggle"); 123 | return true; 124 | } 125 | 126 | return false; 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Windows.Controls; 6 | using System.Windows.Threading; 7 | using Home_Assistant_Taskbar_Menu.Connection; 8 | using Home_Assistant_Taskbar_Menu.Utils; 9 | using MaterialDesignThemes.Wpf; 10 | using Newtonsoft.Json; 11 | using Newtonsoft.Json.Linq; 12 | 13 | namespace Home_Assistant_Taskbar_Menu.Entities 14 | { 15 | public abstract class Entity 16 | { 17 | [JsonProperty("entity_id")] public string EntityId { get; set; } 18 | 19 | public string State { get; set; } 20 | 21 | public Dictionary Attributes { protected get; set; } 22 | 23 | public string GetAttribute(string name) 24 | { 25 | return Attributes.ContainsKey(name) ? Attributes[name]?.ToString() : null; 26 | } 27 | 28 | public bool IsOn() 29 | { 30 | return !OffStates().Contains(State) && OffStates().Count != 0; 31 | } 32 | 33 | public bool IsAvailable() 34 | { 35 | return State != States.Unavailable; 36 | } 37 | 38 | public string GetName(string nameOverride = null) 39 | { 40 | return (!string.IsNullOrEmpty(nameOverride) 41 | ? nameOverride 42 | : GetAttribute("friendly_name") 43 | ?? EntityId).Replace("_", "__"); 44 | } 45 | 46 | public abstract string Domain(); 47 | 48 | public MenuItem ToMenuItemSafe(Dispatcher dispatcher, string name) 49 | { 50 | try 51 | { 52 | return ToMenuItem(dispatcher, name); 53 | } 54 | catch (Exception) 55 | { 56 | ConsoleWriter.WriteLine($"ERROR CREATING UI FOR ENTITY: {EntityId}", ConsoleColor.Red); 57 | return new MenuItem {Header = $"ERROR: {EntityId.Replace("_", "__")}", IsEnabled = false}; 58 | } 59 | } 60 | 61 | protected bool GetBoolAttribute(string name) 62 | { 63 | return bool.Parse(GetAttribute(name) ?? "false"); 64 | } 65 | 66 | protected int GetIntAttribute(string name, int defaultValue = 0) 67 | { 68 | return ParseInt(GetAttribute(name)); 69 | } 70 | 71 | protected double GetDoubleAttribute(string name) 72 | { 73 | return ParseDouble(GetAttribute(name)); 74 | } 75 | 76 | protected double ParseDouble(string value) 77 | { 78 | return value?.Length == 0 79 | ? 0 80 | : double.Parse(value?.Replace(",", ".") ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture); 81 | } 82 | 83 | protected int ParseInt(string value) 84 | { 85 | return value?.Length == 0 86 | ? 0 87 | : int.Parse(value?.Replace(",", ".") ?? "0", NumberStyles.Any, CultureInfo.InvariantCulture); 88 | } 89 | 90 | protected List GetListAttribute(string name) 91 | { 92 | return Attributes.ContainsKey(name) 93 | ? ((JArray) Attributes[name]).Select(i => (string) i).ToList() 94 | : new List(); 95 | } 96 | 97 | protected string GetListAttribute(string name, int index) 98 | { 99 | var attribute = GetListAttribute(name); 100 | return attribute.Count > index ? attribute[index] : ""; 101 | } 102 | 103 | protected virtual List OffStates() 104 | { 105 | return new List(); 106 | } 107 | 108 | protected virtual List AllSupportedFeatures() 109 | { 110 | return new List(); 111 | } 112 | 113 | protected virtual Dictionary FeatureToServiceMap() 114 | { 115 | return new Dictionary(); 116 | } 117 | 118 | protected virtual List GetSupportedFeatures() 119 | { 120 | var supportedFeatures = GetIntAttribute("supported_features"); 121 | return AllSupportedFeatures() 122 | .Where(sf => (sf & supportedFeatures) > 0) 123 | .ToList(); 124 | } 125 | 126 | protected abstract MenuItem ToMenuItem(Dispatcher dispatcher, string name); 127 | 128 | public virtual bool ToggleIfPossible(Dispatcher dispatcher) 129 | { 130 | return false; 131 | } 132 | 133 | protected bool IsSupported(params int[] supportedFeatures) 134 | { 135 | return supportedFeatures.ToList() 136 | .TrueForAll(supportedFeature => GetSupportedFeatures().Contains(supportedFeature)); 137 | } 138 | 139 | protected void AddMenuItemIfSupported(Dispatcher dispatcher, ItemsControl root, int supportedFeature) 140 | { 141 | var featureToServiceMap = FeatureToServiceMap(); 142 | if (IsSupported(supportedFeature) && featureToServiceMap.ContainsKey(supportedFeature)) 143 | { 144 | root.Items.Add(CreateMenuItem(dispatcher, featureToServiceMap[supportedFeature])); 145 | } 146 | } 147 | 148 | protected MenuItem CreateMenuItem(Dispatcher dispatcher, (string service, string header) featureToService) 149 | { 150 | return CreateMenuItem(dispatcher, featureToService.service, featureToService.header); 151 | } 152 | 153 | protected MenuItem CreateMenuItem(Dispatcher dispatcher, string service, string header, bool isChecked = false, 154 | bool isEnabled = true, string toolTip = null, params Tuple[] data) 155 | { 156 | var serviceItem = new MenuItem {Header = header, ToolTip = toolTip, StaysOpenOnClick = true}; 157 | if (isChecked) 158 | { 159 | serviceItem.Icon = new PackIcon {Kind = PackIconKind.Tick}; 160 | } 161 | 162 | serviceItem.Click += (sender, args) => { HaClientContext.CallService(dispatcher, this, service, data); }; 163 | return serviceItem; 164 | } 165 | 166 | protected Slider CreateSlider(Dispatcher dispatcher, double min, double max, double value, string service, 167 | string toolTip, string attribute, double step = 1, Action changer = null, 168 | Func converter = null) 169 | { 170 | var slider = new Slider 171 | { 172 | Minimum = min, Maximum = max, MinWidth = 100, ToolTip = toolTip, Value = value, 173 | IsSnapToTickEnabled = true, 174 | TickFrequency = step, 175 | IsMoveToPointEnabled = true 176 | }; 177 | slider.PreviewMouseUp += (sender, args) => 178 | { 179 | if (converter == null) 180 | { 181 | HaClientContext.CallService(dispatcher, this, service, 182 | Tuple.Create(attribute, step == 1 ? (int) slider.Value : slider.Value)); 183 | } 184 | else 185 | { 186 | HaClientContext.CallService(dispatcher, this, service, 187 | Tuple.Create(attribute, converter.Invoke(slider.Value))); 188 | } 189 | }; 190 | 191 | if (changer != null) 192 | { 193 | slider.ValueChanged += (sender, args) => changer.Invoke(slider, args.NewValue); 194 | changer.Invoke(slider, value); 195 | } 196 | 197 | return slider; 198 | } 199 | 200 | protected void AddSliderIfSupported(Dispatcher dispatcher, ItemsControl root, int supportedFeature, double min, 201 | double max, double value, string attribute, double step = 1, Action changer = null, 202 | Func converter = null) 203 | { 204 | var supportedFeatures = GetSupportedFeatures(); 205 | var featureToServiceMap = FeatureToServiceMap(); 206 | if (supportedFeatures.Contains(supportedFeature) && featureToServiceMap.ContainsKey(supportedFeature)) 207 | { 208 | var (service, header) = featureToServiceMap[supportedFeature]; 209 | root.Items.Add(CreateSlider(dispatcher, min, max, value, service, header, attribute, step, changer, 210 | converter)); 211 | } 212 | } 213 | 214 | protected static class States 215 | { 216 | public const string Unavailable = "unavailable"; 217 | public const string On = "on"; 218 | public const string Off = "off"; 219 | public const string Open = "open"; 220 | public const string Closed = "closed"; 221 | public const string Idle = "idle"; 222 | public const string Docked = "docked"; 223 | } 224 | 225 | public override string ToString() 226 | { 227 | var fName = GetAttribute("friendly_name"); 228 | return fName != null ? $"{fName} ({EntityId})" : EntityId; 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/FanEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Threading; 6 | using Home_Assistant_Taskbar_Menu.Connection; 7 | using MaterialDesignThemes.Wpf; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Entities 10 | { 11 | public class FanEntity : Entity 12 | { 13 | public const string DomainName = "fan"; 14 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 15 | 16 | public override string Domain() 17 | { 18 | return DomainName; 19 | } 20 | 21 | protected override List OffStates() 22 | { 23 | return OffStatesList; 24 | } 25 | 26 | protected override List AllSupportedFeatures() 27 | { 28 | return SupportedFeatures.All; 29 | } 30 | 31 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 32 | { 33 | var root = new MenuItem 34 | { 35 | Header = GetName(name), 36 | StaysOpenOnClick = true, 37 | IsEnabled = IsAvailable() 38 | }; 39 | if (IsOn()) 40 | { 41 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 42 | } 43 | 44 | var features = GetSupportedFeatures(); 45 | if (features.Count == 0) 46 | { 47 | root.Click += (sender, args) => { HaClientContext.CallService(dispatcher, this, "toggle"); }; 48 | } 49 | else 50 | { 51 | root.PreviewMouseDown += (sender, args) => 52 | { 53 | if (args.ChangedButton == MouseButton.Right) 54 | { 55 | args.Handled = ToggleIfPossible(dispatcher); 56 | } 57 | }; 58 | 59 | root.Items.Add(CreateMenuItem(dispatcher, "turn_on", "Turn On")); 60 | root.Items.Add(CreateMenuItem(dispatcher, "turn_off", "Turn Off")); 61 | if (features.Contains(SupportedFeatures.SetSpeed)) 62 | { 63 | var currentSpeed = GetAttribute("speed"); 64 | var speedRootItem = new MenuItem {Header = "Set Speed", StaysOpenOnClick = true}; 65 | GetListAttribute("speed_list") 66 | .ForEach(speedValue => 67 | speedRootItem.Items.Add( 68 | CreateMenuItem(dispatcher, "set_speed", speedValue, speedValue == currentSpeed, 69 | data: Tuple.Create("speed", speedValue)))); 70 | root.Items.Add(speedRootItem); 71 | } 72 | 73 | if (features.Contains(SupportedFeatures.Oscillate)) 74 | { 75 | var oscillating = GetBoolAttribute("oscillating"); 76 | root.Items.Add(CreateMenuItem(dispatcher, "oscillate", "Oscillating", oscillating, 77 | data: Tuple.Create("oscillating", !oscillating))); 78 | } 79 | 80 | if (features.Contains(SupportedFeatures.Direction)) 81 | { 82 | var currentDirection = GetAttribute("speed"); 83 | var directionsItem = new MenuItem {Header = "Set Direction", StaysOpenOnClick = true}; 84 | new List> 85 | {Tuple.Create("forward", "Forward"), Tuple.Create("reverse", "Reverse")} 86 | .ForEach(t => 87 | directionsItem.Items.Add( 88 | CreateMenuItem(dispatcher, "set_direction", t.Item2, t.Item1 == currentDirection, 89 | data: Tuple.Create("direction", t.Item1)))); 90 | root.Items.Add(directionsItem); 91 | } 92 | } 93 | 94 | return root; 95 | } 96 | 97 | public override bool ToggleIfPossible(Dispatcher dispatcher) 98 | { 99 | HaClientContext.CallService(dispatcher, this, "toggle"); 100 | return true; 101 | } 102 | 103 | private static class SupportedFeatures 104 | { 105 | public const int SetSpeed = 1; 106 | public const int Oscillate = 2; 107 | public const int Direction = 4; 108 | 109 | public static List All = new List 110 | { 111 | SetSpeed, Oscillate, Direction 112 | }; 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/InputBooleanEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class InputBooleanEntity : Entity 8 | { 9 | public const string DomainName = "input_boolean"; 10 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 11 | 12 | public override string Domain() 13 | { 14 | return DomainName; 15 | } 16 | 17 | protected override List OffStates() 18 | { 19 | return OffStatesList; 20 | } 21 | 22 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 23 | { 24 | return CreateMenuItem(dispatcher, "toggle", GetName(name), IsOn(), IsAvailable()); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/InputButton.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Threading; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class InputButton : Entity 7 | { 8 | public const string DomainName = "input_button"; 9 | 10 | public override string Domain() 11 | { 12 | return DomainName; 13 | } 14 | 15 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 16 | { 17 | return CreateMenuItem(dispatcher, "press", GetName(name), isEnabled: IsAvailable()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/InputNumberEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Threading; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class InputNumberEntity : Entity 7 | { 8 | public const string DomainName = "input_number"; 9 | 10 | public override string Domain() 11 | { 12 | return DomainName; 13 | } 14 | 15 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 16 | { 17 | var root = new MenuItem 18 | { 19 | Header = GetName(name), 20 | StaysOpenOnClick = true, 21 | IsEnabled = IsAvailable() 22 | }; 23 | var min = GetDoubleAttribute("min"); 24 | var value = ParseDouble(State); 25 | var max = GetDoubleAttribute("max"); 26 | var step = GetDoubleAttribute("step"); 27 | root.Items.Add(CreateSlider(dispatcher, min, max, value, "set_value", "Set Value", "value", step)); 28 | root.Items.Add(CreateMenuItem(dispatcher, "increment", "Increment")); 29 | root.Items.Add(CreateMenuItem(dispatcher, "decrement", "Decrement")); 30 | return root; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/InputSelectEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class InputSelectEntity : Entity 8 | { 9 | public const string DomainName = "input_select"; 10 | 11 | public override string Domain() 12 | { 13 | return DomainName; 14 | } 15 | 16 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 17 | { 18 | var root = new MenuItem 19 | { 20 | Header = GetName(name), 21 | StaysOpenOnClick = true, 22 | IsEnabled = IsAvailable() 23 | }; 24 | GetListAttribute("options").ForEach(option => 25 | root.Items.Add(CreateMenuItem(dispatcher, "select_option", option, State == option, 26 | data: Tuple.Create("option", option)))); 27 | return root; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/LightEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Windows.Controls; 5 | using System.Windows.Input; 6 | using System.Windows.Media; 7 | using System.Windows.Threading; 8 | using Home_Assistant_Taskbar_Menu.Connection; 9 | using MaterialDesignThemes.Wpf; 10 | 11 | namespace Home_Assistant_Taskbar_Menu.Entities 12 | { 13 | public class LightEntity : Entity 14 | { 15 | public const string DomainName = "light"; 16 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 17 | 18 | public override string Domain() 19 | { 20 | return DomainName; 21 | } 22 | 23 | protected override List OffStates() 24 | { 25 | return OffStatesList; 26 | } 27 | 28 | protected override List AllSupportedFeatures() 29 | { 30 | return SupportedFeatures.All; 31 | } 32 | 33 | protected override Dictionary FeatureToServiceMap() 34 | { 35 | return SupportedFeatures.ServiceMap; 36 | } 37 | 38 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 39 | { 40 | var root = new MenuItem 41 | { 42 | Header = GetName(name), 43 | StaysOpenOnClick = true, 44 | IsEnabled = IsAvailable() 45 | }; 46 | if (IsOn()) 47 | { 48 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 49 | } 50 | 51 | if (GetSupportedFeatures().Count == 0 || 52 | GetSupportedModes().Equals(new HashSet {SupportedColorModes.OnOff})) 53 | { 54 | root.Click += (sender, args) => HaClientContext.CallService(dispatcher, this, "toggle"); 55 | } 56 | else 57 | { 58 | root.PreviewMouseDown += (sender, args) => 59 | { 60 | if (args.ChangedButton == MouseButton.Right) 61 | { 62 | args.Handled = ToggleIfPossible(dispatcher); 63 | } 64 | }; 65 | root.Items.Add(CreateMenuItem(dispatcher, "turn_on", "Turn On")); 66 | root.Items.Add(CreateMenuItem(dispatcher, "turn_off", "Turn Off")); 67 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.Brightness, 0, 255, 68 | GetIntAttribute("brightness"), "brightness"); 69 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.ColorTemp, GetIntAttribute("min_mireds"), 70 | GetIntAttribute("max_mireds"), GetIntAttribute("color_temp", GetIntAttribute("min_mireds")), 71 | "color_temp", 72 | changer: (slider, value) => slider.Foreground = new SolidColorBrush(FromMireds(slider, value))); 73 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.WhiteValue, 0, 255, 74 | GetIntAttribute("white_value"), "white_value"); 75 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.Color, 0, 360, 76 | ParseDouble(GetListAttribute("hs_color", 0)), "hs_color", 1, 77 | (slider, value) => slider.Foreground = new SolidColorBrush(FromHue(value)), 78 | converter: value => new[] {(int) value, 100}); 79 | if (IsSupported(SupportedFeatures.Effect)) 80 | { 81 | var effectItem = new MenuItem {Header = "Effect", StaysOpenOnClick = true}; 82 | var currentEffect = GetAttribute("effect"); 83 | GetListAttribute("effect_list").ForEach(effect => 84 | { 85 | effectItem.Items.Add(CreateMenuItem(dispatcher, "turn_on", effect, 86 | effect == currentEffect, 87 | data: Tuple.Create("effect", effect))); 88 | }); 89 | root.Items.Add(effectItem); 90 | } 91 | } 92 | 93 | return root; 94 | } 95 | 96 | public override bool ToggleIfPossible(Dispatcher dispatcher) 97 | { 98 | HaClientContext.CallService(dispatcher, this, "toggle"); 99 | return true; 100 | } 101 | 102 | protected override List GetSupportedFeatures() 103 | { 104 | List supportedFeatures = new List(base.GetSupportedFeatures()); 105 | HashSet supportedModes = GetSupportedModes(); 106 | if (!supportedFeatures.Contains(SupportedFeatures.Brightness) && 107 | supportedModes.Intersect(SupportedColorModes.BrightnessModes).Any()) 108 | { 109 | supportedFeatures.Add(SupportedFeatures.Brightness); 110 | } 111 | 112 | if (!supportedFeatures.Contains(SupportedFeatures.Color) && 113 | supportedModes.Intersect(SupportedColorModes.ColorModes).Any()) 114 | { 115 | supportedFeatures.Add(SupportedFeatures.Color); 116 | } 117 | 118 | if (!supportedFeatures.Contains(SupportedFeatures.ColorTemp) && 119 | supportedModes.Contains(SupportedColorModes.ColorTemp)) 120 | { 121 | supportedFeatures.Add(SupportedFeatures.ColorTemp); 122 | } 123 | 124 | if (!supportedFeatures.Contains(SupportedFeatures.WhiteValue) && 125 | supportedModes.Contains(SupportedColorModes.White)) 126 | { 127 | supportedFeatures.Add(SupportedFeatures.WhiteValue); 128 | } 129 | 130 | return supportedFeatures; 131 | } 132 | 133 | private HashSet GetSupportedModes() 134 | { 135 | return new HashSet(GetListAttribute("supported_color_modes")); 136 | } 137 | 138 | private static Color FromHue(double hue) 139 | { 140 | int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6; 141 | double f = hue / 60 - Math.Floor(hue / 60); 142 | 143 | byte v = 255; 144 | byte p = 0; 145 | byte q = (byte) (255 * (1 - f)); 146 | byte t = (byte) (255 * (1 - (1 - f))); 147 | 148 | switch (hi) 149 | { 150 | case 0: 151 | return Color.FromArgb(255, v, t, p); 152 | case 1: 153 | return Color.FromArgb(255, q, v, p); 154 | case 2: 155 | return Color.FromArgb(255, p, v, t); 156 | case 3: 157 | return Color.FromArgb(255, p, q, v); 158 | case 4: 159 | return Color.FromArgb(255, t, p, v); 160 | default: 161 | return Color.FromArgb(255, v, p, q); 162 | } 163 | } 164 | 165 | private static Color FromMireds(Slider slider, double mireds) 166 | { 167 | double percent = (mireds - slider.Minimum) / (slider.Maximum - slider.Minimum); 168 | if (percent >= 0.5) 169 | { 170 | percent = 2 * (percent - 0.5); 171 | return Color.Add(Color.Multiply(Colors.White, (float) (1 - percent)), 172 | Color.Multiply(Color.FromRgb(255, 160, 0), (float) percent)); 173 | } 174 | 175 | percent = 2 * percent; 176 | return Color.Add(Color.Multiply(Color.FromRgb(166, 209, 255), (float) (1 - percent)), 177 | Color.Multiply(Colors.White, (float) percent)); 178 | } 179 | 180 | private static class SupportedFeatures 181 | { 182 | public const int Brightness = 1; 183 | public const int ColorTemp = 2; 184 | public const int Effect = 4; 185 | public const int Flash = 8; 186 | public const int Color = 16; 187 | public const int Transition = 32; 188 | public const int WhiteValue = 128; 189 | 190 | public static readonly List All = new List 191 | { 192 | Brightness, ColorTemp, WhiteValue, Color, Effect 193 | }; 194 | 195 | public static Dictionary ServiceMap = 196 | new Dictionary 197 | { 198 | {Brightness, (service: "turn_on", header: "Brightness")}, 199 | {ColorTemp, (service: "turn_on", header: "Color Temperature")}, 200 | {Color, (service: "turn_on", header: "Color")}, 201 | {WhiteValue, (service: "turn_on", header: "White Value")} 202 | }; 203 | } 204 | 205 | private static class SupportedColorModes 206 | { 207 | public const string Unknown = "unknown"; 208 | public const string OnOff = "onoff"; 209 | public const string Brightness = "brightness"; 210 | public const string ColorTemp = "color_temp"; 211 | public const string Hs = "hs"; 212 | public const string Xy = "xy"; 213 | public const string Rgb = "rgb"; 214 | public const string Rgbw = "rgbw"; 215 | public const string Rgbww = "rgbww"; 216 | public const string White = "white"; 217 | 218 | public static readonly HashSet All = new HashSet 219 | {OnOff, Brightness, ColorTemp, Hs, Xy, Rgb, Rgbw, Rgbww, White}; 220 | 221 | public static readonly HashSet ColorModes = new HashSet {Hs, Xy, Rgb, Rgbw, Rgbww}; 222 | 223 | public static readonly HashSet BrightnessModes = new HashSet 224 | {Brightness, ColorTemp, Hs, Xy, Rgb, Rgbw, Rgbww, White}; 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/LockEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | using MaterialDesignThemes.Wpf; 5 | 6 | namespace Home_Assistant_Taskbar_Menu.Entities 7 | { 8 | public class LockEntity : Entity 9 | { 10 | public const string DomainName = "lock"; 11 | private static readonly List OffStatesList = new List {States.Closed, States.Unavailable}; 12 | 13 | public override string Domain() 14 | { 15 | return DomainName; 16 | } 17 | 18 | protected override List OffStates() 19 | { 20 | return OffStatesList; 21 | } 22 | 23 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 24 | { 25 | var root = new MenuItem 26 | { 27 | Header = GetName(name), 28 | StaysOpenOnClick = true, 29 | IsEnabled = IsAvailable() 30 | }; 31 | if (IsOn()) 32 | { 33 | root.Icon = new PackIcon { Kind = PackIconKind.Tick }; 34 | } 35 | new List<(string service, string header)> 36 | { 37 | (service: "lock", header: "Lock"), 38 | (service: "unlock", header: "Unlock") 39 | }.ForEach(t => root.Items.Add(CreateMenuItem(dispatcher, t))); 40 | return root; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/MediaPlayerEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Threading; 6 | using Home_Assistant_Taskbar_Menu.Connection; 7 | using MaterialDesignThemes.Wpf; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Entities 10 | { 11 | public class MediaPlayerEntity : Entity 12 | { 13 | public const string DomainName = "media_player"; 14 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 15 | 16 | public override string Domain() 17 | { 18 | return DomainName; 19 | } 20 | 21 | protected override List OffStates() 22 | { 23 | return OffStatesList; 24 | } 25 | 26 | protected override List AllSupportedFeatures() 27 | { 28 | return SupportedFeatures.All; 29 | } 30 | 31 | protected override Dictionary FeatureToServiceMap() 32 | { 33 | return SupportedFeatures.ServiceMap; 34 | } 35 | 36 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 37 | { 38 | var root = new MenuItem 39 | { 40 | Header = GetName(name), 41 | StaysOpenOnClick = true, 42 | IsEnabled = IsAvailable() 43 | }; 44 | if (IsOn()) 45 | { 46 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 47 | } 48 | 49 | root.PreviewMouseDown += (sender, args) => 50 | { 51 | if (args.ChangedButton == MouseButton.Right) 52 | { 53 | args.Handled = ToggleIfPossible(dispatcher); 54 | } 55 | }; 56 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.TurnOn); 57 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.TurnOff); 58 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Play); 59 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Pause); 60 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.Stop); 61 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.NextTrack); 62 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.PreviousTrack); 63 | if (IsSupported(SupportedFeatures.VolumeMute)) 64 | { 65 | root.Items.Add(CreateMenuItem(dispatcher, "volume_mute", "Mute", GetBoolAttribute("is_volume_muted"), 66 | data: Tuple.Create("is_volume_muted", !GetBoolAttribute("is_volume_muted")))); 67 | } 68 | 69 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.VolumeSet, 0, 1, 70 | GetDoubleAttribute("volume_level"), "volume_level", 0.01); 71 | if (IsSupported(SupportedFeatures.VolumeStep)) 72 | { 73 | root.Items.Add(CreateMenuItem(dispatcher, "volume_up", "Volume Up")); 74 | root.Items.Add(CreateMenuItem(dispatcher, "volume_down", "Volume Down")); 75 | } 76 | 77 | AddSliderIfSupported(dispatcher, root, SupportedFeatures.Seek, 0, GetDoubleAttribute("media_duration"), 78 | GetDoubleAttribute("media_position"), "seek_position"); 79 | if (IsSupported(SupportedFeatures.SelectSource)) 80 | { 81 | var currentSource = GetAttribute("source"); 82 | var sources = GetListAttribute("source_list"); 83 | var sourcesRoot = new MenuItem {Header = "Source", StaysOpenOnClick = true}; 84 | sources.ForEach(source => sourcesRoot.Items.Add(CreateMenuItem(dispatcher, "select_source", source, 85 | currentSource == source, data: Tuple.Create("source", source)))); 86 | root.Items.Add(sourcesRoot); 87 | } 88 | 89 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.ClearPlaylist); 90 | if (IsSupported(SupportedFeatures.ShuffleSet)) 91 | { 92 | root.Items.Add(CreateMenuItem(dispatcher, "shuffle_set", "Shuffle", GetBoolAttribute("shuffle"), 93 | data: Tuple.Create("shuffle", !GetBoolAttribute("shuffle")))); 94 | } 95 | 96 | AddMenuItemIfSupported(dispatcher, root, SupportedFeatures.SelectSoundMode); 97 | if (IsSupported(SupportedFeatures.SelectSoundMode)) 98 | { 99 | var currentSoundMode = GetAttribute("sound_mode"); 100 | var soundModes = GetListAttribute("sound_mode_list"); 101 | var soundModesRoot = new MenuItem {Header = "Sound Mode", StaysOpenOnClick = true}; 102 | soundModes.ForEach(soundMode => soundModesRoot.Items.Add(CreateMenuItem(dispatcher, "select_sound_mode", 103 | soundMode, currentSoundMode == soundMode, 104 | data: Tuple.Create("sound_mode", soundMode)))); 105 | root.Items.Add(soundModesRoot); 106 | } 107 | 108 | return root; 109 | } 110 | 111 | public override bool ToggleIfPossible(Dispatcher dispatcher) 112 | { 113 | HaClientContext.CallService(dispatcher, this, "toggle"); 114 | return true; 115 | } 116 | 117 | private static class SupportedFeatures 118 | { 119 | public const int Pause = 1; 120 | public const int Seek = 2; 121 | public const int VolumeSet = 4; 122 | public const int VolumeMute = 8; 123 | public const int PreviousTrack = 16; 124 | public const int NextTrack = 32; 125 | public const int TurnOn = 128; 126 | public const int TurnOff = 256; 127 | public const int PlayMedia = 512; 128 | public const int VolumeStep = 1024; 129 | public const int SelectSource = 2048; 130 | public const int Stop = 4096; 131 | public const int ClearPlaylist = 8192; 132 | public const int Play = 16384; 133 | public const int ShuffleSet = 32768; 134 | public const int SelectSoundMode = 65536; 135 | 136 | public static readonly List All = new List 137 | { 138 | TurnOn, TurnOff, Play, Pause, Stop, NextTrack, PreviousTrack, VolumeMute, VolumeSet, VolumeStep, Seek, 139 | SelectSource, ClearPlaylist, ShuffleSet, PlayMedia, SelectSoundMode 140 | }; 141 | 142 | public static readonly Dictionary ServiceMap = 143 | new Dictionary 144 | { 145 | {TurnOn, (service: "turn_on", header: "Turn On")}, 146 | {TurnOff, (service: "turn_off", header: "Turn Off")}, 147 | {Play, (service: "media_play", header: "Play")}, 148 | {Pause, (service: "media_pause", header: "Pause")}, 149 | {Stop, (service: "media_stop", header: "Stop")}, 150 | {NextTrack, (service: "media_next_track", header: "Next")}, 151 | {PreviousTrack, (service: "media_previous_track", header: "Previous")}, 152 | {VolumeMute, (service: "volume_mute", header: "Mute")}, 153 | {VolumeSet, (service: "volume_set", header: "Volume")}, 154 | {VolumeStep, (service: "volume_up", header: "Volume Up")}, 155 | {Seek, (service: "media_seek", header: "Seek")}, 156 | {SelectSource, (service: "select_source", header: "Source")}, 157 | {ClearPlaylist, (service: "clear_playlist", header: "Clear Playlist")}, 158 | {ShuffleSet, (service: "shuffle_set", header: "Shuffle")}, 159 | {PlayMedia, (service: "play_media", header: "Play Media")}, 160 | {SelectSoundMode, (service: "select_sound_mode", header: "Sound Mode")} 161 | }; 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/NotificationEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Newtonsoft.Json.Linq; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class NotificationEvent 7 | { 8 | public static string Domain = "persistent_notification"; 9 | 10 | public Type EventType { get; } 11 | public string Id { get; } 12 | public string Title { get; } 13 | public string Message { get; } 14 | 15 | public NotificationEvent(Type eventType, string id, string title, string message) 16 | { 17 | EventType = eventType; 18 | Id = id; 19 | Title = title; 20 | Message = message; 21 | } 22 | 23 | public static NotificationEvent FromJson(string json) 24 | { 25 | try 26 | { 27 | JToken data = JObject.Parse(json)["event"]?["data"]; 28 | string entityId = data?["entity_id"]?.ToString(); 29 | var newState = data?["new_state"]; 30 | if (newState != null && entityId != null) 31 | { 32 | string id = entityId.Replace($"{Domain}.", ""); 33 | string title = newState["attributes"]?["title"]?.ToString(); 34 | string message = newState["attributes"]?["message"]?.ToString(); 35 | return new NotificationEvent(Type.CREATE, id, title, message); 36 | } 37 | } 38 | catch (Exception) 39 | { 40 | //ignored 41 | } 42 | 43 | return null; 44 | } 45 | 46 | public enum Type 47 | { 48 | CREATE 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/NumberEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Threading; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class NumberEntity : Entity 7 | { 8 | public const string DomainName = "number"; 9 | 10 | public override string Domain() 11 | { 12 | return DomainName; 13 | } 14 | 15 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 16 | { 17 | var root = new MenuItem 18 | { 19 | Header = GetName(name), 20 | StaysOpenOnClick = true, 21 | IsEnabled = IsAvailable() 22 | }; 23 | var min = GetDoubleAttribute("min"); 24 | var value = ParseDouble(State); 25 | var max = GetDoubleAttribute("max"); 26 | var step = GetDoubleAttribute("step"); 27 | root.Items.Add(CreateSlider(dispatcher, min, max, value, "set_value", "Set Value", "value", step)); 28 | root.Items.Add(CreateMenuItem(dispatcher, "increment", "Increment")); 29 | root.Items.Add(CreateMenuItem(dispatcher, "decrement", "Decrement")); 30 | return root; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/SceneEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Controls; 2 | using System.Windows.Threading; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Entities 5 | { 6 | public class SceneEntity : Entity 7 | { 8 | public const string DomainName = "scene"; 9 | 10 | public override string Domain() 11 | { 12 | return DomainName; 13 | } 14 | 15 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 16 | { 17 | return CreateMenuItem(dispatcher, "turn_on", GetName(name), isEnabled: IsAvailable()); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/ScriptEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class ScriptEntity : Entity 8 | { 9 | public const string DomainName = "script"; 10 | 11 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 12 | 13 | public override string Domain() 14 | { 15 | return DomainName; 16 | } 17 | 18 | protected override List OffStates() 19 | { 20 | return OffStatesList; 21 | } 22 | 23 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 24 | { 25 | return CreateMenuItem(dispatcher, "toggle", GetName(name), IsOn(), IsAvailable()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/SelectEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class SelectEntity : Entity 8 | { 9 | public const string DomainName = "select"; 10 | 11 | public override string Domain() 12 | { 13 | return DomainName; 14 | } 15 | 16 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 17 | { 18 | var root = new MenuItem 19 | { 20 | Header = GetName(name), 21 | StaysOpenOnClick = true, 22 | IsEnabled = IsAvailable() 23 | }; 24 | GetListAttribute("options").ForEach(option => 25 | root.Items.Add(CreateMenuItem(dispatcher, "select_option", option, State == option, 26 | data: Tuple.Create("option", option)))); 27 | return root; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/SirenEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class SirenEntity : Entity 8 | { 9 | public const string DomainName = "siren"; 10 | 11 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 12 | 13 | public override string Domain() 14 | { 15 | return DomainName; 16 | } 17 | 18 | protected override List OffStates() 19 | { 20 | return OffStatesList; 21 | } 22 | 23 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 24 | { 25 | return CreateMenuItem(dispatcher, "toggle", GetName(name), IsOn(), IsAvailable()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/SwitchEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Windows.Controls; 3 | using System.Windows.Threading; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Entities 6 | { 7 | public class SwitchEntity : Entity 8 | { 9 | public const string DomainName = "switch"; 10 | 11 | private static readonly List OffStatesList = new List {States.Off, States.Unavailable}; 12 | 13 | public override string Domain() 14 | { 15 | return DomainName; 16 | } 17 | 18 | protected override List OffStates() 19 | { 20 | return OffStatesList; 21 | } 22 | 23 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 24 | { 25 | return CreateMenuItem(dispatcher, "toggle", GetName(name), IsOn(), IsAvailable()); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Entities/VacuumEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Windows.Controls; 4 | using System.Windows.Input; 5 | using System.Windows.Threading; 6 | using Home_Assistant_Taskbar_Menu.Connection; 7 | using MaterialDesignThemes.Wpf; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Entities 10 | { 11 | public class VacuumEntity : Entity 12 | { 13 | public const string DomainName = "vacuum"; 14 | 15 | private static readonly List OffStatesList = new List {States.Docked, States.Unavailable}; 16 | 17 | public override string Domain() 18 | { 19 | return DomainName; 20 | } 21 | 22 | protected override List OffStates() 23 | { 24 | return OffStatesList; 25 | } 26 | 27 | protected override List AllSupportedFeatures() 28 | { 29 | return SupportedFeatures.All; 30 | } 31 | 32 | protected override Dictionary FeatureToServiceMap() 33 | { 34 | return SupportedFeatures.ServiceMap; 35 | } 36 | 37 | protected override MenuItem ToMenuItem(Dispatcher dispatcher, string name) 38 | { 39 | var root = new MenuItem 40 | { 41 | Header = GetName(name), 42 | StaysOpenOnClick = true, 43 | IsEnabled = IsAvailable() 44 | }; 45 | if (IsOn()) 46 | { 47 | root.Icon = new PackIcon {Kind = PackIconKind.Tick}; 48 | } 49 | 50 | GetSupportedFeatures() 51 | .ForEach(supportedFeature => AddMenuItemIfSupported(dispatcher, root, supportedFeature)); 52 | root.PreviewMouseDown += (sender, args) => 53 | { 54 | if (args.ChangedButton == MouseButton.Right) 55 | { 56 | args.Handled = ToggleIfPossible(dispatcher); 57 | } 58 | }; 59 | 60 | if (IsSupported(SupportedFeatures.FanSpeed)) 61 | { 62 | var fanSpeedItem = new MenuItem {Header = "Fan Speed", StaysOpenOnClick = true}; 63 | var currentFanSpeed = GetAttribute("fan_speed"); 64 | GetListAttribute("fan_speed_list").ForEach(fanSpeed => 65 | { 66 | fanSpeedItem.Items.Add(CreateMenuItem(dispatcher, "set_fan_speed", fanSpeed, 67 | fanSpeed == currentFanSpeed, 68 | data: Tuple.Create("fan_speed", fanSpeed))); 69 | }); 70 | root.Items.Add(fanSpeedItem); 71 | } 72 | 73 | return root; 74 | } 75 | 76 | public override bool ToggleIfPossible(Dispatcher dispatcher) 77 | { 78 | if (IsSupported(SupportedFeatures.TurnOn, SupportedFeatures.TurnOff)) 79 | { 80 | HaClientContext.CallService(dispatcher, this, "toggle"); 81 | return true; 82 | } 83 | 84 | return false; 85 | } 86 | 87 | private static class SupportedFeatures 88 | { 89 | public const int TurnOn = 1; 90 | public const int TurnOff = 2; 91 | private const int Pause = 4; 92 | private const int Stop = 8; 93 | private const int ReturnHome = 16; 94 | public const int FanSpeed = 32; 95 | private const int Battery = 64; 96 | private const int Status = 128; 97 | private const int SendCommand = 256; 98 | private const int Locate = 512; 99 | private const int CleanSpot = 1024; 100 | private const int Map = 2048; 101 | private const int State = 4096; 102 | private const int Start = 8192; 103 | 104 | public static readonly List All = new List 105 | { 106 | TurnOn, TurnOff, Start, Pause, Stop, ReturnHome, FanSpeed, 107 | Battery, Status, SendCommand, Locate, CleanSpot, Map, 108 | State 109 | }; 110 | 111 | public static readonly Dictionary ServiceMap = 112 | new Dictionary 113 | { 114 | {TurnOn, (service: "turn_on", header: "Turn On")}, 115 | {TurnOff, (service: "turn_off", header: "Turn Off")}, 116 | {Pause, (service: "pause", header: "Pause")}, 117 | {Stop, (service: "stop", header: "Stop")}, 118 | {ReturnHome, (service: "return_to_base", header: "Return To Base")}, 119 | {Locate, (service: "locate", header: "Locate")}, 120 | {CleanSpot, (service: "clean_spot", header: "Clean Spot")}, 121 | {Start, (service: "start", header: "Start")} 122 | }; 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Images/small.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PiotrMachowski/Home-Assistant-Taskbar-Menu/3af1c8e03f278dda7c7a659b8134d1116716de9a/Home Assistant Taskbar Menu/Images/small.ico -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Resources; 3 | using System.Runtime.InteropServices; 4 | using System.Windows; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("Home Assistant Taskbar Menu")] 10 | [assembly: AssemblyDescription("https://github.com/PiotrMachowski/Home-Assistant-Taskbar-Menu")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("Piotr Machowski")] 13 | [assembly: AssemblyProduct("Home Assistant Taskbar Menu")] 14 | [assembly: AssemblyCopyright("Copyright © 2022 Piotr Machowski")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | //In order to begin building localizable applications, set 24 | //CultureYouAreCodingWith in your .csproj file 25 | //inside a . For example, if you are using US english 26 | //in your source files, set the to en-US. Then uncomment 27 | //the NeutralResourceLanguage attribute below. Update the "en-US" in 28 | //the line below to match the UICulture setting in the project file. 29 | 30 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] 31 | 32 | 33 | [assembly: ThemeInfo( 34 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located 35 | //(used if a resource is not found in the page, 36 | // or application resource dictionaries) 37 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located 38 | //(used if a resource is not found in the page, 39 | // app, or any theme specific resource dictionaries) 40 | )] 41 | 42 | 43 | // Version information for an assembly consists of the following four values: 44 | // 45 | // Major Version 46 | // Minor Version 47 | // Build Number 48 | // Revision 49 | // 50 | // You can specify all the values or you can default the Build and Revision Numbers 51 | // by using the '*' as shown below: 52 | // [assembly: AssemblyVersion("1.0.*")] 53 | [assembly: AssemblyVersion("1.3.0.0")] 54 | [assembly: AssemblyFileVersion("1.3.0.0")] 55 | [assembly: NeutralResourcesLanguage("en")] -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | using System.CodeDom.Compiler; 12 | using System.ComponentModel; 13 | using System.Diagnostics; 14 | using System.Diagnostics.CodeAnalysis; 15 | using System.Globalization; 16 | using System.Resources; 17 | using System.Runtime.CompilerServices; 18 | 19 | namespace Home_Assistant_Taskbar_Menu.Properties 20 | { 21 | 22 | 23 | /// 24 | /// A strongly-typed resource class, for looking up localized strings, etc. 25 | /// 26 | // This class was auto-generated by the StronglyTypedResourceBuilder 27 | // class via a tool like ResGen or Visual Studio. 28 | // To add or remove a member, edit your .ResX file then rerun ResGen 29 | // with the /str option, or rebuild your VS project. 30 | [GeneratedCode("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 31 | [DebuggerNonUserCode()] 32 | [CompilerGenerated()] 33 | internal class Resources 34 | { 35 | 36 | private static ResourceManager resourceMan; 37 | 38 | private static CultureInfo resourceCulture; 39 | 40 | [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 41 | internal Resources() 42 | { 43 | } 44 | 45 | /// 46 | /// Returns the cached ResourceManager instance used by this class. 47 | /// 48 | [EditorBrowsable(EditorBrowsableState.Advanced)] 49 | internal static ResourceManager ResourceManager 50 | { 51 | get 52 | { 53 | if ((resourceMan == null)) 54 | { 55 | ResourceManager temp = new ResourceManager("Home_Assistant_Taskbar_Menu.Properties.Resources", typeof(Resources).Assembly); 56 | resourceMan = temp; 57 | } 58 | return resourceMan; 59 | } 60 | } 61 | 62 | /// 63 | /// Overrides the current thread's CurrentUICulture property for all 64 | /// resource lookups using this strongly typed resource class. 65 | /// 66 | [EditorBrowsable(EditorBrowsableState.Advanced)] 67 | internal static CultureInfo Culture 68 | { 69 | get 70 | { 71 | return resourceCulture; 72 | } 73 | set 74 | { 75 | resourceCulture = value; 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | using System.CodeDom.Compiler; 12 | using System.Configuration; 13 | using System.Runtime.CompilerServices; 14 | 15 | namespace Home_Assistant_Taskbar_Menu.Properties 16 | { 17 | 18 | 19 | [CompilerGenerated()] 20 | [GeneratedCode("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 21 | internal sealed partial class Settings : ApplicationSettingsBase 22 | { 23 | 24 | private static Settings defaultInstance = ((Settings)(Synchronized(new Settings()))); 25 | 26 | public static Settings Default 27 | { 28 | get 29 | { 30 | return defaultInstance; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Properties/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 55 | 56 | 70 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/Configuration.cs: -------------------------------------------------------------------------------- 1 | namespace Home_Assistant_Taskbar_Menu.Utils 2 | { 3 | public class Configuration 4 | { 5 | public string Url { get; } 6 | 7 | public string Token { get; } 8 | 9 | public Configuration(string url, string token) 10 | { 11 | Url = url; 12 | Token = token; 13 | } 14 | 15 | public string HttpUrl() 16 | { 17 | return Url.Replace("wss://", "https://") 18 | .Replace("ws://", "http://") 19 | .Replace("/api/websocket", ""); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/ConsoleWriter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace Home_Assistant_Taskbar_Menu.Utils 5 | { 6 | public static class ConsoleWriter 7 | { 8 | public static void WriteLine(string s, ConsoleColor color, bool addPrefix = true, bool align = false) 9 | { 10 | Write(s, color, addPrefix, align, true); 11 | } 12 | 13 | public static void Write(string s, ConsoleColor color, bool addPrefix = true, bool align = false, 14 | bool breakLine = false) 15 | { 16 | var oldColor = Console.ForegroundColor; 17 | Console.ForegroundColor = color; 18 | var prefix = addPrefix ? $"[{DateTime.Now.ToString(CultureInfo.InvariantCulture)}] " : 19 | align ? new string(' ', 22) : ""; 20 | Console.Write($"{prefix}{s}"); 21 | if (breakLine) 22 | { 23 | Console.WriteLine(); 24 | } 25 | 26 | Console.ForegroundColor = oldColor; 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/EntityCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using Home_Assistant_Taskbar_Menu.Entities; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Home_Assistant_Taskbar_Menu.Utils 8 | { 9 | public static class EntityCreator 10 | { 11 | private static readonly List SupportedDomains = new List 12 | { 13 | AutomationEntity.DomainName, 14 | ButtonEntity.DomainName, 15 | ClimateEntity.DomainName, 16 | CoverEntity.DomainName, 17 | FanEntity.DomainName, 18 | InputBooleanEntity.DomainName, 19 | InputButton.DomainName, 20 | InputNumberEntity.DomainName, 21 | InputSelectEntity.DomainName, 22 | LightEntity.DomainName, 23 | LockEntity.DomainName, 24 | MediaPlayerEntity.DomainName, 25 | NumberEntity.DomainName, 26 | SceneEntity.DomainName, 27 | ScriptEntity.DomainName, 28 | SelectEntity.DomainName, 29 | SirenEntity.DomainName, 30 | SwitchEntity.DomainName, 31 | VacuumEntity.DomainName 32 | }; 33 | 34 | 35 | public static Entity Create(JToken jToken) 36 | { 37 | var domain = jToken["entity_id"].ToString().Split('.')[0]; 38 | JToken newState = jToken; 39 | switch (domain) 40 | { 41 | case AutomationEntity.DomainName: 42 | return newState?.ToObject(); 43 | case ButtonEntity.DomainName: 44 | return newState?.ToObject(); 45 | case ClimateEntity.DomainName: 46 | return newState?.ToObject(); 47 | case CoverEntity.DomainName: 48 | return newState?.ToObject(); 49 | case FanEntity.DomainName: 50 | return newState?.ToObject(); 51 | case InputBooleanEntity.DomainName: 52 | return newState?.ToObject(); 53 | case InputButton.DomainName: 54 | return newState?.ToObject(); 55 | case InputNumberEntity.DomainName: 56 | return newState?.ToObject(); 57 | case InputSelectEntity.DomainName: 58 | return newState?.ToObject(); 59 | case LightEntity.DomainName: 60 | return newState?.ToObject(); 61 | case LockEntity.DomainName: 62 | return newState?.ToObject(); 63 | case MediaPlayerEntity.DomainName: 64 | return newState?.ToObject(); 65 | case NumberEntity.DomainName: 66 | return newState?.ToObject(); 67 | case SceneEntity.DomainName: 68 | return newState?.ToObject(); 69 | case ScriptEntity.DomainName: 70 | return newState?.ToObject(); 71 | case SelectEntity.DomainName: 72 | return newState?.ToObject(); 73 | case SirenEntity.DomainName: 74 | return newState?.ToObject(); 75 | case SwitchEntity.DomainName: 76 | return newState?.ToObject(); 77 | case VacuumEntity.DomainName: 78 | return newState?.ToObject(); 79 | } 80 | 81 | return null; 82 | } 83 | 84 | public static Entity CreateFromChangedState(string json) 85 | { 86 | JToken jToken = JObject.Parse(json)["event"]?["data"]; 87 | string entityId = jToken?["entity_id"].ToString(); 88 | var domain = entityId?.Split('.')[0]; 89 | try 90 | { 91 | if (IsSupported(domain)) 92 | { 93 | var new_state = jToken?["new_state"]; 94 | var myStateObject = Create(new_state); 95 | return myStateObject; 96 | } 97 | } 98 | catch (Exception) 99 | { 100 | ConsoleWriter.WriteLine($"ERROR CREATING MENU ITEM FOR: {entityId}", ConsoleColor.Red); 101 | } 102 | 103 | return null; 104 | } 105 | 106 | public static List CreateFromStateList(string json) 107 | { 108 | return JObject.Parse(json)["result"].Children() 109 | .Select(Create) 110 | .Where(v => v != null) 111 | .ToList(); 112 | } 113 | 114 | public static bool IsSupported(string domain) 115 | { 116 | return SupportedDomains.Contains(domain); 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/ResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | using System.Reflection; 5 | using Newtonsoft.Json.Linq; 6 | 7 | namespace Home_Assistant_Taskbar_Menu.Utils 8 | { 9 | public static class ResourceProvider 10 | { 11 | public static string NameAndVersion() 12 | { 13 | return $"{Assembly.GetExecutingAssembly().GetName().Name} {Version()}"; 14 | } 15 | 16 | public static string Version() 17 | { 18 | return $"v{Assembly.GetExecutingAssembly().GetName().Version}"; 19 | } 20 | 21 | public static string CopyrightInfo() 22 | { 23 | return Assembly.GetExecutingAssembly().CustomAttributes 24 | .Where(ca => ca.AttributeType?.FullName == "System.Reflection.AssemblyCopyrightAttribute") 25 | .Select(ca => ca.ConstructorArguments[0]).First().Value.ToString(); 26 | } 27 | 28 | public static string RepoUri() 29 | { 30 | return Assembly.GetExecutingAssembly().CustomAttributes 31 | .Where(ca => ca.AttributeType?.FullName == "System.Reflection.AssemblyDescriptionAttribute") 32 | .Select(ca => ca.ConstructorArguments[0]).First().Value.ToString(); 33 | } 34 | 35 | public static (string version, string url) LatestVersion() 36 | { 37 | try 38 | { 39 | using (var webClient = new WebClient {Headers = {["User-Agent"] = @"HATM"}}) 40 | { 41 | var releaseUrl = RepoUri().Replace("github.com/", "api.github.com/repos/") + "/releases/latest"; 42 | var json = webClient.DownloadString(releaseUrl); 43 | var version = JObject.Parse(json)["tag_name"]; 44 | var url = JObject.Parse(json)["html_url"]; 45 | return version != null && url != null ? (version.ToString(), url.ToString()) : (null, null); 46 | } 47 | } 48 | catch (Exception) 49 | { 50 | //ignored 51 | } 52 | 53 | return (null, null); 54 | } 55 | 56 | public static bool IsUpToDate((string version, string url) latestVersion) 57 | { 58 | return latestVersion.version == null || latestVersion.version == Version(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/Storage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Security.Cryptography; 5 | using System.Text; 6 | using Newtonsoft.Json; 7 | 8 | namespace Home_Assistant_Taskbar_Menu.Utils 9 | { 10 | public static class Storage 11 | { 12 | public static string BrowserCachePath => $"{_basePath}\\browserCache"; 13 | private static string CredentialsPathOld => $"{_basePath}\\config.dat"; 14 | private static string ViewConfigPathOld => $"{_basePath}\\viewConfig.dat"; 15 | private static string CredentialsPath => $"{_basePath}\\config_credentials.dat"; 16 | private static string ViewConfigPath => $"{_basePath}\\config_view.dat"; 17 | private static string BrowserConfigPath => $"{_basePath}\\config_position.dat"; 18 | private static string LogPath => $"{_basePath}\\log.txt"; 19 | 20 | private const string PassPhrase = "ThisIsASecurePassword"; 21 | private const int KeySize = 256; 22 | private const int DerivationIterations = 1000; 23 | private static string _basePath = ""; 24 | 25 | public static Configuration RestoreConfiguration() 26 | { 27 | Configuration cfg; 28 | try 29 | { 30 | using (var streamReader = new StreamReader(CredentialsPath)) 31 | { 32 | cfg = JsonConvert.DeserializeObject(Decrypt(streamReader.ReadToEnd())); 33 | } 34 | } 35 | catch (Exception) 36 | { 37 | cfg = null; 38 | } 39 | 40 | return cfg; 41 | } 42 | 43 | public static ViewConfiguration RestoreViewConfiguration() 44 | { 45 | ViewConfiguration viewConfiguration; 46 | try 47 | { 48 | using (var streamReader = new StreamReader(ViewConfigPath)) 49 | { 50 | viewConfiguration = JsonConvert.DeserializeObject(streamReader.ReadToEnd()); 51 | } 52 | } 53 | catch (Exception) 54 | { 55 | viewConfiguration = null; 56 | } 57 | 58 | return viewConfiguration ?? ViewConfiguration.Default(); 59 | } 60 | 61 | public static void Save(Configuration cfg) 62 | { 63 | using (var streamWriter = new StreamWriter(CredentialsPath)) 64 | { 65 | streamWriter.Write(Encrypt(JsonConvert.SerializeObject(cfg))); 66 | } 67 | } 68 | 69 | public static void Save(ViewConfiguration viewConfiguration) 70 | { 71 | using (var streamWriter = new StreamWriter(ViewConfigPath)) 72 | { 73 | streamWriter.Write(JsonConvert.SerializeObject(viewConfiguration, Formatting.Indented, 74 | new JsonSerializerSettings() 75 | { 76 | NullValueHandling = NullValueHandling.Ignore 77 | })); 78 | } 79 | } 80 | 81 | public static void SavePosition((double x, double y, double width, double height) position) 82 | { 83 | using (var streamWriter = new StreamWriter(BrowserConfigPath)) 84 | { 85 | streamWriter.WriteLine(position.x); 86 | streamWriter.WriteLine(position.y); 87 | streamWriter.WriteLine(position.width); 88 | streamWriter.WriteLine(position.height); 89 | } 90 | } 91 | 92 | public static (double x, double y, double width, double height)? RestorePosition() 93 | { 94 | (double, double, double, double)? position; 95 | try 96 | { 97 | using (var streamReader = new StreamReader(BrowserConfigPath)) 98 | { 99 | double x = Convert.ToDouble(streamReader.ReadLine()); 100 | double y = Convert.ToDouble(streamReader.ReadLine()); 101 | double width = Convert.ToDouble(streamReader.ReadLine()); 102 | double height = Convert.ToDouble(streamReader.ReadLine()); 103 | position = (x, y, width, height); 104 | } 105 | } 106 | catch (Exception) 107 | { 108 | position = null; 109 | } 110 | 111 | return position; 112 | } 113 | 114 | public static void InitConfigDirectory(bool enableLogging) 115 | { 116 | string currentDir = Directory.GetCurrentDirectory().Split('\\').ToList().Last(); 117 | string appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 118 | _basePath = $"{appData}\\Home Assistant Taskbar Menu\\{currentDir}"; 119 | Directory.CreateDirectory(_basePath); 120 | if (!IsConsoleAvailable() && enableLogging) 121 | { 122 | FileStream fileStream = new FileStream(LogPath, FileMode.Create); 123 | StreamWriter streamWriter = new StreamWriter(fileStream) {AutoFlush = true}; 124 | Console.SetOut(streamWriter); 125 | Console.SetError(streamWriter); 126 | } 127 | ConsoleWriter.WriteLine($"Config directory: {_basePath}", ConsoleColor.DarkYellow); 128 | MoveFile(CredentialsPathOld, CredentialsPath); 129 | MoveFile(ViewConfigPathOld, ViewConfigPath); 130 | } 131 | 132 | private static void MoveFile(string source, string destination) 133 | { 134 | try 135 | { 136 | File.Move(source, destination); 137 | } 138 | catch (Exception) 139 | { 140 | // ignored 141 | } 142 | } 143 | 144 | 145 | private static string Encrypt(string plainText) 146 | { 147 | var saltStringBytes = Generate256BitsOfRandomEntropy(); 148 | var ivStringBytes = Generate256BitsOfRandomEntropy(); 149 | var plainTextBytes = Encoding.UTF8.GetBytes(plainText); 150 | using (var password = new Rfc2898DeriveBytes(PassPhrase, saltStringBytes, DerivationIterations)) 151 | { 152 | var keyBytes = password.GetBytes(KeySize / 8); 153 | using (var symmetricKey = new RijndaelManaged()) 154 | { 155 | symmetricKey.BlockSize = 256; 156 | symmetricKey.Mode = CipherMode.CBC; 157 | symmetricKey.Padding = PaddingMode.PKCS7; 158 | using (var encryptor = symmetricKey.CreateEncryptor(keyBytes, ivStringBytes)) 159 | { 160 | using (var memoryStream = new MemoryStream()) 161 | { 162 | using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) 163 | { 164 | cryptoStream.Write(plainTextBytes, 0, plainTextBytes.Length); 165 | cryptoStream.FlushFinalBlock(); 166 | var cipherTextBytes = saltStringBytes; 167 | cipherTextBytes = cipherTextBytes.Concat(ivStringBytes).ToArray(); 168 | cipherTextBytes = cipherTextBytes.Concat(memoryStream.ToArray()).ToArray(); 169 | memoryStream.Close(); 170 | cryptoStream.Close(); 171 | return Convert.ToBase64String(cipherTextBytes); 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | 179 | private static string Decrypt(string cipherText) 180 | { 181 | var cipherTextBytesWithSaltAndIv = Convert.FromBase64String(cipherText); 182 | var saltStringBytes = cipherTextBytesWithSaltAndIv.Take(KeySize / 8).ToArray(); 183 | var ivStringBytes = cipherTextBytesWithSaltAndIv.Skip(KeySize / 8).Take(KeySize / 8).ToArray(); 184 | var cipherTextBytes = cipherTextBytesWithSaltAndIv.Skip(KeySize / 8 * 2) 185 | .Take(cipherTextBytesWithSaltAndIv.Length - KeySize / 8 * 2).ToArray(); 186 | 187 | using (var password = new Rfc2898DeriveBytes(PassPhrase, saltStringBytes, DerivationIterations)) 188 | { 189 | var keyBytes = password.GetBytes(KeySize / 8); 190 | using (var symmetricKey = new RijndaelManaged()) 191 | { 192 | symmetricKey.BlockSize = 256; 193 | symmetricKey.Mode = CipherMode.CBC; 194 | symmetricKey.Padding = PaddingMode.PKCS7; 195 | using (var decryptor = symmetricKey.CreateDecryptor(keyBytes, ivStringBytes)) 196 | { 197 | using (var memoryStream = new MemoryStream(cipherTextBytes)) 198 | { 199 | using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) 200 | { 201 | var plainTextBytes = new byte[cipherTextBytes.Length]; 202 | var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); 203 | memoryStream.Close(); 204 | cryptoStream.Close(); 205 | return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | } 212 | 213 | private static byte[] Generate256BitsOfRandomEntropy() 214 | { 215 | var randomBytes = new byte[32]; 216 | using (var rngCsp = new RNGCryptoServiceProvider()) 217 | { 218 | rngCsp.GetBytes(randomBytes); 219 | } 220 | 221 | return randomBytes; 222 | } 223 | 224 | private static bool IsConsoleAvailable() 225 | { 226 | bool consolePresent = true; 227 | try 228 | { 229 | int _ = Console.WindowHeight; 230 | } 231 | catch 232 | { 233 | consolePresent = false; 234 | } 235 | 236 | return consolePresent; 237 | } 238 | } 239 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Utils/ViewConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Home_Assistant_Taskbar_Menu.Entities; 4 | 5 | namespace Home_Assistant_Taskbar_Menu.Utils 6 | { 7 | public class ViewConfiguration 8 | { 9 | public const string ThemeKey = "Theme"; 10 | public const string LightTheme = "Light"; 11 | public const string DarkTheme = "Dark"; 12 | public const string MirrorNotificationsKey = "MirrorNotifications"; 13 | 14 | public Type NodeType { get; set; } 15 | 16 | public string Name { get; set; } 17 | 18 | public string EntityId { get; set; } 19 | 20 | public List Children { get; set; } 21 | 22 | public Dictionary Properties; 23 | 24 | public bool ContainsEntity(Entity stateObject) 25 | { 26 | return NodeType == Type.Entity && stateObject.EntityId == EntityId 27 | || (NodeType == Type.Folder || NodeType == Type.Root) 28 | && Children.Any(c => c.ContainsEntity(stateObject)); 29 | } 30 | 31 | public string GetProperty(string key) 32 | { 33 | return Properties != null && Properties.ContainsKey(key) ? Properties[key] : ""; 34 | } 35 | 36 | public static ViewConfiguration Default() 37 | { 38 | return new ViewConfiguration 39 | { 40 | NodeType = Type.Root, 41 | Children = new List(), 42 | Properties = new Dictionary 43 | { 44 | {ThemeKey, LightTheme}, 45 | {MirrorNotificationsKey, true.ToString()} 46 | } 47 | }; 48 | } 49 | 50 | public static ViewConfiguration Separator() 51 | { 52 | return new ViewConfiguration 53 | { 54 | NodeType = Type.Separator 55 | }; 56 | } 57 | 58 | public static ViewConfiguration Entity(string entityId, string name) 59 | { 60 | return new ViewConfiguration 61 | { 62 | NodeType = Type.Entity, 63 | EntityId = entityId, 64 | Name = name 65 | }; 66 | } 67 | 68 | public static ViewConfiguration Folder(string name) 69 | { 70 | return new ViewConfiguration 71 | { 72 | NodeType = Type.Folder, 73 | Name = name, 74 | Children = new List() 75 | }; 76 | } 77 | 78 | public enum Type 79 | { 80 | Entity, 81 | Folder, 82 | Root, 83 | Separator 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/AboutWindow.xaml: -------------------------------------------------------------------------------- 1 |  17 | 18 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/AboutWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using System.Windows.Navigation; 6 | using Home_Assistant_Taskbar_Menu.Utils; 7 | 8 | namespace Home_Assistant_Taskbar_Menu.Views 9 | { 10 | /// 11 | /// Interaction logic for AboutWindow.xaml 12 | /// 13 | public partial class AboutWindow : Window 14 | { 15 | public AboutWindow() 16 | { 17 | InitializeComponent(); 18 | NameAndVersionLabel.Content = ResourceProvider.NameAndVersion(); 19 | CopyrightInfoLabel.Content = ResourceProvider.CopyrightInfo(); 20 | RepoHyperlink.NavigateUri = new Uri(ResourceProvider.RepoUri()); 21 | } 22 | 23 | private void OpenHyperlink(object sender, RequestNavigateEventArgs e) 24 | { 25 | Process.Start(e.Uri.AbsoluteUri); 26 | e.Handled = true; 27 | } 28 | 29 | private void CloseButton(object sender, RoutedEventArgs e) 30 | { 31 | Close(); 32 | } 33 | 34 | private void HeaderMouseDown(object sender, MouseButtonEventArgs e) 35 | { 36 | if (e.ChangedButton == MouseButton.Left) 37 | DragMove(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/AuthWindow.xaml: -------------------------------------------------------------------------------- 1 |  16 | 17 | 20 | 21 | 23 | 24 | 25 | 26 | 28 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/BrowserWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Windows; 4 | using System.Windows.Input; 5 | using CefSharp; 6 | using CefSharp.Wpf; 7 | using Home_Assistant_Taskbar_Menu.Utils; 8 | 9 | namespace Home_Assistant_Taskbar_Menu.Views 10 | { 11 | /// 12 | /// Interaction logic for BrowserWindow.xaml 13 | /// 14 | public partial class BrowserWindow : Window 15 | { 16 | private string Url { get; } 17 | 18 | public BrowserWindow(Configuration configuration) 19 | { 20 | CefSettings cefSharpSettings = new CefSettings {LogSeverity = LogSeverity.Disable}; 21 | Cef.Initialize(cefSharpSettings); 22 | ShowActivated = true; 23 | Url = configuration.HttpUrl(); 24 | InitializeComponent(); 25 | Browser.Address = Url; 26 | 27 | var requestContextSettings = new RequestContextSettings 28 | {CachePath = Storage.BrowserCachePath}; 29 | Browser.RequestContext = new RequestContext(requestContextSettings); 30 | var position = Storage.RestorePosition(); 31 | if (position.HasValue) 32 | { 33 | Left = position.Value.x; 34 | Top = position.Value.y; 35 | Width = position.Value.width; 36 | Height = position.Value.height; 37 | } 38 | } 39 | 40 | private void MinimizeButton(object sender, RoutedEventArgs e) 41 | { 42 | WindowState = WindowState.Minimized; 43 | } 44 | 45 | private void MaximizeRestoreButton(object sender, RoutedEventArgs e) 46 | { 47 | WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; 48 | } 49 | 50 | private void CloseButton(object sender, RoutedEventArgs e) 51 | { 52 | Hide(); 53 | } 54 | 55 | private void HeaderMouseDown(object sender, MouseButtonEventArgs e) 56 | { 57 | if (e.ChangedButton == MouseButton.Left) 58 | DragMove(); 59 | } 60 | 61 | private void BrowserButton(object sender, RoutedEventArgs e) 62 | { 63 | Process.Start(Browser.Address); 64 | } 65 | 66 | private void RefreshButton(object sender, RoutedEventArgs e) 67 | { 68 | Browser.WebBrowser.Reload(); 69 | } 70 | 71 | private void BrowserWindow_OnClosed(object sender, EventArgs e) 72 | { 73 | Storage.SavePosition((Left, Top, Width, Height)); 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  15 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | using System.Windows; 9 | using System.Windows.Controls; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using Home_Assistant_Taskbar_Menu.Connection; 13 | using Home_Assistant_Taskbar_Menu.Entities; 14 | using Home_Assistant_Taskbar_Menu.Utils; 15 | using Home_Assistant_Taskbar_Menu.Views; 16 | using MaterialDesignThemes.Wpf; 17 | using Icon = System.Drawing.Icon; 18 | 19 | namespace Home_Assistant_Taskbar_Menu 20 | { 21 | /// 22 | /// Interaction logic for MainWindow.xaml 23 | /// 24 | public partial class MainWindow : Window 25 | { 26 | private readonly BrowserWindow _browserWindow; 27 | private AboutWindow _aboutWindow; 28 | private SearchWindow _searchWindow; 29 | private ViewConfigurationWindow _viewConfigurationWindow; 30 | 31 | private ViewConfiguration _viewConfiguration; 32 | private readonly List _defaultMenuItems; 33 | private readonly List _stateObjects; 34 | 35 | public ObservableCollection Menu { get; set; } 36 | 37 | public MainWindow(Configuration configuration, ViewConfiguration viewConfiguration) 38 | { 39 | var latestVersion = ResourceProvider.LatestVersion(); 40 | _browserWindow = new BrowserWindow(configuration); 41 | _viewConfiguration = viewConfiguration; 42 | _stateObjects = new List(); 43 | Menu = new ObservableCollection(); 44 | InitializeComponent(); 45 | _defaultMenuItems = CreateDefaultMenuItems(configuration, latestVersion); 46 | TaskbarMenuRoot.ItemsSource = Menu; 47 | Task.Run(() => { InitConnection(configuration).Wait(); }); 48 | } 49 | 50 | private List CreateDefaultMenuItems(Configuration configuration, 51 | (string version, string url) latestVersion) 52 | { 53 | var showUpdate = !ResourceProvider.IsUpToDate(latestVersion); 54 | Grid grid = new Grid() {MinWidth = 220}; 55 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 56 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 57 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 58 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 59 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 60 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 61 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 62 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 63 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 64 | if (showUpdate) 65 | { 66 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 67 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = GridLength.Auto}); 68 | ShowNotification("Home Assistant Taskbar Menu", "New version of application is available"); 69 | } 70 | 71 | CreateMenuIcon(grid, PackIconKind.Settings, "Edit Application Settings", () => 72 | { 73 | _viewConfigurationWindow?.Close(); 74 | _viewConfigurationWindow = new ViewConfigurationWindow(_stateObjects, _viewConfiguration); 75 | var response = _viewConfigurationWindow.ShowDialog(); 76 | if (response == true) 77 | { 78 | _viewConfiguration = _viewConfigurationWindow.ViewConfiguration; 79 | UpdateTree(); 80 | } 81 | }); 82 | CreateMenuIcon(grid, PackIconKind.HomeAssistant, "Open Home Assistant", 83 | () => ShowBrowser(null, null)); 84 | CreateMenuIcon(grid, PackIconKind.OpenInBrowser, "Open Home Assistant in Browser", 85 | () => Process.Start(configuration.HttpUrl())); 86 | CreateMenuIcon(grid, PackIconKind.About, "About HA Taskbar Menu", () => 87 | { 88 | _aboutWindow?.Close(); 89 | _aboutWindow = new AboutWindow(); 90 | _aboutWindow.ShowDialog(); 91 | }); 92 | if (showUpdate) 93 | { 94 | CreateMenuIcon(grid, PackIconKind.Update, "Update HA Taskbar Menu", 95 | () => Process.Start(latestVersion.url)); 96 | } 97 | 98 | CreateMenuIcon(grid, PackIconKind.Close, "Exit", () => Application.Current.Shutdown()); 99 | return new List {new Separator(), grid}; 100 | } 101 | 102 | private static void CreateMenuIcon(Grid grid, PackIconKind kind, string tooltip, Action clickAction) 103 | { 104 | var paletteHelper = new PaletteHelper(); 105 | var theme = paletteHelper.GetTheme(); 106 | PackIcon icon = new PackIcon 107 | { 108 | Kind = kind, 109 | HorizontalAlignment = HorizontalAlignment.Center, 110 | VerticalAlignment = VerticalAlignment.Center, 111 | Width = double.NaN, 112 | Height = double.NaN, 113 | Margin = new Thickness(3), 114 | Foreground = new SolidColorBrush(theme.ToolTipBackground) 115 | }; 116 | Button button = new Button() 117 | { 118 | Width = double.NaN, 119 | Height = double.NaN, 120 | Content = icon, 121 | ToolTip = tooltip, 122 | Padding = new Thickness(0), 123 | Background = Brushes.Transparent, 124 | BorderBrush = Brushes.Transparent 125 | }; 126 | button.PreviewMouseDown += (sender, args) => { clickAction.Invoke(); }; 127 | Grid.SetColumn(button, grid.Children.Count * 2); 128 | grid.Children.Add(button); 129 | } 130 | 131 | 132 | private void HandleNewEntitiesList(List entitiesList) 133 | { 134 | UpdateMyStateObjects(entitiesList); 135 | ConsoleWriter.WriteLine($"RECEIVED SUPPORTED STATES: {entitiesList.Count}", ConsoleColor.Green); 136 | entitiesList.ForEach(c => { ConsoleWriter.WriteLine($" {c.EntityId}: {c.State}", ConsoleColor.Gray); }); 137 | Dispatcher.Invoke(() => UpdateTree()); 138 | } 139 | 140 | private async Task InitConnection(Configuration configuration) 141 | { 142 | HaClientContext.Initialize(configuration); 143 | await HaClientContext.Start(); 144 | HaClientContext.AddStateChangeListener(this, UpdateState); 145 | HaClientContext.AddEntitiesListListener(HandleNewEntitiesList); 146 | HaClientContext.AddAuthenticationStateListener(auth => Dispatcher.Invoke(() => UpdateTree(auth))); 147 | HaClientContext.AddNotificationListener(HandleNotification); 148 | } 149 | 150 | private List CreateStructure(List stateObjects, ViewConfiguration viewConfiguration) 151 | { 152 | return viewConfiguration.Children.Count == 0 153 | ? stateObjects.Where(e => e.Domain() != AutomationEntity.DomainName && e.Domain() != ScriptEntity.DomainName) 154 | .Select(e => (Control) e.ToMenuItemSafe(Dispatcher, null)) 155 | .Take(100) 156 | .ToList() 157 | : viewConfiguration.Children.Select(c => MapToControl(stateObjects, c)).ToList(); 158 | } 159 | 160 | private Control MapToControl(List stateObjects, ViewConfiguration viewConfiguration) 161 | { 162 | switch (viewConfiguration.NodeType) 163 | { 164 | case ViewConfiguration.Type.Separator: 165 | return new Separator(); 166 | case ViewConfiguration.Type.Entity: 167 | var stateObject = stateObjects.Find(e => e.EntityId.Equals(viewConfiguration.EntityId)); 168 | return stateObject == null 169 | ? new MenuItem {Header = viewConfiguration.EntityId} 170 | : stateObject.ToMenuItemSafe(Dispatcher, viewConfiguration.Name); 171 | case ViewConfiguration.Type.Folder: 172 | var node = new MenuItem 173 | { 174 | Header = viewConfiguration.Name 175 | }; 176 | viewConfiguration.Children.Select(c => MapToControl(stateObjects, c)).ToList() 177 | .ForEach(c => node.Items.Add(c)); 178 | return node; 179 | default: 180 | throw new ArgumentOutOfRangeException(); 181 | } 182 | } 183 | 184 | private void UpdateMyStateObjects(List state) 185 | { 186 | _stateObjects.Clear(); 187 | _stateObjects.AddRange(state); 188 | } 189 | 190 | private void UpdateState(Entity changedState) 191 | { 192 | ConsoleWriter.WriteLine($"STATE UPDATED: {changedState.EntityId} => {changedState.State}", 193 | ConsoleColor.Green); 194 | if (_viewConfiguration.ContainsEntity(changedState) || 195 | _viewConfiguration.Children.Count == 0) 196 | { 197 | var ind = _stateObjects.FindIndex(s => s.EntityId == changedState.EntityId); 198 | if (ind >= 0) 199 | { 200 | _stateObjects[ind] = changedState; 201 | } 202 | 203 | Dispatcher.Invoke(() => UpdateTree()); 204 | } 205 | } 206 | 207 | private void UpdateTree(bool authenticated = true) 208 | { 209 | Menu.Clear(); 210 | if (authenticated) 211 | { 212 | CreateStructure(_stateObjects, _viewConfiguration).ForEach(Menu.Add); 213 | } 214 | else 215 | { 216 | MenuItem reconnect = new MenuItem {Header = "Reconnect", IsEnabled = true}; 217 | reconnect.Click += (sender, args) => { HaClientContext.Recreate(); }; 218 | Menu.Add(reconnect); 219 | } 220 | 221 | _defaultMenuItems.ForEach(Menu.Add); 222 | } 223 | 224 | private void UIElement_OnKeyDown(object sender, KeyEventArgs e) 225 | { 226 | _searchWindow?.Close(); 227 | _searchWindow = new SearchWindow(e.Key.ToString(), _stateObjects); 228 | _searchWindow.ShowDialog(); 229 | } 230 | 231 | private void HandleNotification(NotificationEvent notification) 232 | { 233 | ConsoleWriter.WriteLine($"NOTIFICATION RECEIVED: {notification.Id}", ConsoleColor.Green); 234 | if (_viewConfiguration.GetProperty(ViewConfiguration.MirrorNotificationsKey) == true.ToString()) 235 | { 236 | ShowNotification(notification.Title, notification.Message); 237 | } 238 | } 239 | 240 | private void ShowNotification(string title, string message) 241 | { 242 | TaskbarIcon.ShowBalloonTip(title, message, GetIcon(), true); 243 | } 244 | 245 | private Icon GetIcon() 246 | { 247 | using (Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/Images/small.ico")) 248 | ?.Stream) 249 | { 250 | return new Icon(iconStream); 251 | } 252 | } 253 | 254 | private void ShowBrowser(object sender, RoutedEventArgs e) 255 | { 256 | _browserWindow.Show(); 257 | _browserWindow.Activate(); 258 | } 259 | } 260 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/SearchWindow.xaml: -------------------------------------------------------------------------------- 1 |  17 | 18 | 21 | 22 | 24 | 25 | 26 | 27 | 29 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/SearchWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Windows; 4 | using System.Windows.Controls; 5 | using System.Windows.Input; 6 | using Home_Assistant_Taskbar_Menu.Connection; 7 | using Home_Assistant_Taskbar_Menu.Entities; 8 | using MaterialDesignThemes.Wpf; 9 | 10 | namespace Home_Assistant_Taskbar_Menu.Views 11 | { 12 | /// 13 | /// Interaction logic for SearchWindow.xaml 14 | /// 15 | public partial class SearchWindow : Window 16 | { 17 | private readonly List _entities; 18 | 19 | public SearchWindow(string s, List entities) 20 | { 21 | _entities = new List(entities); 22 | InitializeComponent(); 23 | HaClientContext.AddStateChangeListener(this, updatedEntity => 24 | { 25 | var index = _entities.FindIndex(e => e.EntityId == updatedEntity.EntityId); 26 | if (index >= 0) 27 | { 28 | _entities[index] = updatedEntity; 29 | Dispatcher.Invoke(() => UpdateFoundList(null, null)); 30 | } 31 | }); 32 | if (s.Length == 1) 33 | { 34 | SearchBox.Text = s; 35 | } 36 | 37 | SearchBox.CaretIndex = int.MaxValue; 38 | SearchBox.Focus(); 39 | } 40 | 41 | private void UpdateFoundList(object sender, TextChangedEventArgs ee) 42 | { 43 | var text = SearchBox.Text.ToLower(); 44 | FoundList.Items.Clear(); 45 | var list = _entities.ToList().Where(e => EntityMatches(e, text)).ToList(); 46 | for (var i = 0; i < list.Count && i < 10; i++) 47 | { 48 | var entity = list[i]; 49 | var cm = Convert(entity); 50 | FoundList.Items.Add(cm); 51 | } 52 | } 53 | 54 | private static bool EntityMatches(Entity e, string text) 55 | { 56 | return e.EntityId.ToLower().Contains(text) || e.GetName().ToLower().Contains(text); 57 | } 58 | 59 | private ListBoxItem Convert(Entity entity) 60 | { 61 | MenuItem item = entity.ToMenuItemSafe(Dispatcher, null); 62 | item.Visibility = Visibility.Hidden; 63 | PackIcon icon = new PackIcon 64 | { 65 | Kind = PackIconKind.Tick, 66 | Height = double.NaN, 67 | Width = double.NaN, 68 | Visibility = entity.IsOn() ? Visibility.Visible : Visibility.Hidden, 69 | Padding = new Thickness(5) 70 | }; 71 | Grid.SetColumn(icon, 0); 72 | Label label = new Label 73 | { 74 | Content = entity.ToString().Replace("_", "__"), 75 | VerticalAlignment = VerticalAlignment.Center 76 | }; 77 | Grid.SetColumn(label, 1); 78 | Grid grid = new Grid 79 | { 80 | Width = double.NaN, 81 | IsEnabled = entity.IsAvailable(), 82 | ContextMenu = new ContextMenu {StaysOpen = false} 83 | }; 84 | grid.Children.Add(icon); 85 | grid.Children.Add(label); 86 | grid.Children.Add(item); 87 | grid.ColumnDefinitions.Add(new ColumnDefinition {Width = new GridLength(30)}); 88 | grid.ColumnDefinitions.Add(new ColumnDefinition()); 89 | grid.PreviewMouseUp += (sender, args) => args.Handled = true; 90 | grid.PreviewMouseDown += (sender, args) => 91 | { 92 | if (args.ChangedButton == MouseButton.Right) 93 | args.Handled = entity.ToggleIfPossible(Dispatcher); 94 | }; 95 | grid.MouseDown += (sender, args) => 96 | { 97 | if (grid.ContextMenu.Items.Count == 0) 98 | item.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent)); 99 | else 100 | grid.ContextMenu.IsOpen = true; 101 | args.Handled = true; 102 | }; 103 | List os = item.Items.Cast().ToList(); 104 | os.ForEach(o => 105 | { 106 | item.Items.Remove(o); 107 | grid.ContextMenu.Items.Add(o); 108 | }); 109 | return new ListBoxItem 110 | { 111 | Padding = new Thickness(0), 112 | Content = grid, 113 | HorizontalContentAlignment = HorizontalAlignment.Stretch 114 | }; 115 | } 116 | 117 | private void CloseButton(object sender, RoutedEventArgs e) 118 | { 119 | HaClientContext.RemoveStateChangeListener(this); 120 | Close(); 121 | } 122 | 123 | private void HeaderMouseDown(object sender, MouseButtonEventArgs e) 124 | { 125 | if (e.ChangedButton == MouseButton.Left) 126 | DragMove(); 127 | } 128 | 129 | private void EscapeListener(object sender, KeyEventArgs e) 130 | { 131 | if (e.Key == Key.Escape) 132 | { 133 | HaClientContext.RemoveStateChangeListener(this); 134 | Close(); 135 | } 136 | } 137 | } 138 | } -------------------------------------------------------------------------------- /Home Assistant Taskbar Menu/Views/ViewConfigurationDialog.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 19 | 20 | 22 | 23 | 24 | 25 | 27 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |