├── package.bat
├── HaPlugin
├── Resources
│ ├── light_on.png
│ ├── light_off.png
│ ├── switch_off.png
│ ├── switch_on.png
│ ├── lock_locked.png
│ └── lock_unlocked.png
├── metadata
│ ├── Icon16x16.png
│ ├── Icon32x32.png
│ ├── Icon48x48.png
│ ├── Icon256x256.png
│ └── LoupedeckPackage.yaml
├── Json
│ ├── HaEvent.cs
│ └── HaState.cs
├── Events
│ └── StateChangedEventArgs.cs
├── Commands
│ ├── WaterHeaterCommand.cs
│ ├── SensorCommand.cs
│ ├── SceneCommand.cs
│ ├── ButtonCommand.cs
│ ├── SwitchCommand.cs
│ ├── LightCommand.cs
│ ├── LockCommand.cs
│ ├── ScriptCommand.cs
│ ├── AutomationCommand.cs
│ ├── ScriptAdvancedCommand.cs
│ ├── BaseCommand.cs
│ └── DualStateCommand.cs
├── HaApplication.cs
├── Helpers
│ ├── PluginLog.cs
│ └── PluginResources.cs
├── HaConfig.cs
├── Adjustments
│ ├── ClimateAdjustment.cs
│ ├── CoverAdjustment.cs
│ └── DimmerAdjustment.cs
├── HaPlugin.csproj
└── HaPlugin.cs
├── Directory.Build.props
├── LICENSE
├── HomeAssistant.sln
├── README.md
├── .vscode
└── tasks.json
├── .gitignore
└── .editorconfig
/package.bat:
--------------------------------------------------------------------------------
1 | LogiPluginTool.exe pack -input="build\bin\Release" -output="..\HomeAssistant-0.9.lplug4"
2 |
--------------------------------------------------------------------------------
/HaPlugin/Resources/light_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/light_on.png
--------------------------------------------------------------------------------
/HaPlugin/metadata/Icon16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/metadata/Icon16x16.png
--------------------------------------------------------------------------------
/HaPlugin/metadata/Icon32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/metadata/Icon32x32.png
--------------------------------------------------------------------------------
/HaPlugin/metadata/Icon48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/metadata/Icon48x48.png
--------------------------------------------------------------------------------
/HaPlugin/Resources/light_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/light_off.png
--------------------------------------------------------------------------------
/HaPlugin/Resources/switch_off.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/switch_off.png
--------------------------------------------------------------------------------
/HaPlugin/Resources/switch_on.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/switch_on.png
--------------------------------------------------------------------------------
/HaPlugin/metadata/Icon256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/metadata/Icon256x256.png
--------------------------------------------------------------------------------
/HaPlugin/Resources/lock_locked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/lock_locked.png
--------------------------------------------------------------------------------
/HaPlugin/Resources/lock_unlocked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/schmic/Loupedeck-HomeAssistant/HEAD/HaPlugin/Resources/lock_unlocked.png
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | latest
4 | $(SolutionDir)build\obj\
5 | $(SolutionDir)build\bin
6 |
7 |
8 |
--------------------------------------------------------------------------------
/HaPlugin/Json/HaEvent.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Json
2 | {
3 | using Newtonsoft.Json.Linq;
4 |
5 | public class HaEvent
6 | {
7 | public JObject new_state
8 | { set => this.State = value.ToObject(); }
9 |
10 | public HaState State;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/HaPlugin/Events/StateChangedEventArgs.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Events
2 | {
3 | using System;
4 |
5 | public class StateChangedEventArgs(String Entity_Id) : EventArgs
6 | {
7 | public String Entity_Id { get; } = Entity_Id;
8 |
9 | public static StateChangedEventArgs Create(String Entity_Id) => new(Entity_Id);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/WaterHeaterCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | public class WaterHeaterCommand : BaseCommand
6 | {
7 | public WaterHeaterCommand() : base("Sensor") { }
8 |
9 | protected override Boolean EntitiyFilter(String entity_id)
10 | => entity_id.StartsWith("water_heater.");
11 |
12 | protected override void RunCommand(String entity_id) { }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/SensorCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | public class SensorCommand : BaseCommand
6 | {
7 | public SensorCommand() : base("Sensor") { }
8 |
9 | protected override Boolean EntitiyFilter(String entity_id)
10 | => entity_id.StartsWith("sensor.") || entity_id.StartsWith("binary_sensor.");
11 |
12 | protected override void RunCommand(String entity_id) { /** sensors do nothing */ }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/SceneCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using Newtonsoft.Json.Linq;
5 |
6 | public class SceneCommand : BaseCommand
7 | {
8 | public SceneCommand() : base("Scene") { }
9 |
10 | protected override Boolean EntitiyFilter(String entity_id)
11 | => entity_id.StartsWith("scene.");
12 |
13 | protected override void RunCommand(String entity_id)
14 | {
15 | var data = new JObject {
16 | { "domain", "scene" },
17 | { "service", "turn_on" },
18 | { "target", new JObject { { "entity_id", entity_id } } }
19 | };
20 |
21 | this.GetPlugin().CallService(data);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/ButtonCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using Newtonsoft.Json.Linq;
5 |
6 | public class ButtonCommand : BaseCommand
7 | {
8 | public ButtonCommand() : base("Button") { }
9 |
10 | protected override Boolean EntitiyFilter(String entity_id)
11 | => entity_id.StartsWith("button.");
12 |
13 | protected override void RunCommand(String entity_id)
14 | {
15 | var data = new JObject {
16 | { "domain", "button" },
17 | { "service", "press" },
18 | { "target", new JObject { { "entity_id", entity_id } } }
19 | };
20 |
21 | this.GetPlugin().CallService(data);
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/SwitchCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using Newtonsoft.Json.Linq;
5 |
6 | public class SwitchCommand : DualStateCommand
7 | {
8 | public SwitchCommand() : base("Switch")
9 | { }
10 |
11 | protected override Boolean EntitiyFilter(String entity_id)
12 | => entity_id.StartsWith("switch.");
13 |
14 | protected override void RunCommand(String entity_id)
15 | {
16 | var data = new JObject {
17 | { "domain", "switch" },
18 | { "service", "toggle" },
19 | { "target", new JObject { { "entity_id", entity_id } } }
20 | };
21 |
22 | this.GetPlugin().CallService(data);
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/HaPlugin/Commands/LightCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | using Newtonsoft.Json.Linq;
6 |
7 | public class LightCommand : DualStateCommand
8 | {
9 | public LightCommand() : base("Light")
10 | { }
11 |
12 | protected override Boolean EntitiyFilter(String entity_id)
13 | => entity_id.StartsWith("light.");
14 |
15 | protected override void RunCommand(String entity_id)
16 | {
17 | var data = new JObject {
18 | { "domain", "light" },
19 | { "service", "toggle" },
20 | { "target", new JObject { { "entity_id", entity_id } } }
21 | };
22 |
23 | this.GetPlugin().CallService(data);
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/HaPlugin/HaApplication.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin
2 | {
3 | using System;
4 |
5 | // This class can be used to connect the Loupedeck plugin to an application.
6 |
7 | public class HaApplication : ClientApplication
8 | {
9 | public HaApplication()
10 | {
11 | }
12 |
13 | // This method can be used to link the plugin to a Windows application.
14 | protected override String GetProcessName() => "";
15 |
16 | // This method can be used to link the plugin to a macOS application.
17 | protected override String GetBundleName() => "";
18 |
19 | // This method can be used to check whether the application is installed or not.
20 | public override ClientApplicationStatus GetApplicationStatus() => ClientApplicationStatus.Unknown;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/LockCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | using Newtonsoft.Json.Linq;
6 |
7 | public class LockCommand : DualStateCommand
8 | {
9 | // my son decided that the offState is locked :)
10 | public LockCommand() : base("Lock", offState: "locked", onState: "unlocked") { }
11 |
12 | protected override Boolean EntitiyFilter(String entity_id)
13 | => entity_id.StartsWith("lock.");
14 |
15 | protected override void RunCommand(String entity_id)
16 | {
17 | var service = this.GetCurrentState(entity_id).Name.Equals(this.OffState) ? "unlock" : "lock";
18 | var data = new JObject {
19 | { "domain", "lock" },
20 | { "service", service },
21 | { "target", new JObject { { "entity_id", entity_id } } }
22 | };
23 |
24 | this.GetPlugin().CallService(data);
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/HaPlugin/Json/HaState.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Json
2 | {
3 | using System;
4 |
5 | using Newtonsoft.Json.Linq;
6 |
7 | public class HaState
8 | {
9 | public String Entity_Id { get; set; }
10 |
11 | public String State { get; set; }
12 |
13 | public JObject Attributes { get; set; }
14 |
15 | public String FriendlyName
16 | {
17 | get
18 | {
19 | var friendly_name = this.Attributes.Value("friendly_name");
20 | return friendly_name.IsNullOrEmpty() ? $"#{this.Entity_Id}" : friendly_name;
21 | }
22 | }
23 |
24 | public String Icon => this.Attributes.ContainsKey("icon") ? this.Attributes.Value("icon") : "";
25 |
26 | public override String ToString() => this.Entity_Id + " // " + this.State + " // " + this.Attributes;
27 |
28 | public static HaState FromString(String jsonState) => JsonHelpers.DeserializeAnyObject(jsonState);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Michael Scherer
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/HomeAssistant.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.8.34408.163
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HaPlugin", "HaPlugin\HaPlugin.csproj", "{393AC8BA-5A54-4927-8E85-6E40FE8BDB96}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {393AC8BA-5A54-4927-8E85-6E40FE8BDB96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {393AC8BA-5A54-4927-8E85-6E40FE8BDB96}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {393AC8BA-5A54-4927-8E85-6E40FE8BDB96}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {393AC8BA-5A54-4927-8E85-6E40FE8BDB96}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {641F1926-B753-4BEF-AE1F-8C3D6868B389}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/ScriptCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using Newtonsoft.Json.Linq;
5 |
6 | public class ScriptCommand : BaseCommand
7 | {
8 | public ScriptCommand() : base("Script") { }
9 |
10 | protected override Boolean EntitiyFilter(String entity_id)
11 | => entity_id.StartsWith("script.");
12 |
13 | protected override void RunCommand(String entity_id)
14 | {
15 | var data = new JObject {
16 | { "domain", "script" },
17 | { "service", "turn_on" },
18 | { "target", new JObject { { "entity_id", entity_id } } }
19 | };
20 |
21 | this.GetPlugin().CallService(data);
22 | }
23 |
24 | protected override String GetCommandDisplayName(String entity_id, PluginImageSize imageSize)
25 | {
26 | if (entity_id.IsNullOrEmpty())
27 | { return base.GetCommandDisplayName(entity_id, imageSize); }
28 |
29 | var states = this.GetStates();
30 |
31 | var FriendlyName = states[entity_id].FriendlyName;
32 |
33 | return $"{FriendlyName}";
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/HaPlugin/Helpers/PluginLog.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Helpers
2 | {
3 | using System;
4 |
5 | // A helper class that enables logging from the plugin code.
6 |
7 | internal static class PluginLog
8 | {
9 | private static PluginLogFile _pluginLogFile;
10 |
11 | public static void Init(PluginLogFile pluginLogFile)
12 | {
13 | pluginLogFile.CheckNullArgument(nameof(pluginLogFile));
14 | PluginLog._pluginLogFile = pluginLogFile;
15 | }
16 |
17 | public static void Verbose(String text) => PluginLog._pluginLogFile?.Verbose(text);
18 |
19 | public static void Verbose(Exception ex, String text) => PluginLog._pluginLogFile?.Verbose(ex, text);
20 |
21 | public static void Info(String text) => PluginLog._pluginLogFile?.Info(text);
22 |
23 | public static void Info(Exception ex, String text) => PluginLog._pluginLogFile?.Info(ex, text);
24 |
25 | public static void Warning(String text) => PluginLog._pluginLogFile?.Warning(text);
26 |
27 | public static void Warning(Exception ex, String text) => PluginLog._pluginLogFile?.Warning(ex, text);
28 |
29 | public static void Error(String text) => PluginLog._pluginLogFile?.Error(text);
30 |
31 | public static void Error(Exception ex, String text) => PluginLog._pluginLogFile?.Error(ex, text);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/HaPlugin/HaConfig.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin
2 | {
3 | using System;
4 | using System.IO;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Helpers;
8 |
9 | public class HaConfig
10 | {
11 | public String Url { get; set; }
12 |
13 | public Uri ApiUrl
14 | {
15 | get
16 | {
17 | var builder = new UriBuilder(this.Url.Replace("http", "ws"))
18 | {
19 | Path = "/api/websocket",
20 | };
21 | return builder.Uri;
22 | }
23 | }
24 |
25 | public String Token;
26 |
27 | public static HaConfig FromString(String jsonConfig) => JsonHelpers.DeserializeAnyObject(jsonConfig);
28 |
29 | public static HaConfig Read()
30 | {
31 | var DEFAULT_PATH = Path.Combine(".loupedeck", "homeassistant");
32 | var UserProfilePath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
33 | var ConfigFile = Path.Combine(UserProfilePath, DEFAULT_PATH, "homeassistant.json");
34 |
35 | if (!IoHelpers.FileExists(ConfigFile))
36 | {
37 | PluginLog.Error($"Configuration file is missing or unreadable.");
38 | return null;
39 | }
40 |
41 | var Config = JsonHelpers.DeserializeAnyObjectFromFile(ConfigFile);
42 |
43 | return Config;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/AutomationCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | using Loupedeck.HaPlugin.Helpers;
6 |
7 | using Newtonsoft.Json.Linq;
8 |
9 | public class AutomationCommand : DualStateCommand
10 | {
11 | public AutomationCommand() : base("Automation") { }
12 |
13 | protected override Boolean EntitiyFilter(String entity_id)
14 | => entity_id.StartsWith("automation.");
15 |
16 | protected override void RunCommand(String entity_id) => this.CallService("trigger", entity_id);
17 |
18 | protected override Boolean ProcessTouchEvent(String entity_id, DeviceTouchEvent touchEvent)
19 | {
20 | if (touchEvent.IsLongPress())
21 | {
22 | PluginLog.Verbose($"ProcessTouchEvent.IsLongPress: {entity_id}");
23 | this.CallService("toggle", entity_id);
24 | return true;
25 | }
26 |
27 | return false;
28 | }
29 |
30 | protected override BitmapImage GetCommandImage(String entity_id, PluginImageSize imageSize)
31 | {
32 | if (entity_id.IsNullOrEmpty())
33 | { return base.GetCommandImage(entity_id, imageSize); }
34 |
35 | var states = this.GetStates();
36 |
37 | var isOn = states[entity_id].State.Equals("on");
38 | var entity_friendly_name = states[entity_id].FriendlyName;
39 |
40 | var colorRed = new BitmapColor(200, 10, 20);
41 | var colorGreen = new BitmapColor(10, 200, 20);
42 |
43 | var bitmapBuilder = new BitmapBuilder(imageSize);
44 | bitmapBuilder.FillCircle(0, 0, 20, isOn ? colorGreen : colorRed);
45 | bitmapBuilder.DrawText(entity_friendly_name, 0, 0, bitmapBuilder.Width, bitmapBuilder.Height);
46 | return bitmapBuilder.ToImage();
47 | }
48 |
49 | private void CallService(String service, String entity_id)
50 | {
51 | var data = new JObject {
52 | { "domain", "automation" },
53 | { "service", service },
54 | { "target", new JObject { { "entity_id", entity_id } } }
55 | };
56 |
57 | this.GetPlugin().CallService(data);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/ScriptAdvancedCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 |
5 | using Loupedeck.HaPlugin;
6 | using Loupedeck.HaPlugin.Helpers;
7 |
8 | using Newtonsoft.Json.Linq;
9 |
10 | class ScriptAdvancedCommand : ActionEditorCommand
11 | {
12 | public ScriptAdvancedCommand()
13 | {
14 | this.Name = "ScriptAdvancedCommand";
15 | this.DisplayName = "Execute Script with parameters";
16 |
17 | this.ActionEditor.AddControlEx(
18 | new ActionEditorTextbox("script", "Script").SetPlaceholder("Name of script").SetRequired()
19 | );
20 | this.ActionEditor.AddControlEx(
21 | new ActionEditorTextbox("data", "Data", "Must be valid JSON").SetPlaceholder("{ \"message\": \"foobar\" }")
22 | );
23 | }
24 |
25 | protected HaPlugin GetPlugin() => (HaPlugin)base.Plugin;
26 |
27 | protected override String GetCommandDisplayName(ActionEditorActionParameters actionParameters)
28 | {
29 | var scriptName = actionParameters.GetString("script");
30 |
31 | PluginLog.Info(scriptName);
32 | return base.GetCommandDisplayName(actionParameters);
33 | }
34 |
35 | //protected override BitmapImage GetCommandImage(ActionEditorActionParameters actionParameters, Int32 imageWidth, Int32 imageHeight) => base.GetCommandImage(actionParameters, imageWidth, imageHeight);
36 |
37 | protected override Boolean RunCommand(ActionEditorActionParameters actionParameters)
38 | {
39 | var scriptName = actionParameters.GetString("script");
40 |
41 | if (scriptName.IsNullOrEmpty())
42 | {
43 | return false;
44 | }
45 |
46 | var scriptData = actionParameters.GetString("data");
47 | PluginLog.Info($"Executing script: {scriptName} with \"{scriptData}\"");
48 |
49 | JObject scriptJson = null;
50 | if (!scriptData.IsNullOrEmpty())
51 | {
52 | scriptJson = JObject.Parse(scriptData);
53 | }
54 |
55 |
56 | var data = new JObject {
57 | { "domain", "script" },
58 | { "service", "turn_on" },
59 | { "target", new JObject { { "entity_id", $"script.{scriptName}"} } }
60 | };
61 |
62 | if (scriptJson != null)
63 | {
64 | data["service_data"] = new JObject { { "variables", scriptJson} };
65 | }
66 |
67 | this.GetPlugin().CallService(data);
68 |
69 | return true;
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/BaseCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Events;
8 | using Loupedeck.HaPlugin.Helpers;
9 |
10 | public abstract class BaseCommand : PluginDynamicCommand
11 | {
12 | protected BaseCommand(String groupName) : base() => this.GroupName = groupName;
13 |
14 | protected abstract Boolean EntitiyFilter(String entity_id);
15 |
16 | protected abstract override void RunCommand(String entity_id);
17 |
18 | protected override Boolean OnLoad()
19 | {
20 | using (var plugin = base.Plugin as HaPlugin)
21 | {
22 | plugin.StatesReady += this.StatesReady;
23 | plugin.StateChanged += this.StateChanged;
24 | }
25 |
26 | return true;
27 | }
28 |
29 | protected override Boolean OnUnload()
30 | {
31 | using (var plugin = base.Plugin as HaPlugin)
32 | {
33 | plugin.StateChanged -= this.StateChanged;
34 | plugin.StatesReady -= this.StatesReady;
35 | }
36 |
37 | return true;
38 | }
39 |
40 | protected HaPlugin GetPlugin() => (HaPlugin)base.Plugin;
41 |
42 | protected Dictionary GetStates() => this.GetPlugin().States;
43 |
44 | private void StatesReady(Object sender, EventArgs e)
45 | {
46 | PluginLog.Verbose($"{this.GroupName}Command.OnLoad() => StatesReady");
47 |
48 | foreach (KeyValuePair kvp in this.GetStates())
49 | {
50 | if (!this.EntitiyFilter(kvp.Key))
51 | { continue; }
52 |
53 | var state = kvp.Value;
54 | this.AddParameter(state.Entity_Id, state.FriendlyName, this.GroupName);
55 | }
56 |
57 | PluginLog.Info($"[group: {this.GroupName}] [count: {this.GetParameters().Length}]");
58 | }
59 |
60 | private void StateChanged(Object sender, StateChangedEventArgs e)
61 | {
62 | if (!this.EntitiyFilter(e.Entity_Id))
63 | { return; }
64 |
65 | this.ActionImageChanged(e.Entity_Id);
66 | }
67 |
68 | protected override String GetCommandDisplayName(String entity_id, PluginImageSize imageSize)
69 | {
70 | if (entity_id.IsNullOrEmpty())
71 | { return base.GetCommandDisplayName(entity_id, imageSize); }
72 |
73 | var states = this.GetStates();
74 |
75 | var FriendlyName = states[entity_id].FriendlyName;
76 | var State = states[entity_id].State;
77 | var Unit_Of_Measurement = states[entity_id].Attributes["unit_of_measurement"];
78 |
79 | return $"{FriendlyName}\n{State}{Unit_Of_Measurement}";
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/HaPlugin/metadata/LoupedeckPackage.yaml:
--------------------------------------------------------------------------------
1 | # ==================================================================================================
2 | # GENERAL SETTINGS
3 | # ==================================================================================================
4 |
5 | # Package type. Must be plugin4 for plugins.
6 | type: plugin4
7 |
8 | # Name of the plugin.
9 | name: Ha
10 |
11 | # Name that is displayed in the Loupedeck configuration application.
12 | displayName: Home Assistant Websocket
13 |
14 | # Name of the plugin library file.
15 | pluginFileName: HaPlugin.dll
16 |
17 | # Plugin version.
18 | version: 0.9
19 |
20 | # Author of the plugin. The author can be a company or an individual developer.
21 | author: schmic
22 |
23 | # Copyright information.
24 | copyright: Copyright © 2024 . All rights reserved.
25 |
26 |
27 | # ==================================================================================================
28 | # PLUGIN PROPERTIES
29 | # ==================================================================================================
30 |
31 | # Location of plugin files on Windows (relative to the plugin base directory).
32 | # This parameter is required to support Windows.
33 | pluginFolderWin: win
34 |
35 | # Location of plugin files on macOS (relative to the plugin base directory).
36 | # This parameter is required to support Mac.
37 | #pluginFolderMac: mac
38 |
39 | # List of supported devices.
40 | supportedDevices:
41 | # LoupedeckCtFamily covers the following devices: Loupedeck CT, Live and Live S, and Razer Stream Controller.
42 | - LoupedeckCtFamily
43 |
44 | # LoupedeckPlusFamily covers Loupedeck+ device. Uncomment the following line to support Loupedeck+.
45 | #- LoupedeckPlusFamily
46 |
47 | # List of plugin capabilities.
48 | pluginCapabilities:
49 | # Uncomment the following line if this plugin is an application plugin.
50 | #- HasApplication
51 |
52 | # Uncomment the following line if the plugin sends keyboard shortcuts to the target application.
53 | #- ActivatesApplication
54 |
55 | # Minimum Loupedeck version supported by the plugin.
56 | minimumLoupedeckVersion: 6.0
57 |
58 |
59 | # ==================================================================================================
60 | # LOUPEDECK MARKETPLACE SETTINGS
61 | # ==================================================================================================
62 |
63 | # Name of the license that the plugin is licensed under. Select the one that you prefer.
64 | # NOTE: GPL license is not compatible with Loupedeck Marketplace.
65 | license: MIT
66 |
67 | # URL of the plugin license.
68 | licenseUrl: https://opensource.org/licenses/MIT
69 |
70 | # URL of the support page where the users can send improvement suggestions and report bugs.
71 | # The URL is shown in Loupedeck Marketplace. The page can be for example a GitHub issues page.
72 | # NOTE: This setting is recommended when publishing the plugin in Loupedeck Marketplace.
73 | supportPageUrl: https://github.com/schmic/Loupedeck-HomeAssistant/issues
74 |
75 | # URL of the plugin homepage. The URL is shown in Loupedeck Marketplace.
76 | homePageUrl: https://github.com/schmic/Loupedeck-HomeAssistant
77 |
--------------------------------------------------------------------------------
/HaPlugin/Adjustments/ClimateAdjustment.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Adjustments
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Helpers;
8 |
9 | using Newtonsoft.Json.Linq;
10 |
11 | public class ClimateAdjustment : PluginDynamicAdjustment
12 | {
13 | private HaPlugin plugin;
14 |
15 | public ClimateAdjustment() : base(true) => this.GroupName = "Climate";
16 |
17 | protected override Boolean OnLoad()
18 | {
19 | this.plugin = base.Plugin as HaPlugin;
20 |
21 | this.plugin.StatesReady += (sender, e) =>
22 | {
23 | PluginLog.Verbose($"{this.GroupName}Command.OnLoad() => StatesReady");
24 |
25 | foreach (KeyValuePair group in this.plugin.States)
26 | {
27 | var state = group.Value;
28 | if (state.Entity_Id.StartsWith("climate."))
29 | {
30 | this.AddParameter(state.Entity_Id, state.FriendlyName, this.GroupName);
31 | }
32 | }
33 |
34 | PluginLog.Info($"[group: {this.GroupName}] [count: {this.GetParameters().Length}]");
35 | };
36 |
37 | this.plugin.StateChanged += (sender, e) => this.ActionImageChanged(e.Entity_Id);
38 |
39 | return true;
40 | }
41 |
42 | protected override String GetCommandDisplayName(String actionParameter, PluginImageSize imageSize)
43 | {
44 | if (actionParameter.IsNullOrEmpty())
45 | { return null; }
46 |
47 | var entityState = this.plugin.States[actionParameter];
48 | Int32.TryParse(entityState.Attributes["temperature"]?.ToString(), out var entityValue);
49 | return $"{entityState.FriendlyName}\n{entityState.State}/{entityValue}°";
50 | }
51 |
52 | //protected override String GetAdjustmentDisplayName(String actionParameter, PluginImageSize imageSize)
53 | //{
54 | //}
55 |
56 | //protected override String GetAdjustmentValue(String actionParameter)
57 | //{
58 | //}
59 |
60 | protected override void ApplyAdjustment(String entity_id, Int32 changeTemperature)
61 | {
62 | if (entity_id.IsNullOrEmpty())
63 | { return; }
64 |
65 | var entityState = this.plugin.States[entity_id];
66 | Int32.TryParse(entityState.Attributes["temperature"]?.ToString(), out var currentTemperature);
67 | var temperature = currentTemperature + changeTemperature;
68 |
69 | PluginLog.Verbose($"{entity_id} {currentTemperature} => {temperature}");
70 | this.plugin.States[entity_id].Attributes["temperature"] = temperature.ToString();
71 |
72 | this.SetTemperature(entity_id, temperature);
73 | }
74 |
75 | protected override void RunCommand(String entity_id) => this.SetTemperature(entity_id, 18);
76 |
77 | private void SetTemperature(String entity_id, Int32 temperature)
78 | {
79 | var reqData = new JObject {
80 | { "domain", "climate" },
81 | { "service", "set_temperature" },
82 | { "service_data", new JObject { { "temperature", temperature} } },
83 | { "target", new JObject { { "entity_id", entity_id } } }
84 | };
85 |
86 | this.plugin.CallService(reqData);
87 | this.AdjustmentValueChanged();
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/HaPlugin/HaPlugin.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | disable
7 | Loupedeck.HaPlugin
8 |
9 | prompt
10 | 4
11 |
12 | false
13 | true
14 |
15 | C:\Program Files\Logi\LogiPluginService\
16 | /Applications/Utilities/LogiPluginService.app/Contents/MonoBundle/
17 |
18 | $(LocalAppData)\Logi\LogiPluginService\Plugins\
19 | ~/Library/Application\ Support/Logi/LogiPluginService/Plugins/
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | $(PluginApiDir)PluginApi.dll
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/HaPlugin/Helpers/PluginResources.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Helpers
2 | {
3 | using System;
4 | using System.IO;
5 | using System.Reflection;
6 |
7 | // A helper class for managing plugin resources.
8 | // Note that the resource files handled by this class must be embedded in the plugin assembly at compile time.
9 | // That is, the Build Action of the files must be "Embedded Resource" in the plugin project.
10 |
11 | internal static class PluginResources
12 | {
13 | private static Assembly _assembly;
14 |
15 | public static void Init(Assembly assembly)
16 | {
17 | assembly.CheckNullArgument(nameof(assembly));
18 | PluginResources._assembly = assembly;
19 | }
20 |
21 | // Retrieves the names of all the resource files in the specified folder.
22 | // The parameter `folderName` must be specified as a full path, for example, `Loupedeck.HaPlugin.Resources`.
23 | // Returns the full names of the resource files, for example, `Loupedeck.HaPlugin.Resources.Resource.txt`.
24 | public static String[] GetFilesInFolder(String folderName) => PluginResources._assembly.GetFilesInFolder(folderName);
25 |
26 | // Finds the first resource file with the specified file name.
27 | // Returns the full name of the found resource file.
28 | // Throws `FileNotFoundException` if the resource file is not found.
29 | public static String FindFile(String fileName) => PluginResources._assembly.FindFileOrThrow(fileName);
30 |
31 | // Finds all the resource files that match the specified regular expression pattern.
32 | // Returns the full names of the found resource files.
33 | // Example:
34 | // `PluginResources.FindFiles(@"\w+\.txt$")` returns all the resource files with the extension `.txt`.
35 | public static String[] FindFiles(String regexPattern) => PluginResources._assembly.FindFiles(regexPattern);
36 |
37 | // Finds the first resource file with the specified file name, and returns the file as a stream.
38 | // Throws `FileNotFoundException` if the resource file is not found.
39 | public static Stream GetStream(String resourceName) => PluginResources._assembly.GetStream(PluginResources.FindFile(resourceName));
40 |
41 | // Reads content of the specified text file, and returns the file content as a string.
42 | // Throws `FileNotFoundException` if the resource file is not found.
43 | public static String ReadTextFile(String resourceName) => PluginResources._assembly.ReadTextFile(PluginResources.FindFile(resourceName));
44 |
45 | // Reads content of the specified binary file, and returns the file content as bytes.
46 | // Throws `FileNotFoundException` if the resource file is not found.
47 | public static Byte[] ReadBinaryFile(String resourceName) => PluginResources._assembly.ReadBinaryFile(PluginResources.FindFile(resourceName));
48 |
49 | // Reads content of the specified image file, and returns the file content as a bitmap image.
50 | // Throws `FileNotFoundException` if the resource file is not found.
51 | public static BitmapImage ReadImage(String resourceName) => PluginResources._assembly.ReadImage(PluginResources.FindFile(resourceName));
52 |
53 | // Extracts the specified resource file to the given file path in the file system.
54 | // Throws `FileNotFoundException` if the resource file is not found, or a system exception if the output file cannot be written.
55 | public static void ExtractFile(String resourceName, String filePathName)
56 | => PluginResources._assembly.ExtractFile(PluginResources.FindFile(resourceName), filePathName);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Loupedeck-HomeAssistant
2 |
3 | This plugin enables you to control various [Home-Assistant](https://home-assistant.io) entities with your Loupedeck.
4 |
5 | Supported entities:
6 | - Automations
7 | - toggle on & off
8 | - trigger
9 | - Buttons
10 | - press
11 | - Climate
12 | - set `temperature`
13 | - Cover
14 | - toggle open & close
15 | - set `position` with knob
16 | - Dimmers (lights with `brightness` attribute)
17 | - on & off
18 | - set `brightness` with knob
19 | - Lights
20 | - toggle on & off
21 | - Locks
22 | - toggle lock & unlock
23 | - Scene
24 | - turn on
25 | - Script
26 | - execute, simple option via `Script` Folder or via manually configuration with data option
27 | - Sensor
28 | - just visual representation of the state
29 | - Switches
30 | - toggle on & off
31 | Water Heater
32 | - just visual representation of the state
33 |
34 | The plugin communicates via the [Websocket API](https://developers.home-assistant.io/docs/api/websocket/) of Home-Assistant.
35 | State changes are immediately reflect on your Loupedeck display.
36 |
37 | ## Installation
38 | - download the latest .lplug4 asset [here](https://github.com/schmic/Loupedeck-HomeAssistant/releases/latest)
39 | - install into the Loupedeck software
40 | - right click and select `Install Plugin`
41 |
42 | This plugin is not yet published to the official Loupedeck Store.
43 | Only available for Windows, tested with Loupedeck Live.
44 |
45 | ## Configuration
46 |
47 | - Create the following path & file in your home folder:
48 | `C:/Users//.loupedeck/homeassistant/homeassistant.json`
49 | - Copy & Paste the example into the file
50 | - Replace token and URL
51 | - for the URL you can simply use the URL from your browser, the plugin will do the rest
52 | - using the `Nabu Casa-URL` is possible but not recommended, especially in a LAN setup
53 | - you must not add a path to the URL, it will be ignored and replaced with `/api/websocket`
54 |
55 | Examples:
56 | ```
57 | {
58 | "token": "eyJhbGciOiJIUzI1NiIsInR......",
59 | "url": "http://your.home.assistant.host:8123"
60 | }
61 | ```
62 | ```
63 | {
64 | "token": "eyJhbGciOiJIUzI1NiIsInR......",
65 | "url": "https://your.home.assistant.host"
66 | }
67 | ```
68 |
69 | If you are already using [Lubedas Home-Assistant Plugin](https://github.com/lubeda/Loupedeck-HomeAssistantPlugin) you don't have to do anything,
70 | this plugin uses the same type of configuration but ignores any other configuration settings except `token` and `url`.
71 |
72 | ### Home-Assistant Token
73 | To get a token you have to log into your Home-Assistant, then go to your Profile page. At the bottom you can create a token.
74 | Someone explained it [here](https://community.home-assistant.io/t/how-to-get-long-lived-access-token/162159/5) as well.
75 |
76 | ## Logs
77 | From Loupedeck 6.0+ onward you can find the plugin logs in:
78 | - `%LOCALAPPDATA%\Logi\LogiPluginService`
79 |
80 | ## Bugs & Issues
81 | - Climate Control, current state is WIP
82 | - visually different to others, no icons, no colors
83 | - only shows the current state reported from HA
84 | - if you change the value with a knob you won't see the result until it is synced with the thermostat
85 | - with sleeping devices this can take a couple of minutes depending on your configuration
86 |
87 | # Icons
88 | All icons used from the [Material Design Icons](https://pictogrammers.com/docs/general/about/) group and are [licensed](https://github.com/Templarian/MaterialDesign/blob/master/LICENSE) by them.
89 |
90 | # TODOs & IDEAs
91 |
92 | - deal with long `friendly_name` entries better
93 | - override `Reset` for adjustments
94 |
95 | ## Buttons
96 | - are they actually of any use?
97 |
98 | ## Lights
99 | - add support for different modes
100 | - rgbw/led
101 |
102 | # Commandline Paramters
103 | - -enablelogs
104 |
--------------------------------------------------------------------------------
/HaPlugin/Adjustments/CoverAdjustment.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Adjustments
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Helpers;
8 |
9 | using Newtonsoft.Json.Linq;
10 |
11 | public class CoverAdjustment : PluginDynamicAdjustment
12 | {
13 | private HaPlugin plugin;
14 |
15 | public CoverAdjustment() : base(true)
16 | {
17 | this.GroupName = "Cover";
18 | this.ResetDisplayName = "Toggle"; // FIXME: does not apply
19 | }
20 |
21 | protected override Boolean OnLoad()
22 | {
23 | this.plugin = base.Plugin as HaPlugin;
24 |
25 | this.plugin.StatesReady += (sender, e) =>
26 | {
27 | PluginLog.Verbose($"{this.GroupName}Command.OnLoad() => StatesReady");
28 |
29 | foreach (KeyValuePair group in this.plugin.States)
30 | {
31 | var state = group.Value;
32 | if (state.Entity_Id.StartsWith("cover."))
33 | {
34 | this.AddParameter(state.Entity_Id, state.FriendlyName, this.GroupName);
35 | }
36 | }
37 |
38 | PluginLog.Info($"[group: {this.GroupName}] [count: {this.GetParameters().Length}]");
39 | };
40 |
41 | this.plugin.StateChanged += (sender, e) => this.ActionImageChanged(e.Entity_Id);
42 |
43 | return true;
44 | }
45 |
46 | protected override String GetCommandDisplayName(String actionParameter, PluginImageSize imageSize)
47 | {
48 | if (actionParameter.IsNullOrEmpty())
49 | { return null; }
50 |
51 | var entityState = this.plugin.States[actionParameter];
52 | return $"{entityState.State} {entityState.FriendlyName}";
53 | }
54 |
55 | protected override String GetAdjustmentDisplayName(String actionParameter, PluginImageSize imageSize)
56 | {
57 | if (actionParameter.IsNullOrEmpty())
58 | { return null; }
59 |
60 | var entityState = this.plugin.States[actionParameter];
61 | return $"{entityState.FriendlyName}";
62 | }
63 |
64 | protected override String GetAdjustmentValue(String actionParameter)
65 | {
66 | if (actionParameter.IsNullOrEmpty())
67 | { return null; }
68 |
69 | var entityState = this.plugin.States[actionParameter];
70 | Int32.TryParse(entityState.Attributes["current_position"]?.ToString(), out var entityValue);
71 |
72 | return entityValue.ToString();
73 | }
74 |
75 | protected override void ApplyAdjustment(String entity_id, Int32 value)
76 | {
77 | if (entity_id.IsNullOrEmpty())
78 | { return; }
79 |
80 | var entityState = this.plugin.States[entity_id];
81 | PluginLog.Verbose(entityState.ToString());
82 | Int32.TryParse(entityState.Attributes["current_position"]?.ToString(), out var entityValue);
83 | var position = entityValue + value;
84 |
85 | this.plugin.States[entity_id].Attributes["current_position"] = position.ToString();
86 |
87 | var reqData = new JObject {
88 | { "domain", "cover" },
89 | { "service", "set_cover_position" },
90 | { "service_data", new JObject { { "position", position} } },
91 | { "target", new JObject { { "entity_id", entity_id } } }
92 | };
93 |
94 | this.plugin.CallService(reqData);
95 | }
96 |
97 | protected override void RunCommand(String entity_id)
98 | {
99 | var reqData = new JObject {
100 | { "domain", "cover" },
101 | { "service", "toggle" },
102 | { "service_data", new JObject { } },
103 | { "target", new JObject { { "entity_id", entity_id } } }
104 | };
105 |
106 | this.plugin.CallService(reqData);
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/HaPlugin/Adjustments/DimmerAdjustment.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Adjustments
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Helpers;
8 | using Loupedeck.HaPlugin.Json;
9 |
10 | using Newtonsoft.Json.Linq;
11 |
12 | public class DimmerAdjustment : PluginDynamicAdjustment
13 | {
14 | private HaPlugin plugin;
15 |
16 | public DimmerAdjustment() : base(true)
17 | {
18 | this.GroupName = "Dimmer";
19 | this.ResetDisplayName = "Toggle";
20 | }
21 |
22 | protected override Boolean OnLoad()
23 | {
24 | this.plugin = base.Plugin as HaPlugin;
25 |
26 | this.plugin.StatesReady += (sender, e) =>
27 | {
28 | PluginLog.Verbose($"{this.GroupName}Command.OnLoad() => StatesReady");
29 |
30 | foreach (KeyValuePair group in this.plugin.States)
31 | {
32 | var state = group.Value;
33 | if (this.IsDimmer(state))
34 | {
35 | this.AddParameter(state.Entity_Id, state.FriendlyName, "Dimmer");
36 | }
37 | }
38 |
39 | PluginLog.Info($"[group: {this.GroupName}] [count: {this.GetParameters().Length}]");
40 | };
41 |
42 | this.plugin.StateChanged += (sender, e) => this.ActionImageChanged(e.Entity_Id);
43 |
44 | return true;
45 | }
46 |
47 | private Boolean IsDimmer(HaState state) => state.Entity_Id.StartsWith("light.") && state.Attributes.ContainsKey("brightness");
48 |
49 | protected override String GetCommandDisplayName(String entity_id, PluginImageSize imageSize)
50 | {
51 | if (entity_id.IsNullOrEmpty())
52 | { return null; }
53 |
54 | var entityState = this.plugin.States[entity_id];
55 | return $"{entityState.FriendlyName}";
56 | }
57 |
58 | protected override String GetAdjustmentDisplayName(String entity_id, PluginImageSize imageSize)
59 | {
60 | if (entity_id.IsNullOrEmpty())
61 | { return null; }
62 |
63 | var entityState = this.plugin.States[entity_id];
64 | return $"{entityState.FriendlyName}";
65 | }
66 |
67 | protected override String GetAdjustmentValue(String entity_id)
68 | {
69 | if (entity_id.IsNullOrEmpty())
70 | { return null; }
71 |
72 | var entityState = this.plugin.States[entity_id];
73 | var entityValue = AsInt(entityState, "brightness");
74 | return entityValue.ToString();
75 | }
76 |
77 | protected override void ApplyAdjustment(String entity_id, Int32 value)
78 | {
79 | if (entity_id.IsNullOrEmpty())
80 | { return; }
81 |
82 | var entityState = this.plugin.States[entity_id];
83 | var entityValue = AsInt(entityState, "brightness");
84 | var brightness = entityValue + value;
85 |
86 | PluginLog.Verbose($"{entity_id} brightness {entityValue} => {brightness}");
87 |
88 | this.plugin.States[entity_id].Attributes["brightness"] = brightness.ToString();
89 |
90 | var data = new JObject {
91 | { "domain", "light" },
92 | { "service", "turn_on" },
93 | { "service_data", new JObject { { "brightness", brightness } } },
94 | { "target", new JObject { { "entity_id", entity_id } } }
95 | };
96 |
97 | this.plugin.CallService(data);
98 | }
99 |
100 | protected override void RunCommand(String entity_id)
101 | {
102 | var data = new JObject {
103 | { "domain", "light" },
104 | { "service", "toggle" },
105 | { "target", new JObject { { "entity_id", entity_id } } }
106 | };
107 |
108 | this.plugin.CallService(data);
109 | }
110 |
111 | private static Int32 AsInt(HaState entityState, String attributeName)
112 | {
113 | var entityValueF = Convert.ToSingle(entityState.Attributes[attributeName].ToString());
114 | var entityValue = Convert.ToInt32(entityValueF);
115 | return entityValue;
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "stop-loupedeck",
6 | "type": "shell",
7 | // "dependsOn": ["stop-pluginservice"],
8 | "osx": {
9 | "command": "/usr/bin/pkill -x Loupedeck"
10 | },
11 | "windows": {
12 | "command": "Taskkill /IM \"Loupedeck.exe\" /T /F"
13 | }
14 | },
15 | {
16 | "label": "stop-pluginservice",
17 | "type": "shell",
18 | "osx": {
19 | "command": "/usr/bin/pkill -x LogiPluginService"
20 | },
21 | "windows": {
22 | "command": "Taskkill /IM \"LogiPluginService.exe\" /T /F"
23 | }
24 | },
25 | {
26 | "label": "debug-plugin",
27 | "osx": {
28 | // "command": "/Applications/Loupedeck.app/Contents/MacOS/Loupedeck"
29 | "command": "/Applications/Utilities/LogiPluginService.app/Contents/MacOS/LogiPluginService"
30 | },
31 | "windows": {
32 | "command": "wt 'C:\\Program Files\\Logi\\LogiPluginService\\LogiPluginService.exe'"
33 | // "command": "wt 'C:\\Program Files (x86)\\Loupedeck\\Loupedeck2\\configui2\\Loupedeck.exe'"
34 | },
35 | "type": "shell",
36 | "dependsOn": [
37 | "build-debug"
38 | ],
39 | "isBackground": true,
40 | "problemMatcher": {
41 | "owner": "custom",
42 | "fileLocation": [
43 | "absolute"
44 | ],
45 | "pattern": [
46 | {
47 | "regexp": "^\\d{4}-\\d{2}-\\d{2}T(\\d{2}-\\d{2}-\\d{2})-(\\d+) \\|\\s+(\\d+) \\|\\s+([A-Z]+) \\|\\s+(.*)$",
48 | "severity": 4,
49 | "line": 0,
50 | "column": 1,
51 | "message": 5,
52 | "code": 4
53 | },
54 | {
55 | "regexp": "^\\d{2}-\\d{2}-\\d{2}-(\\d+) \\[CN\\] File name is '(.*)'",
56 | "file": 2,
57 | "message": 2
58 | },
59 | {
60 | "regexp": "^\\d{2}-\\d{2}-\\d{2}-(\\d+) \\[WS\\] (.*)$",
61 | "message": 2
62 | },
63 | {
64 | "regexp": "^\\d{2}-\\d{2}-\\d{2}-(\\d+) Waiting \\(locking\\) main thread\\.\\.\\.$",
65 | "message": 0
66 | }
67 | ],
68 | "background": {
69 | "activeOnStart": true,
70 | "beginsPattern": "^\\d{2}-\\d{2}-\\d{2}-(\\d+) Initializing Loupedeck connection",
71 | "endsPattern": "^\\d{2}-\\d{2}-\\d{2}-(\\d+) Waiting \\(locking\\) main thread\\.\\.\\.$"
72 | }
73 | }
74 | },
75 | {
76 | "label": "build-debug",
77 | "type": "shell",
78 | "command": "dotnet",
79 | "osx": {
80 | "args": [
81 | "build",
82 | "${workspaceFolder}/HomeAssistant.sln", // Adjust to your path
83 | "/property:GenerateFullPaths=true",
84 | "/consoleloggerparameters:NoSummary",
85 | "/p:Configuration=Debug",
86 | "/p:Platform=\"Any CPU\""
87 | ]
88 | },
89 | "windows": {
90 | "args": [
91 | "build",
92 | "${workspaceFolder}\\HomeAssistant.sln", // Adjust to your path
93 | "/property:GenerateFullPaths=true",
94 | "/consoleloggerparameters:NoSummary",
95 | "/p:Configuration=Debug"
96 | ]
97 | },
98 | "problemMatcher": "$msCompile"
99 | },
100 | {
101 | "label": "build-release",
102 | "command": "dotnet",
103 | "type": "shell",
104 | "osx": {
105 | "args": [
106 | "build",
107 | "${workspaceFolder}/HomeAssistant.sln", // Adjust to your path
108 | "/property:GenerateFullPaths=true",
109 | "/consoleloggerparameters:NoSummary",
110 | "/p:Configuration=Release",
111 | "/p:Platform=\"Any CPU\""
112 | ]
113 | },
114 | "windows": {
115 | "args": [
116 | "build",
117 | "${workspaceFolder}\\HomeAssistant.sln", // Adjust to your path
118 | "/property:GenerateFullPaths=true",
119 | "/consoleloggerparameters:NoSummary",
120 | "/p:Configuration=Release"
121 | ]
122 | },
123 | "problemMatcher": "$msCompile"
124 | },
125 | {
126 | "label": "build",
127 | "command": "dotnet",
128 | "type": "process",
129 | "args": [
130 | "build",
131 | "${workspaceFolder}/HomeAssistant.sln",
132 | "/property:GenerateFullPaths=true",
133 | "/consoleloggerparameters:NoSummary;ForceNoAlign"
134 | ],
135 | "problemMatcher": "$msCompile"
136 | },
137 | {
138 | "label": "publish",
139 | "command": "dotnet",
140 | "type": "process",
141 | "args": [
142 | "publish",
143 | "${workspaceFolder}/HomeAssistant.sln",
144 | "/property:GenerateFullPaths=true",
145 | "/consoleloggerparameters:NoSummary;ForceNoAlign"
146 | ],
147 | "problemMatcher": "$msCompile"
148 | },
149 | {
150 | "label": "watch",
151 | "command": "dotnet",
152 | "type": "process",
153 | "args": [
154 | "watch",
155 | "run",
156 | "--project",
157 | "${workspaceFolder}/HomeAssistant.sln"
158 | ],
159 | "problemMatcher": "$msCompile"
160 | }
161 | ]
162 | }
163 |
--------------------------------------------------------------------------------
/HaPlugin/Commands/DualStateCommand.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin.Commands
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin;
7 | using Loupedeck.HaPlugin.Events;
8 | using Loupedeck.HaPlugin.Helpers;
9 |
10 | public abstract class DualStateCommand : PluginMultistateDynamicCommand
11 | {
12 | protected readonly String OffState;
13 | protected readonly String OffLabel;
14 |
15 | protected readonly String OnState;
16 | protected readonly String OnLabel;
17 |
18 | public DualStateCommand(String groupName, String offState = "off", String onState = "on") : base()
19 | {
20 | this.GroupName = groupName;
21 | this.OffState = offState;
22 | this.OnState = onState;
23 |
24 | this.OffLabel = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(this.OffState.ToLower());
25 | this.OnLabel = System.Globalization.CultureInfo.CurrentCulture.TextInfo.ToTitleCase(this.OnState.ToLower());
26 |
27 | this.AddState(this.OffState, this.OffLabel, $"{this.GroupName} is {this.OffState}.");
28 | this.AddState(this.OnState, this.OnLabel, $"{this.GroupName} is {this.OnState}.");
29 | }
30 | protected abstract Boolean EntitiyFilter(String entity_id);
31 |
32 | protected override Boolean OnLoad()
33 | {
34 | using (var plugin = base.Plugin as HaPlugin)
35 | {
36 | plugin.StatesReady += this.StatesReady;
37 | plugin.StateChanged += this.StateChanged;
38 | }
39 |
40 | return true;
41 | }
42 |
43 | protected override Boolean OnUnload()
44 | {
45 | using (var plugin = base.Plugin as HaPlugin)
46 | {
47 | plugin.StateChanged -= this.StateChanged;
48 | plugin.StatesReady -= this.StatesReady;
49 | }
50 |
51 | return true;
52 | }
53 |
54 | protected HaPlugin GetPlugin() => (HaPlugin)base.Plugin;
55 |
56 | protected Dictionary GetStates() => this.GetPlugin().States;
57 |
58 | private void StatesReady(Object sender, EventArgs e)
59 | {
60 | PluginLog.Verbose($"{this.GroupName}Command.OnLoad() => StatesReady");
61 |
62 | foreach (KeyValuePair kvp in this.GetStates())
63 | {
64 | if (!this.EntitiyFilter(kvp.Key))
65 | { continue; }
66 |
67 | var state = kvp.Value;
68 | this.AddParameter(state.Entity_Id, state.FriendlyName, this.GroupName);
69 | }
70 |
71 | PluginLog.Info($"[group: {this.GroupName}] [offLabel: {this.OffLabel}] [onLabel: {this.OnLabel}] [count: {this.GetParameters().Length}]");
72 | }
73 |
74 | private void StateChanged(Object sender, StateChangedEventArgs e)
75 | {
76 | if (!this.EntitiyFilter(e.Entity_Id))
77 | { return; }
78 |
79 | var states = this.GetStates();
80 |
81 | var entity_state = states[e.Entity_Id].State;
82 | var state_idx = entity_state.Equals(this.OnState) ? 1 : 0;
83 |
84 | this.SetCurrentState(e.Entity_Id, state_idx);
85 | this.ActionImageChanged(e.Entity_Id);
86 | }
87 |
88 | protected override BitmapImage GetCommandImage(String entity_id, PluginImageSize imageSize)
89 | {
90 | if (entity_id.IsNullOrEmpty())
91 | { return base.GetCommandImage(entity_id, imageSize); }
92 |
93 | var states = this.GetStates();
94 |
95 | var isOn = states[entity_id].State.Equals(this.OnState);
96 | var entity_friendly_name = states[entity_id].FriendlyName;
97 |
98 | var entity_img = isOn ?
99 | EmbeddedResources.ReadImage($"Loupedeck.HaPlugin.Resources.{this.GroupName.ToLower()}_{this.OnState}.png") :
100 | EmbeddedResources.ReadImage($"Loupedeck.HaPlugin.Resources.{this.GroupName.ToLower()}_{this.OffState}.png");
101 |
102 | var bitmapBuilder = new BitmapBuilder(imageSize);
103 | bitmapBuilder.DrawImage(entity_img, bitmapBuilder.Width / 2 - entity_img.Width / 2, 4);
104 | bitmapBuilder.DrawText(entity_friendly_name, 0, bitmapBuilder.Height / 4, bitmapBuilder.Width, bitmapBuilder.Height);
105 | return bitmapBuilder.ToImage();
106 | }
107 |
108 | protected override String GetCommandDisplayName(String entity_id, PluginImageSize imageSize)
109 | {
110 | if (entity_id.IsNullOrEmpty())
111 | { return ""; }
112 |
113 | var states = this.GetStates();
114 |
115 | var FriendlyName = states[entity_id].FriendlyName;
116 | return $"{FriendlyName}";
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/HaPlugin/HaPlugin.cs:
--------------------------------------------------------------------------------
1 | namespace Loupedeck.HaPlugin
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 |
6 | using Loupedeck.HaPlugin.Events;
7 | using Loupedeck.HaPlugin.Json;
8 | using Loupedeck.HaPlugin.Helpers;
9 |
10 | using Newtonsoft.Json.Linq;
11 |
12 | using Websocket.Client;
13 | using System.Reactive.Linq;
14 | using Newtonsoft.Json;
15 |
16 | public class HaPlugin : Plugin
17 | {
18 | // Gets a value indicating whether this is an API-only plugin.
19 | public override Boolean UsesApplicationApiOnly => true;
20 |
21 | // Gets a value indicating whether this is a Universal plugin or an Application plugin.
22 | public override Boolean HasNoApplication => true;
23 |
24 | private String Token;
25 | private WebsocketClient WebSocket;
26 |
27 | public HaPlugin()
28 | {
29 | PluginLog.Init(this.Log);
30 | PluginResources.Init(this.Assembly);
31 | }
32 |
33 | public override void Load()
34 | {
35 | var Config = HaConfig.Read();
36 |
37 | if (Config == null)
38 | {
39 | this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Configuration could not be read.", "https://github.com/schmic/Loupedeck-HomeAssistant", "Help");
40 | return;
41 | }
42 | else if (Config.Token.IsNullOrEmpty())
43 | {
44 | this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Configuration is missing token.", "https://github.com/schmic/Loupedeck-HomeAssistant", "Help");
45 | return;
46 | }
47 | else if (Config.Url.IsNullOrEmpty())
48 | {
49 | this.OnPluginStatusChanged(Loupedeck.PluginStatus.Error, "Configuration is missing url.", "https://github.com/schmic/Loupedeck-HomeAssistant", "Help");
50 | return;
51 | }
52 |
53 | this.Token = Config.Token;
54 | this.SetupWebsocket(Config.ApiUrl);
55 | }
56 |
57 | public override void Unload() => this.WebSocket.Dispose();
58 |
59 | private enum Events : Int32
60 | {
61 | get_states = 1001,
62 | subscribe_events = 1002,
63 | }
64 |
65 | private Int32 Event_Id = 2000;
66 |
67 | public Dictionary States = new Dictionary();
68 | public event EventHandler StatesReady;
69 | public event EventHandler StateChanged;
70 |
71 | private void SetupWebsocket(Uri url)
72 | {
73 | PluginLog.Verbose($"SetupWebsocket(): [url: {url}] [token: {this.Token.Substring(0, 16)}...]");
74 | this.WebSocket = new WebsocketClient(url);
75 |
76 | this.WebSocket.ReconnectTimeout = TimeSpan.FromSeconds(15);
77 | this.WebSocket.ErrorReconnectTimeout = TimeSpan.FromSeconds(15);
78 |
79 | this.WebSocket.ReconnectionHappened.Subscribe(info =>
80 | PluginLog.Info($"Reconnection happened, type: {info.Type}"));
81 | this.WebSocket.DisconnectionHappened.Subscribe(info =>
82 | PluginLog.Info(info.Exception, $"Disconnect happened, type: {info.Type}"));
83 |
84 | this.WebSocket.MessageReceived.Subscribe(this.OnMessageHdl);
85 |
86 | this.WebSocket.Start();
87 | }
88 |
89 | private void OnMessageHdl(ResponseMessage msg)
90 | {
91 | var respData = JObject.Parse(msg.Text);
92 | var respType = (String)respData["type"];
93 |
94 | if (respType.Equals("event"))
95 | {
96 | var evtType = (String)respData["event"]["event_type"];
97 |
98 | if (evtType.Equals("state_changed"))
99 | {
100 | try
101 | {
102 | var haEvent = respData["event"]["data"].ToObject();
103 | var haState = haEvent.State;
104 | this.States[haState.Entity_Id] = haState;
105 | this.StateChanged?.Invoke(null, StateChangedEventArgs.Create(haState.Entity_Id));
106 | }
107 | catch (JsonSerializationException)
108 | {
109 | PluginLog.Warning($"JsonSerializationException HaEvent: {respData["event"]["data"]}");
110 | }
111 | }
112 | }
113 | else if (respType.Equals("result"))
114 | {
115 | var result_id = Int32.Parse(respData["id"].ToString());
116 | var result_event = (Events)result_id;
117 | var result_status = (Boolean)respData["success"];
118 |
119 | if (result_event.Equals(Events.get_states))
120 | {
121 | PluginLog.Info("States Update Success: " + result_status);
122 |
123 | foreach (JToken t in respData["result"].Children())
124 | {
125 | var haState = t.ToObject();
126 | //PluginLog.Verbose("state.ToString(): " + haState.ToString());
127 | this.States[haState.Entity_Id] = haState;
128 | }
129 |
130 | PluginLog.Info($"Received {this.States.Count} states");
131 | StatesReady.Invoke(null, null);
132 | }
133 | else if (result_event.Equals(Events.subscribe_events))
134 | {
135 | PluginLog.Info("Event Subscription Success: " + result_status);
136 | }
137 | //else
138 | //{
139 | // PluginLog.Warning($"Unknown ResultID: {result_id}\n{data}");
140 | //}
141 | }
142 | else if (respType.Equals("auth_required"))
143 | {
144 | var auth = new JObject
145 | {
146 | ["type"] = "auth",
147 | ["access_token"] = this.Token
148 | };
149 | PluginLog.Info("Sending auth token ..");
150 | this.WebSocket.Send(auth.ToString());
151 | }
152 | else if (respType.Equals("auth_ok"))
153 | {
154 | PluginLog.Info("Socket Auth: OK (HA Version: " + respData["ha_version"] + ")");
155 |
156 | var get_states = new JObject
157 | {
158 | ["id"] = (Int32)Events.get_states,
159 | ["type"] = Events.get_states.ToString()
160 | };
161 | this.WebSocket.Send(get_states.ToString());
162 |
163 | var subscribe = new JObject
164 | {
165 | ["id"] = (Int32)Events.subscribe_events,
166 | ["type"] = Events.subscribe_events.ToString()
167 | };
168 | this.WebSocket.Send(subscribe.ToString());
169 | }
170 | else
171 | {
172 | PluginLog.Warning($"### HA unknown response type {respType}\n{respData}");
173 | }
174 | }
175 |
176 | public void CallService(JObject data)
177 | {
178 | if (!data.ContainsKey("type"))
179 | {
180 | data.Add("type", "call_service");
181 | }
182 | PluginLog.Verbose(data.ToString());
183 | this.Send(data);
184 | }
185 |
186 | private void Send(JObject data)
187 | {
188 | data.Add("id", ++this.Event_Id);
189 | this.WebSocket.Send(data.ToString());
190 | }
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
68 |
69 | # StyleCop
70 | StyleCopReport.xml
71 |
72 | # Files built by Visual Studio
73 | *_i.c
74 | *_p.c
75 | *_h.h
76 | *.ilk
77 | *.meta
78 | *.obj
79 | *.iobj
80 | *.pch
81 | *.pdb
82 | *.ipdb
83 | *.pgc
84 | *.pgd
85 | *.rsp
86 | *.sbr
87 | *.tlb
88 | *.tli
89 | *.tlh
90 | *.tmp
91 | *.tmp_proj
92 | *_wpftmp.csproj
93 | *.log
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Remove the line below if you want to inherit .editorconfig settings from higher directories
2 | root = true
3 |
4 | # C# files
5 | [*.cs]
6 |
7 | #### Core EditorConfig Options ####
8 |
9 | # Indentation and spacing
10 | indent_size = 4
11 | indent_style = space
12 | tab_width = 4
13 |
14 | # New line preferences
15 | end_of_line = crlf
16 | insert_final_newline = false
17 |
18 | #### .NET Coding Conventions ####
19 |
20 | # Organize usings
21 | dotnet_separate_import_directive_groups = true
22 | dotnet_sort_system_directives_first = true
23 |
24 | # this. and Me. preferences
25 | dotnet_style_qualification_for_event = true:warning
26 | dotnet_style_qualification_for_field = true:warning
27 | dotnet_style_qualification_for_method = true:warning
28 | dotnet_style_qualification_for_property = true:warning
29 |
30 | # Language keywords vs BCL types preferences
31 | dotnet_style_predefined_type_for_locals_parameters_members = false:warning
32 | dotnet_style_predefined_type_for_member_access = false:warning
33 |
34 | # Parentheses preferences
35 | dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:warning
36 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
37 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
39 |
40 | # Modifier preferences
41 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
42 |
43 | # Expression-level preferences
44 | csharp_style_deconstructed_variable_declaration = true:suggestion
45 | csharp_style_inlined_variable_declaration = true:warning
46 | csharp_style_throw_expression = true:warning
47 | dotnet_style_coalesce_expression = true:warning
48 | dotnet_style_collection_initializer = true:warning
49 | dotnet_style_explicit_tuple_names = true:warning
50 | dotnet_style_null_propagation = true:warning
51 | dotnet_style_object_initializer = false:suggestion
52 | dotnet_style_prefer_auto_properties = true:warning
53 | dotnet_style_prefer_compound_assignment = true:warning
54 | dotnet_style_prefer_conditional_expression_over_assignment = true:warning
55 | dotnet_style_prefer_conditional_expression_over_return = true:warning
56 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
57 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
58 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning
59 |
60 | # Field preferences
61 | dotnet_style_readonly_field = true:warning
62 |
63 | # Parameter preferences
64 | dotnet_code_quality_unused_parameters = all:warning
65 |
66 | #### C# Coding Conventions ####
67 |
68 | # var preferences
69 | csharp_style_var_elsewhere = true:silent
70 | csharp_style_var_for_built_in_types = true:warning
71 | csharp_style_var_when_type_is_apparent = true:warning
72 |
73 | # Expression-bodied members
74 | csharp_style_expression_bodied_accessors = when_on_single_line:suggestion
75 | csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
76 | csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
77 | csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion
78 | csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
79 | csharp_style_expression_bodied_methods = when_on_single_line:suggestion
80 | csharp_style_expression_bodied_operators = when_on_single_line:suggestion
81 | csharp_style_expression_bodied_properties = when_on_single_line:suggestion
82 |
83 | # Pattern matching preferences
84 | csharp_style_pattern_matching_over_as_with_null_check = true:warning
85 | csharp_style_pattern_matching_over_is_with_cast_check = true:warning
86 |
87 | # Null-checking preferences
88 | csharp_style_conditional_delegate_call = true:warning
89 |
90 | # Modifier preferences
91 | csharp_prefer_static_local_function = false:warning
92 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async
93 |
94 | # Code-block preferences
95 | csharp_prefer_braces = true:warning
96 | csharp_prefer_simple_using_statement = false:warning
97 |
98 | # Expression-level preferences
99 | csharp_prefer_simple_default_expression = true:warning
100 | csharp_style_pattern_local_over_anonymous_function = true:warning
101 | csharp_style_prefer_index_operator = true:suggestion
102 | csharp_style_prefer_range_operator = true:suggestion
103 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion
104 | csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
105 |
106 | # 'using' directive preferences
107 | csharp_using_directive_placement = inside_namespace:warning
108 |
109 | #### C# Formatting Rules ####
110 |
111 | # New line preferences
112 | csharp_new_line_before_catch = true
113 | csharp_new_line_before_else = true
114 | csharp_new_line_before_finally = true
115 | csharp_new_line_before_members_in_anonymous_types = true
116 | csharp_new_line_before_members_in_object_initializers = true
117 | csharp_new_line_before_open_brace = all
118 | csharp_new_line_between_query_expression_clauses = true
119 |
120 | # Indentation preferences
121 | csharp_indent_block_contents = true
122 | csharp_indent_braces = false
123 | csharp_indent_case_contents = true
124 | csharp_indent_case_contents_when_block = false
125 | csharp_indent_labels = one_less_than_current
126 | csharp_indent_switch_labels = true
127 |
128 | # Space preferences
129 | csharp_space_after_cast = false
130 | csharp_space_after_colon_in_inheritance_clause = true
131 | csharp_space_after_comma = true
132 | csharp_space_after_dot = false
133 | csharp_space_after_keywords_in_control_flow_statements = true
134 | csharp_space_after_semicolon_in_for_statement = true
135 | csharp_space_around_binary_operators = before_and_after
136 | csharp_space_around_declaration_statements = false
137 | csharp_space_before_colon_in_inheritance_clause = true
138 | csharp_space_before_comma = false
139 | csharp_space_before_dot = false
140 | csharp_space_before_open_square_brackets = false
141 | csharp_space_before_semicolon_in_for_statement = false
142 | csharp_space_between_empty_square_brackets = false
143 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
144 | csharp_space_between_method_call_name_and_opening_parenthesis = false
145 | csharp_space_between_method_call_parameter_list_parentheses = false
146 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
147 | csharp_space_between_method_declaration_name_and_open_parenthesis = false
148 | csharp_space_between_method_declaration_parameter_list_parentheses = false
149 | csharp_space_between_parentheses = false
150 | csharp_space_between_square_brackets = false
151 |
152 | # Wrapping preferences
153 | csharp_preserve_single_line_blocks = true
154 | csharp_preserve_single_line_statements = false
155 |
156 | #### Naming styles ####
157 |
158 | # Naming rules
159 |
160 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
161 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
162 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
163 |
164 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
165 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
166 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
167 |
168 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
169 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
170 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
171 |
172 | # Symbol specifications
173 |
174 | dotnet_naming_symbols.interface.applicable_kinds = interface
175 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal
176 | dotnet_naming_symbols.interface.required_modifiers =
177 |
178 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
179 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal
180 | dotnet_naming_symbols.types.required_modifiers =
181 |
182 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
183 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal
184 | dotnet_naming_symbols.non_field_members.required_modifiers =
185 |
186 | # Naming styles
187 |
188 | dotnet_naming_style.pascal_case.required_prefix =
189 | dotnet_naming_style.pascal_case.required_suffix =
190 | dotnet_naming_style.pascal_case.word_separator =
191 | dotnet_naming_style.pascal_case.capitalization = pascal_case
192 |
193 | dotnet_naming_style.begins_with_i.required_prefix = I
194 | dotnet_naming_style.begins_with_i.required_suffix =
195 | dotnet_naming_style.begins_with_i.word_separator =
196 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
197 |
198 | #### Diagnostic configuration ####
199 |
200 | dotnet_diagnostic.CA1028.severity = none
201 | dotnet_diagnostic.CA1031.severity = none
202 | dotnet_diagnostic.CA1054.severity = none
203 | dotnet_diagnostic.CA1056.severity = none
204 | dotnet_diagnostic.CA1060.severity = none
205 | dotnet_diagnostic.CA1303.severity = none
206 | dotnet_diagnostic.CA1308.severity = none
207 | dotnet_diagnostic.CA1716.severity = none
208 | dotnet_diagnostic.CA1720.severity = none
209 | dotnet_diagnostic.CA2101.severity = none
210 | dotnet_diagnostic.CA2234.severity = none
211 | dotnet_diagnostic.CA5350.severity = none
212 | dotnet_diagnostic.CA9998.severity = none
213 |
214 | dotnet_diagnostic.CS1591.severity = none
215 |
216 | dotnet_diagnostic.IDE0002.severity = none
217 | dotnet_diagnostic.IDE0021.severity = warning
218 | dotnet_diagnostic.IDE0022.severity = warning
219 | dotnet_diagnostic.IDE0058.severity = none
220 | dotnet_diagnostic.IDE0059.severity = none
221 |
222 | dotnet_diagnostic.SA1108.severity = none
223 | dotnet_diagnostic.SA1117.severity = none
224 | dotnet_diagnostic.SA1121.severity = none
225 | dotnet_diagnostic.SA1122.severity = none
226 | dotnet_diagnostic.SA1131.severity = none
227 | dotnet_diagnostic.SA1201.severity = none
228 | dotnet_diagnostic.SA1202.severity = none
229 | dotnet_diagnostic.SA1203.severity = none
230 | dotnet_diagnostic.SA1204.severity = none
231 | dotnet_diagnostic.SA1214.severity = none
232 | dotnet_diagnostic.SA1309.severity = none
233 | dotnet_diagnostic.SA1512.severity = none
234 | dotnet_diagnostic.SA1513.severity = none
235 | dotnet_diagnostic.SA1600.severity = none
236 | dotnet_diagnostic.SA1601.severity = none
237 | dotnet_diagnostic.SA1602.severity = none
238 | dotnet_diagnostic.SA1629.severity = none
239 | dotnet_diagnostic.SA1633.severity = none
240 |
241 | dotnet_diagnostic.SX1309.severity = warning
242 | dotnet_diagnostic.SX1309S.severity = warning
243 |
--------------------------------------------------------------------------------