From 73f504ca93a3aeeda0518bf02cf4ea72686299c6 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Fri, 3 May 2024 21:33:12 -0400 Subject: [PATCH] 1.1.16 - Add MQTT audio support --- OmniLinkBridge/Global.cs | 3 + OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs | 6 +- .../MQTT/HomeAssistant/BinarySensor.cs | 5 + OmniLinkBridge/MQTT/HomeAssistant/Button.cs | 17 ++ OmniLinkBridge/MQTT/HomeAssistant/Climate.cs | 5 + OmniLinkBridge/MQTT/HomeAssistant/Device.cs | 11 +- OmniLinkBridge/MQTT/HomeAssistant/Light.cs | 5 + OmniLinkBridge/MQTT/HomeAssistant/Lock.cs | 5 + OmniLinkBridge/MQTT/HomeAssistant/Number.cs | 8 +- OmniLinkBridge/MQTT/HomeAssistant/Select.cs | 16 ++ OmniLinkBridge/MQTT/HomeAssistant/Sensor.cs | 8 +- OmniLinkBridge/MQTT/HomeAssistant/Switch.cs | 5 + OmniLinkBridge/MQTT/MappingExtensions.cs | 149 +++++++++++++++--- OmniLinkBridge/MQTT/MessageProcessor.cs | 107 +++++++++++-- OmniLinkBridge/MQTT/Parser/CommandTypes.cs | 3 +- OmniLinkBridge/MQTT/Parser/Topic.cs | 6 + OmniLinkBridge/Modules/LoggerModule.cs | 17 +- OmniLinkBridge/Modules/MQTTModule.cs | 140 ++++++++++++++-- OmniLinkBridge/Modules/OmniLinkII.cs | 25 +++ .../OmniLink/AudioZoneStatusEventArgs.cs | 11 ++ OmniLinkBridge/OmniLink/SystemEventType.cs | 8 +- .../OmniLink/SystemStatusEventArgs.cs | 3 +- OmniLinkBridge/OmniLinkBridge.csproj | 3 + OmniLinkBridge/OmniLinkBridge.ini | 6 + OmniLinkBridge/Properties/AssemblyInfo.cs | 4 +- OmniLinkBridge/Settings.cs | 19 +++ OmniLinkBridgeTest/MQTTTest.cs | 123 ++++++++++++++- OmniLinkBridgeTest/Mock/MockOmniLinkII.cs | 5 + .../Mock/SendCommandEventArgs.cs | 12 ++ OmniLinkBridgeTest/SettingsTest.cs | 16 +- README.md | 31 ++++ 31 files changed, 708 insertions(+), 74 deletions(-) create mode 100644 OmniLinkBridge/MQTT/HomeAssistant/Button.cs create mode 100644 OmniLinkBridge/MQTT/HomeAssistant/Select.cs create mode 100644 OmniLinkBridge/OmniLink/AudioZoneStatusEventArgs.cs diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs index 05646a5..8aad965 100644 --- a/OmniLinkBridge/Global.cs +++ b/OmniLinkBridge/Global.cs @@ -34,6 +34,7 @@ public abstract class Global public static bool verbose_unit; public static bool verbose_message; public static bool verbose_lock; + public static bool verbose_audio; // mySQL Logging public static bool mysql_logging; @@ -59,6 +60,8 @@ public abstract class Global public static HashSet mqtt_discovery_area_code_required; public static ConcurrentDictionary mqtt_discovery_override_zone; public static ConcurrentDictionary mqtt_discovery_override_unit; + public static Type mqtt_discovery_button_type; + public static bool mqtt_audio_local_mute; // Notifications public static bool notify_area; diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs b/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs index 51ec3d7..51350fa 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Alarm.cs @@ -1,10 +1,14 @@ using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace OmniLinkBridge.MQTT.HomeAssistant { public class Alarm : Device { + public Alarm(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + public string command_topic { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/OmniLinkBridge/MQTT/HomeAssistant/BinarySensor.cs b/OmniLinkBridge/MQTT/HomeAssistant/BinarySensor.cs index 748d353..fd4b6b1 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/BinarySensor.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/BinarySensor.cs @@ -5,6 +5,11 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class BinarySensor : Device { + public BinarySensor(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + [JsonConverter(typeof(StringEnumConverter))] public enum DeviceClass { diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Button.cs b/OmniLinkBridge/MQTT/HomeAssistant/Button.cs new file mode 100644 index 0000000..6ef58b6 --- /dev/null +++ b/OmniLinkBridge/MQTT/HomeAssistant/Button.cs @@ -0,0 +1,17 @@ +using Newtonsoft.Json; + +namespace OmniLinkBridge.MQTT.HomeAssistant +{ + public class Button : Device + { + public Button(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + + public string command_topic { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string payload_press { get; set; } + } +} diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Climate.cs b/OmniLinkBridge/MQTT/HomeAssistant/Climate.cs index dba39fc..82549f5 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Climate.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Climate.cs @@ -4,6 +4,11 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class Climate : Device { + public Climate(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + public string status { get; set; } public string action_topic { get; set; } diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Device.cs b/OmniLinkBridge/MQTT/HomeAssistant/Device.cs index a1a9541..4d656b6 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Device.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Device.cs @@ -1,12 +1,16 @@ using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using OmniLinkBridge.Modules; using System.Collections.Generic; namespace OmniLinkBridge.MQTT.HomeAssistant { public class Device { + public Device(DeviceRegistry deviceRegistry) + { + device = deviceRegistry; + } + [JsonConverter(typeof(StringEnumConverter))] public enum AvailabilityMode { @@ -19,6 +23,9 @@ public enum AvailabilityMode public string name { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string icon { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string state_topic { get; set; } @@ -32,6 +39,6 @@ public enum AvailabilityMode public AvailabilityMode? availability_mode { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public DeviceRegistry device { get; set; } = MQTTModule.MqttDeviceRegistry; + public DeviceRegistry device { get; set; } } } diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Light.cs b/OmniLinkBridge/MQTT/HomeAssistant/Light.cs index 184f569..5c6dfcb 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Light.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Light.cs @@ -2,6 +2,11 @@ { public class Light : Device { + public Light(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + public string command_topic { get; set; } public string brightness_state_topic { get; set; } diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs b/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs index 98d2fb6..fb17d97 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Lock.cs @@ -4,6 +4,11 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class Lock : Device { + public Lock(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + public string command_topic { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Number.cs b/OmniLinkBridge/MQTT/HomeAssistant/Number.cs index 060c5e8..79fb256 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Number.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Number.cs @@ -4,10 +4,12 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class Number : Device { - public string command_topic { get; set; } + public Number(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string icon { get; set; } + } + + public string command_topic { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public int? min { get; set; } diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Select.cs b/OmniLinkBridge/MQTT/HomeAssistant/Select.cs new file mode 100644 index 0000000..5d7d8e1 --- /dev/null +++ b/OmniLinkBridge/MQTT/HomeAssistant/Select.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace OmniLinkBridge.MQTT.HomeAssistant +{ + public class Select : Device + { + public Select(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + + public string command_topic { get; set; } + + public List options { get; set; } = null; + } +} diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Sensor.cs b/OmniLinkBridge/MQTT/HomeAssistant/Sensor.cs index f4b1f1a..296e301 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Sensor.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Sensor.cs @@ -5,6 +5,11 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class Sensor : Device { + public Sensor(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + [JsonConverter(typeof(StringEnumConverter))] public enum DeviceClass { @@ -15,9 +20,6 @@ public enum DeviceClass [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public DeviceClass? device_class { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string icon { get; set; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string unit_of_measurement { get; set; } diff --git a/OmniLinkBridge/MQTT/HomeAssistant/Switch.cs b/OmniLinkBridge/MQTT/HomeAssistant/Switch.cs index 454bba5..e6de054 100644 --- a/OmniLinkBridge/MQTT/HomeAssistant/Switch.cs +++ b/OmniLinkBridge/MQTT/HomeAssistant/Switch.cs @@ -4,6 +4,11 @@ namespace OmniLinkBridge.MQTT.HomeAssistant { public class Switch : Device { + public Switch(DeviceRegistry deviceRegistry) : base(deviceRegistry) + { + + } + public string command_topic { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index a466bf0..a9edd11 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using OmniLinkBridge.MQTT.HomeAssistant; using OmniLinkBridge.MQTT.Parser; +using OmniLinkBridge.Modules; namespace OmniLinkBridge.MQTT { @@ -15,7 +16,7 @@ public static string ToTopic(this clsArea area, Topic topic) public static Alarm ToConfig(this clsArea area) { - Alarm ret = new Alarm + Alarm ret = new Alarm(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}", name = Global.mqtt_discovery_name_prefix + area.Name, @@ -83,7 +84,7 @@ public static string ToBasicState(this clsArea area) public static BinarySensor ToConfigBurglary(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}burglary", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Burglary", @@ -96,7 +97,7 @@ public static BinarySensor ToConfigBurglary(this clsArea area) public static BinarySensor ToConfigFire(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}fire", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Fire", @@ -109,7 +110,7 @@ public static BinarySensor ToConfigFire(this clsArea area) public static BinarySensor ToConfigGas(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}gas", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Gas", @@ -122,7 +123,7 @@ public static BinarySensor ToConfigGas(this clsArea area) public static BinarySensor ToConfigAux(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}auxiliary", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Auxiliary", @@ -135,7 +136,7 @@ public static BinarySensor ToConfigAux(this clsArea area) public static BinarySensor ToConfigFreeze(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}freeze", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Freeze", @@ -148,7 +149,7 @@ public static BinarySensor ToConfigFreeze(this clsArea area) public static BinarySensor ToConfigWater(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}water", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Water", @@ -161,7 +162,7 @@ public static BinarySensor ToConfigWater(this clsArea area) public static BinarySensor ToConfigDuress(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}duress", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Duress", @@ -174,7 +175,7 @@ public static BinarySensor ToConfigDuress(this clsArea area) public static BinarySensor ToConfigTemp(this clsArea area) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}area{area.Number}temp", name = $"{Global.mqtt_discovery_name_prefix}{area.Name} Temp", @@ -219,7 +220,7 @@ public static string ToTopic(this clsZone zone, Topic topic) public static Sensor ToConfigTemp(this clsZone zone, enuTempFormat format) { - Sensor ret = new Sensor + Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}zone{zone.Number}temp", name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Temp", @@ -232,7 +233,7 @@ public static Sensor ToConfigTemp(this clsZone zone, enuTempFormat format) public static Sensor ToConfigHumidity(this clsZone zone) { - Sensor ret = new Sensor + Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}zone{zone.Number}humidity", name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Humidity", @@ -245,7 +246,7 @@ public static Sensor ToConfigHumidity(this clsZone zone) public static Sensor ToConfigSensor(this clsZone zone) { - Sensor ret = new Sensor + Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}zone{zone.Number}", name = Global.mqtt_discovery_name_prefix + zone.Name @@ -287,7 +288,7 @@ public static Sensor ToConfigSensor(this clsZone zone) public static Switch ToConfigSwitch(this clsZone zone) { - Switch ret = new Switch + Switch ret = new Switch(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}zone{zone.Number}switch", name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Bypass", @@ -302,7 +303,7 @@ public static Switch ToConfigSwitch(this clsZone zone) public static BinarySensor ToConfig(this clsZone zone) { - BinarySensor ret = new BinarySensor + BinarySensor ret = new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}zone{zone.Number}binary", name = Global.mqtt_discovery_name_prefix + zone.Name @@ -377,7 +378,7 @@ public static string ToTopic(this clsUnit unit, Topic topic) public static Light ToConfig(this clsUnit unit) { - Light ret = new Light + Light ret = new Light(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}unit{unit.Number}light", name = Global.mqtt_discovery_name_prefix + unit.Name, @@ -391,7 +392,7 @@ public static Light ToConfig(this clsUnit unit) public static Switch ToConfigSwitch(this clsUnit unit) { - Switch ret = new Switch + Switch ret = new Switch(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}unit{unit.Number}switch", name = Global.mqtt_discovery_name_prefix + unit.Name, @@ -403,7 +404,7 @@ public static Switch ToConfigSwitch(this clsUnit unit) public static Number ToConfigNumber(this clsUnit unit) { - Number ret = new Number + Number ret = new Number(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}unit{unit.Number}number", name = Global.mqtt_discovery_name_prefix + unit.Name, @@ -446,7 +447,7 @@ public static string ToTopic(this clsThermostat thermostat, Topic topic) public static Sensor ToConfigTemp(this clsThermostat thermostat, enuTempFormat format) { - Sensor ret = new Sensor + Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}temp", name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Temp", @@ -459,7 +460,7 @@ public static Sensor ToConfigTemp(this clsThermostat thermostat, enuTempFormat f public static Number ToConfigHumidify(this clsThermostat thermostat) { - Number ret = new Number + Number ret = new Number(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}humidify", name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Humidify", @@ -472,7 +473,7 @@ public static Number ToConfigHumidify(this clsThermostat thermostat) public static Number ToConfigDehumidify(this clsThermostat thermostat) { - Number ret = new Number + Number ret = new Number(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}dehumidify", name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Dehumidify", @@ -485,7 +486,7 @@ public static Number ToConfigDehumidify(this clsThermostat thermostat) public static Sensor ToConfigHumidity(this clsThermostat thermostat) { - Sensor ret = new Sensor + Sensor ret = new Sensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}humidity", name = $"{Global.mqtt_discovery_name_prefix}{thermostat.Name} Humidity", @@ -498,7 +499,7 @@ public static Sensor ToConfigHumidity(this clsThermostat thermostat) public static Climate ToConfig(this clsThermostat thermostat, enuTempFormat format) { - Climate ret = new Climate + Climate ret = new Climate(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}", name = Global.mqtt_discovery_name_prefix + thermostat.Name, @@ -579,9 +580,9 @@ public static string ToTopic(this clsButton button, Topic topic) return $"{Global.mqtt_prefix}/button{button.Number}/{topic}"; } - public static Switch ToConfig(this clsButton button) + public static Switch ToConfigSwitch(this clsButton button) { - Switch ret = new Switch + Switch ret = new Switch(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}button{button.Number}", name = Global.mqtt_discovery_name_prefix + button.Name, @@ -591,6 +592,18 @@ public static Switch ToConfig(this clsButton button) return ret; } + public static Button ToConfigButton(this clsButton button) + { + Button ret = new Button(MQTTModule.MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}button{button.Number}", + name = Global.mqtt_discovery_name_prefix + button.Name, + command_topic = button.ToTopic(Topic.command), + payload_press = "ON" + }; + return ret; + } + public static string ToTopic(this clsMessage message, Topic topic) { return $"{Global.mqtt_prefix}/message{message.Number}/{topic}"; @@ -613,7 +626,7 @@ public static string ToTopic(this clsAccessControlReader reader, Topic topic) public static Lock ToConfig(this clsAccessControlReader reader) { - Lock ret = new Lock + Lock ret = new Lock(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}lock{reader.Number}", name = Global.mqtt_discovery_name_prefix + reader.Name, @@ -635,5 +648,91 @@ public static string ToState(this clsAccessControlReader reader) else return "unlocked"; } + + public static string ToTopic(this clsAudioSource audioSource, Topic topic) + { + return $"{Global.mqtt_prefix}/source{audioSource.Number}/{topic}"; + } + + public static string ToTopic(this clsAudioZone audioZone, Topic topic) + { + return $"{Global.mqtt_prefix}/audio{audioZone.Number}/{topic}"; + } + + public static Switch ToConfig(this clsAudioZone audioZone) + { + Switch ret = new Switch(MQTTModule.MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}", + name = Global.mqtt_discovery_name_prefix + audioZone.rawName, + icon = "mdi:speaker", + state_topic = audioZone.ToTopic(Topic.state), + command_topic = audioZone.ToTopic(Topic.command) + }; + return ret; + } + + public static string ToState(this clsAudioZone audioZone) + { + return audioZone.Power ? "ON" : "OFF"; + } + + public static Switch ToConfigMute(this clsAudioZone audioZone) + { + Switch ret = new Switch(MQTTModule.MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}mute", + name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Mute", + icon = "mdi:volume-mute", + state_topic = audioZone.ToTopic(Topic.mute_state), + command_topic = audioZone.ToTopic(Topic.mute_command) + }; + return ret; + } + + public static string ToMuteState(this clsAudioZone audioZone) + { + if(Global.mqtt_audio_local_mute) + return audioZone.Volume == 0 ? "ON" : "OFF"; + else + return audioZone.Mute ? "ON" : "OFF"; + } + + public static Select ToConfigSource(this clsAudioZone audioZone, List audioSources) + { + Select ret = new Select(MQTTModule.MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}source", + name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Source", + icon = "mdi:volume-source", + state_topic = audioZone.ToTopic(Topic.source_state), + command_topic = audioZone.ToTopic(Topic.source_command), + options = audioSources + }; + return ret; + } + + public static int ToSourceState(this clsAudioZone audioZone) + { + return audioZone.Source; + } + + public static Number ToConfigVolume(this clsAudioZone audioZone) + { + Number ret = new Number(MQTTModule.MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}audio{audioZone.Number}volume", + name = $"{Global.mqtt_discovery_name_prefix}{audioZone.rawName} Volume", + icon = "mdi:volume-low", + state_topic = audioZone.ToTopic(Topic.volume_state), + command_topic = audioZone.ToTopic(Topic.volume_command), + }; + return ret; + } + + public static int ToVolumeState(this clsAudioZone audioZone) + { + return audioZone.Volume; + } } } diff --git a/OmniLinkBridge/MQTT/MessageProcessor.cs b/OmniLinkBridge/MQTT/MessageProcessor.cs index e06947f..28c1ae8 100644 --- a/OmniLinkBridge/MQTT/MessageProcessor.cs +++ b/OmniLinkBridge/MQTT/MessageProcessor.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; namespace OmniLinkBridge.MQTT { @@ -15,11 +16,18 @@ public class MessageProcessor private readonly Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); - private IOmniLinkII OmniLink { get; set; } + private readonly int[] audioMuteVolumes; + private const int VOLUME_DEFAULT = 10; - public MessageProcessor(IOmniLinkII omni) + private IOmniLinkII OmniLink { get; } + private Dictionary AudioSources { get; } + + public MessageProcessor(IOmniLinkII omni, Dictionary audioSources, int numAudioZones) { OmniLink = omni; + AudioSources = audioSources; + + audioMuteVolumes = new int[numAudioZones]; } public void Process(string messageTopic, string payload) @@ -29,8 +37,8 @@ public void Process(string messageTopic, string payload) if (!match.Success) return; - if (!Enum.TryParse(match.Groups[1].Value, true, out CommandTypes type) - || !Enum.TryParse(match.Groups[3].Value, true, out Topic topic) + if (!Enum.TryParse(match.Groups[1].Value, true, out CommandTypes type) + || !Enum.TryParse(match.Groups[3].Value, true, out Topic topic) || !ushort.TryParse(match.Groups[2].Value, out ushort id)) return; @@ -51,6 +59,8 @@ public void Process(string messageTopic, string payload) ProcessMessageReceived(OmniLink.Controller.Messages[id], topic, payload); else if (type == CommandTypes.@lock && id <= OmniLink.Controller.AccessControlReaders.Count) ProcessLockReceived(OmniLink.Controller.AccessControlReaders[id], topic, payload); + else if (type == CommandTypes.audio && id <= OmniLink.Controller.AudioZones.Count) + ProcessAudioReceived(OmniLink.Controller.AudioZones[id], topic, payload); } private static readonly IDictionary AreaMapping = new Dictionary @@ -78,7 +88,7 @@ private void ProcessAreaReceived(clsArea area, Topic command, string payload) { string sCode = parser.Code.ToString(); - if(sCode.Length != 4) + if (sCode.Length != 4) { log.Warning("SetArea: {id}, Invalid security code: must be 4 digits", area.Number); return; @@ -98,7 +108,7 @@ private void ProcessAreaReceived(clsArea area, Topic command, string payload) var validateCode = new clsOL2MsgValidateCode(OmniLink.Controller.Connection, B); - if(validateCode.AuthorityLevel == 0) + if (validateCode.AuthorityLevel == 0) { log.Warning("SetArea: {id}, Invalid security code: validation failed", area.Number); return; @@ -143,7 +153,7 @@ private void ProcessZoneReceived(clsZone zone, Topic command, string payload) { AreaCommandCode parser = payload.ToCommandCode(); - if (parser.Success && command == Topic.command && Enum.TryParse(parser.Command, true, out ZoneCommands cmd) && + if (parser.Success && command == Topic.command && Enum.TryParse(parser.Command, true, out ZoneCommands cmd) && !(zone.Number == 0 && cmd == ZoneCommands.bypass)) { if (zone.Number == 0) @@ -176,7 +186,7 @@ private void ProcessUnitReceived(clsUnit unit, Topic command, string payload) log.Debug("SetUnit: {id} to {value}", unit.Number, payload); OmniLink.SendCommand(enuUnitCommand.Set, BitConverter.GetBytes(flagValue)[0], (ushort)unit.Number); } - else if (unit.Type != enuOL2UnitType.Output && + else if (unit.Type != enuOL2UnitType.Output && command == Topic.brightness_command && int.TryParse(payload, out int unitValue)) { log.Debug("SetUnit: {id} to {value}%", unit.Number, payload); @@ -187,7 +197,7 @@ private void ProcessUnitReceived(clsUnit unit, Topic command, string payload) // which will cause light to go to 100% brightness unit.Status = (byte)(100 + unitValue); } - else if (unit.Type != enuOL2UnitType.Output && + else if (unit.Type != enuOL2UnitType.Output && command == Topic.scene_command && char.TryParse(payload, out char scene)) { log.Debug("SetUnit: {id} to {value}", unit.Number, payload); @@ -315,5 +325,84 @@ private void ProcessLockReceived(clsAccessControlReader reader, Topic command, s OmniLink.SendCommand(LockMapping[cmd], 0, (ushort)reader.Number); } } + + private void ProcessAudioReceived(clsAudioZone audioZone, Topic command, string payload) + { + if (command == Topic.command && Enum.TryParse(payload, true, out UnitCommands cmd)) + { + if (audioZone.Number == 0) + log.Debug("SetAudio: 0 implies all audio zones will be changed"); + + log.Debug("SetAudio: {id} to {value}", audioZone.Number, payload); + + OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)cmd, (ushort)audioZone.Number); + + // Send power ON twice to workaround Russound standby + if(cmd == UnitCommands.ON) + { + Thread.Sleep(500); + OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)cmd, (ushort)audioZone.Number); + } + } + else if (command == Topic.mute_command && Enum.TryParse(payload, true, out UnitCommands mute)) + { + if (audioZone.Number == 0) + { + if (Global.mqtt_audio_local_mute) + { + log.Warning("SetAudioMute: 0 not supported with local mute"); + return; + } + else + log.Debug("SetAudioMute: 0 implies all audio zones will be changed"); + } + + if (Global.mqtt_audio_local_mute) + { + if (mute == UnitCommands.ON) + { + log.Debug("SetAudioMute: {id} local mute, previous volume {level}", + audioZone.Number, audioZone.Volume); + audioMuteVolumes[audioZone.Number] = audioZone.Volume; + + OmniLink.SendCommand(enuUnitCommand.AudioVolume, 0, (ushort)audioZone.Number); + } + else + { + if (audioMuteVolumes[audioZone.Number] == 0) + { + log.Debug("SetAudioMute: {id} local mute, defaulting to volume {level}", + audioZone.Number, VOLUME_DEFAULT); + audioMuteVolumes[audioZone.Number] = VOLUME_DEFAULT; + } + else + { + log.Debug("SetAudioMute: {id} local mute, restoring to volume {level}", + audioZone.Number, audioMuteVolumes[audioZone.Number]); + } + + OmniLink.SendCommand(enuUnitCommand.AudioVolume, (byte)audioMuteVolumes[audioZone.Number], (ushort)audioZone.Number); + } + } + else + { + log.Debug("SetAudioMute: {id} to {value}", audioZone.Number, payload); + + OmniLink.SendCommand(enuUnitCommand.AudioZone, (byte)(mute + 2), (ushort)audioZone.Number); + } + } + else if (command == Topic.source_command && AudioSources.TryGetValue(payload, out int source)) + { + log.Debug("SetAudioSource: {id} to {value}", audioZone.Number, payload); + + OmniLink.SendCommand(enuUnitCommand.AudioSource, (byte)source, (ushort)audioZone.Number); + } + else if (command == Topic.volume_command && int.TryParse(payload, out int volume)) + { + log.Debug("SetAudioVolume: {id} to {value}", audioZone.Number, payload); + + OmniLink.SendCommand(enuUnitCommand.AudioVolume, (byte)volume, (ushort)audioZone.Number); + } + } } } diff --git a/OmniLinkBridge/MQTT/Parser/CommandTypes.cs b/OmniLinkBridge/MQTT/Parser/CommandTypes.cs index ade6e59..5bbe967 100644 --- a/OmniLinkBridge/MQTT/Parser/CommandTypes.cs +++ b/OmniLinkBridge/MQTT/Parser/CommandTypes.cs @@ -8,6 +8,7 @@ enum CommandTypes thermostat, button, message, - @lock + @lock, + audio } } diff --git a/OmniLinkBridge/MQTT/Parser/Topic.cs b/OmniLinkBridge/MQTT/Parser/Topic.cs index bea3ddb..7f1171c 100644 --- a/OmniLinkBridge/MQTT/Parser/Topic.cs +++ b/OmniLinkBridge/MQTT/Parser/Topic.cs @@ -33,5 +33,11 @@ public enum Topic fan_mode_command, hold_state, hold_command, + mute_state, + mute_command, + source_state, + source_command, + volume_state, + volume_command, } } diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs index cbed488..244bf33 100644 --- a/OmniLinkBridge/Modules/LoggerModule.cs +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -40,6 +40,7 @@ public LoggerModule(OmniLinkII omni) omnilink.OnUnitStatus += Omnilink_OnUnitStatus; omnilink.OnMessageStatus += Omnilink_OnMessageStatus; omnilink.OnLockStatus += Omnilink_OnLockStatus; + omnilink.OnAudioZoneStatus += Omnilink_OnAudioZoneStatus; omnilink.OnSystemStatus += Omnilink_OnSystemStatus; } @@ -224,6 +225,9 @@ private void Omnilink_OnConnect(object sender, EventArgs e) continue; audioSourceUsage++; + + if (Global.verbose_audio) + log.Verbose("Initial AudioSource {id} {name}", i, audioSource.rawName); } ushort audioZoneUsage = 0; @@ -235,6 +239,10 @@ private void Omnilink_OnConnect(object sender, EventArgs e) continue; audioZoneUsage++; + + if (Global.verbose_audio) + log.Verbose("Initial AudioZoneStatus {id} {name}, Power: {power}, Source: {source}, Volume: {volume}, Mute: {mute}", + i, audioZone.rawName, audioZone.Power, audioZone.Source, audioZone.Volume, audioZone.Mute); } using (LogContext.PushProperty("Telemetry", "ControllerUsage")) @@ -270,7 +278,7 @@ INSERT INTO log_areas (timestamp, id, name, e.Area.AreaDuressAlarmText + "','" + status + "')"); if (Global.verbose_area) - log.Verbose("AreaStatus {id} {name}, Status: {status}, Alarams: {alarms}", e.ID, e.Area.Name, status, e.Area.AreaAlarms); + log.Verbose("AreaStatus {id} {name}, Status: {status}, Alarms: {alarms}", e.ID, e.Area.Name, status, e.Area.AreaAlarms); if (Global.notify_area && e.Area.LastMode != e.Area.AreaMode) Notification.Notify("Security", e.Area.Name + " " + e.Area.ModeText()); @@ -383,6 +391,13 @@ private void Omnilink_OnLockStatus(object sender, LockStatusEventArgs e) log.Verbose("LockStatus {id} {name}, Status: {status}", e.ID, e.Reader.Name, e.Reader.LockStatusText()); } + private void Omnilink_OnAudioZoneStatus(object sender, AudioZoneStatusEventArgs e) + { + if (Global.verbose_audio) + log.Verbose("AudioZoneStatus {id} {name}, Power: {power}, Source: {source}, Volume: {volume}, Mute: {mute}", + e.ID, e.AudioZone.rawName, e.AudioZone.Power, e.AudioZone.Source, e.AudioZone.Volume, e.AudioZone.Mute); + } + private void Omnilink_OnSystemStatus(object sender, SystemStatusEventArgs e) { DBQueue(@" diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index 3868d60..f0d8732 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -34,11 +34,16 @@ public class MQTTModule : IModule private bool ControllerConnected { get; set; } private MessageProcessor MessageProcessor { get; set; } + private Dictionary AudioSources { get; set; } = new Dictionary(); + private readonly AutoResetEvent trigger = new AutoResetEvent(false); private const string ONLINE = "online"; private const string OFFLINE = "offline"; + private const string SECURE = "secure"; + private const string TROUBLE = "trouble"; + public MQTTModule(OmniLinkII omni) { OmniLink = omni; @@ -51,9 +56,10 @@ public MQTTModule(OmniLinkII omni) OmniLink.OnButtonStatus += OmniLink_OnButtonStatus; OmniLink.OnMessageStatus += OmniLink_OnMessageStatus; OmniLink.OnLockStatus += OmniLink_OnLockStatus; + OmniLink.OnAudioZoneStatus += OmniLink_OnAudioZoneStatus; OmniLink.OnSystemStatus += OmniLink_OnSystemStatus; - MessageProcessor = new MessageProcessor(omni); + MessageProcessor = new MessageProcessor(omni, AudioSources, omni.Controller.CAP.numAudioZones); } public void Startup() @@ -120,7 +126,10 @@ public void Startup() Topic.dehumidify_command, Topic.mode_command, Topic.fan_mode_command, - Topic.hold_command + Topic.hold_command, + Topic.mute_command, + Topic.source_command, + Topic.volume_command }; toSubscribe.ForEach((command) => MqttClient.SubscribeAsync( @@ -171,6 +180,8 @@ private void PublishConfig() PublishButtons(); PublishMessages(); PublishLocks(); + PublishAudioSources(); + PublishAudioZones(); PublishControllerStatus(ONLINE); PublishAsync($"{Global.mqtt_prefix}/model", OmniLink.Controller.GetModelText()); @@ -190,10 +201,10 @@ private void PublishSystem() PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/system_dcm/config", JsonConvert.SerializeObject(SystemTroubleConfig("dcm", "DCM"))); - PublishAsync(SystemTroubleTopic("phone"), OmniLink.TroublePhone ? "trouble" : "secure"); - PublishAsync(SystemTroubleTopic("ac"), OmniLink.TroubleAC ? "trouble" : "secure"); - PublishAsync(SystemTroubleTopic("battery"), OmniLink.TroubleBattery ? "trouble" : "secure"); - PublishAsync(SystemTroubleTopic("dcm"), OmniLink.TroubleDCM ? "trouble" : "secure"); + PublishAsync(SystemTroubleTopic("phone"), OmniLink.TroublePhone ? TROUBLE : SECURE); + PublishAsync(SystemTroubleTopic("ac"), OmniLink.TroubleAC ? TROUBLE : SECURE); + PublishAsync(SystemTroubleTopic("battery"), OmniLink.TroubleBattery ? TROUBLE : SECURE); + PublishAsync(SystemTroubleTopic("dcm"), OmniLink.TroubleDCM ? TROUBLE : SECURE); } public string SystemTroubleTopic(string type) @@ -203,14 +214,14 @@ public string SystemTroubleTopic(string type) public BinarySensor SystemTroubleConfig(string type, string name) { - return new BinarySensor + return new BinarySensor(MQTTModule.MqttDeviceRegistry) { unique_id = $"{Global.mqtt_prefix}system{type}", name = $"{Global.mqtt_discovery_name_prefix}System {name}", state_topic = SystemTroubleTopic(type), device_class = BinarySensor.DeviceClass.problem, - payload_off = "secure", - payload_on = "trouble" + payload_off = SECURE, + payload_on = TROUBLE }; } @@ -399,6 +410,7 @@ private void PublishButtons() { PublishAsync(button.ToTopic(Topic.name), null); PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config", null); continue; } @@ -406,8 +418,21 @@ private void PublishButtons() PublishAsync(button.ToTopic(Topic.state), "OFF"); PublishAsync(button.ToTopic(Topic.name), button.Name); - PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", - JsonConvert.SerializeObject(button.ToConfig())); + + if (Global.mqtt_discovery_button_type == typeof(Switch)) + { + log.Information("See {setting} for new option when publishing {type}", "mqtt_discovery_button_type", "buttons"); + + PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", + JsonConvert.SerializeObject(button.ToConfigSwitch())); + } + else if (Global.mqtt_discovery_button_type == typeof(Button)) + { + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/button{i}/config", + JsonConvert.SerializeObject(button.ToConfigButton())); + } } } @@ -454,6 +479,74 @@ private void PublishLocks() } } + private void PublishAudioSources() + { + log.Debug("Publishing {type}", "audio sources"); + + for (ushort i = 1; i <= OmniLink.Controller.AudioSources.Count; i++) + { + clsAudioSource audioSource = OmniLink.Controller.AudioSources[i]; + + if (audioSource.DefaultProperties == true) + { + PublishAsync(audioSource.ToTopic(Topic.name), null); + continue; + } + + PublishAsync(audioSource.ToTopic(Topic.name), audioSource.rawName); + + if (AudioSources.ContainsKey(audioSource.rawName)) + { + log.Warning("Duplicate audio source name {name}", audioSource.rawName); + continue; + } + + AudioSources.Add(audioSource.rawName, i); + } + } + + private void PublishAudioZones() + { + log.Debug("Publishing {type}", "audio zones"); + + for (ushort i = 1; i <= OmniLink.Controller.AudioZones.Count; i++) + { + clsAudioZone audioZone = OmniLink.Controller.AudioZones[i]; + + if (audioZone.DefaultProperties == true) + { + PublishAsync(audioZone.ToTopic(Topic.name), null); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}mute/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/select/{Global.mqtt_prefix}/audio{i}source/config", null); + PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/audio{i}volume/config", null); + continue; + } + + PublishAudioZoneStateAsync(audioZone); + + PublishAsync(audioZone.ToTopic(Topic.name), audioZone.rawName); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}/config", + JsonConvert.SerializeObject(audioZone.ToConfig())); + PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/audio{i}mute/config", + JsonConvert.SerializeObject(audioZone.ToConfigMute())); + PublishAsync($"{Global.mqtt_discovery_prefix}/select/{Global.mqtt_prefix}/audio{i}source/config", + JsonConvert.SerializeObject(audioZone.ToConfigSource(new List(AudioSources.Keys)))); + PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/audio{i}volume/config", + JsonConvert.SerializeObject(audioZone.ToConfigVolume())); + } + + PublishAsync($"{Global.mqtt_discovery_prefix}/button/{Global.mqtt_prefix}/audio0/config", + JsonConvert.SerializeObject(new Button(MqttDeviceRegistry) + { + unique_id = $"{Global.mqtt_prefix}audio0", + name = Global.mqtt_discovery_name_prefix + "Audio All Off", + icon = "mdi:speaker", + command_topic = $"{Global.mqtt_prefix}/audio0/{Topic.command}", + payload_press = "OFF" + })); + } + private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) { if (!MqttClient.IsConnected) @@ -547,19 +640,27 @@ private void OmniLink_OnLockStatus(object sender, LockStatusEventArgs e) PublishLockStateAsync(e.Reader); } + private void OmniLink_OnAudioZoneStatus(object sender, AudioZoneStatusEventArgs e) + { + if (!MqttClient.IsConnected) + return; + + PublishAudioZoneStateAsync(e.AudioZone); + } + private void OmniLink_OnSystemStatus(object sender, SystemStatusEventArgs e) { if (!MqttClient.IsConnected) return; if(e.Type == SystemEventType.Phone) - PublishAsync(SystemTroubleTopic("phone"), e.Trouble ? "trouble" : "secure"); + PublishAsync(SystemTroubleTopic("phone"), e.Trouble ? TROUBLE : SECURE); else if (e.Type == SystemEventType.AC) - PublishAsync(SystemTroubleTopic("ac"), e.Trouble ? "trouble" : "secure"); + PublishAsync(SystemTroubleTopic("ac"), e.Trouble ? TROUBLE : SECURE); else if (e.Type == SystemEventType.Button) - PublishAsync(SystemTroubleTopic("battery"), e.Trouble ? "trouble" : "secure"); + PublishAsync(SystemTroubleTopic("battery"), e.Trouble ? TROUBLE : SECURE); else if (e.Type == SystemEventType.DCM) - PublishAsync(SystemTroubleTopic("dcm"), e.Trouble ? "trouble" : "secure"); + PublishAsync(SystemTroubleTopic("dcm"), e.Trouble ? TROUBLE : SECURE); } private void PublishAreaState(clsArea area) @@ -628,6 +729,15 @@ private Task PublishLockStateAsync(clsAccessControlReader reader) return PublishAsync(reader.ToTopic(Topic.state), reader.ToState()); } + private void PublishAudioZoneStateAsync(clsAudioZone audioZone) + { + PublishAsync(audioZone.ToTopic(Topic.state), audioZone.ToState()); + PublishAsync(audioZone.ToTopic(Topic.mute_state), audioZone.ToMuteState()); + PublishAsync(audioZone.ToTopic(Topic.source_state), + OmniLink.Controller.AudioSources[audioZone.ToSourceState()].rawName); + PublishAsync(audioZone.ToTopic(Topic.volume_state), audioZone.ToVolumeState().ToString()); + } + private Task PublishAsync(string topic, string payload) { return MqttClient.PublishAsync(topic, payload, MqttQualityOfServiceLevel.AtMostOnce, true); diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs index 3363402..62f4a38 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -41,6 +41,7 @@ public class OmniLinkII : IModule, IOmniLinkII public event EventHandler OnButtonStatus; public event EventHandler OnMessageStatus; public event EventHandler OnLockStatus; + public event EventHandler OnAudioZoneStatus; public event EventHandler OnSystemStatus; private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -86,6 +87,7 @@ public void Shutdown() public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2) { + log.Verbose("Sending: {command}, Par1: {par1}, Par2: {par2}", Cmd, Par, Pr2); return Controller.SendCommand(Cmd, Par, Pr2); } @@ -216,6 +218,8 @@ private async Task GetNamedProperties() await GetNamed(enuObjectType.Message); await GetNamed(enuObjectType.Button); await GetNamed(enuObjectType.AccessControlReader); + await GetNamed(enuObjectType.AudioSource); + await GetNamed(enuObjectType.AudioZone); } private async Task GetSystemFormats() @@ -347,6 +351,14 @@ private void HandleNamedPropertiesResponse(clsOmniLinkMessageQueueItem M, byte[] case enuObjectType.AccessControlReader: Controller.AccessControlReaders.CopyProperties(MSG); break; + case enuObjectType.AudioSource: + Controller.AudioSources.CopyProperties(MSG); + Controller.AudioSources[MSG.ObjectNumber].rawName = MSG.ObjectName; + break; + case enuObjectType.AudioZone: + Controller.AudioZones.CopyProperties(MSG); + Controller.AudioZones[MSG.ObjectNumber].rawName = MSG.ObjectName; + break; default: break; } @@ -427,6 +439,8 @@ private bool HandleUnsolicitedPackets(byte[] B) case enuOmniLink2MessageType.CmdExtSecurity: break; case enuOmniLink2MessageType.AudioSourceStatus: + // Ignore audio source metadata status updates + handled = true; break; case enuOmniLink2MessageType.SystemEvents: HandleUnsolicitedSystemEvent(B); @@ -695,6 +709,17 @@ private void HandleUnsolicitedExtendedStatus(byte[] B) }); } break; + case enuObjectType.AudioZone: + for (byte i = 0; i < MSG.AudioZoneStatusCount(); i++) + { + Controller.AudioZones[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); + OnAudioZoneStatus?.Invoke(this, new AudioZoneStatusEventArgs() + { + ID = MSG.ObjectNumber(i), + AudioZone = Controller.AudioZones[MSG.ObjectNumber(i)] + }); + } + break; default: if (Global.verbose_unhandled) { diff --git a/OmniLinkBridge/OmniLink/AudioZoneStatusEventArgs.cs b/OmniLinkBridge/OmniLink/AudioZoneStatusEventArgs.cs new file mode 100644 index 0000000..1f59307 --- /dev/null +++ b/OmniLinkBridge/OmniLink/AudioZoneStatusEventArgs.cs @@ -0,0 +1,11 @@ +using HAI_Shared; +using System; + +namespace OmniLinkBridge.OmniLink +{ + public class AudioZoneStatusEventArgs : EventArgs + { + public ushort ID { get; set; } + public clsAudioZone AudioZone { get; set; } + } +} diff --git a/OmniLinkBridge/OmniLink/SystemEventType.cs b/OmniLinkBridge/OmniLink/SystemEventType.cs index eb57ab8..749f7b6 100644 --- a/OmniLinkBridge/OmniLink/SystemEventType.cs +++ b/OmniLinkBridge/OmniLink/SystemEventType.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace OmniLinkBridge.OmniLink +namespace OmniLinkBridge.OmniLink { public enum SystemEventType { diff --git a/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs b/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs index 07116da..f9d21ab 100644 --- a/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs +++ b/OmniLinkBridge/OmniLink/SystemStatusEventArgs.cs @@ -1,5 +1,4 @@ -using HAI_Shared; -using System; +using System; namespace OmniLinkBridge.OmniLink { diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index d33972b..b53bf53 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -83,8 +83,10 @@ + + @@ -117,6 +119,7 @@ + diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini index 73e4cd3..f19b8e2 100644 --- a/OmniLinkBridge/OmniLinkBridge.ini +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -23,6 +23,7 @@ verbose_thermostat = yes verbose_unit = yes verbose_message = yes verbose_lock = yes +verbose_audio = yes # mySQL Logging (yes/no) mysql_logging = no @@ -68,6 +69,11 @@ mqtt_discovery_area_code_required = # Flags (LTe 41-88, IIe 73-128, Pro 393-511) switch or number, defaults to switch #mqtt_discovery_override_unit = id=1;type=switch #mqtt_discovery_override_unit = id=395;type=number +# Publish buttons as this Home Assistant device type +# must be button or switch (previous behavior) +#mqtt_discovery_button_type = button +# Handle mute locally by setting volume to 0 and restoring to previous value +mqtt_audio_local_mute = no # Notifications (yes/no) # Always sent for area alarms and critical system events diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index 398d642..0209f91 100644 --- a/OmniLinkBridge/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.1.15.0")] -[assembly: AssemblyFileVersion("1.1.15.0")] +[assembly: AssemblyVersion("1.1.16.0")] +[assembly: AssemblyFileVersion("1.1.16.0")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index 601a0fc..76584cb 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -1,3 +1,4 @@ +using OmniLinkBridge.MQTT.HomeAssistant; using Serilog; using System; using System.Collections.Concurrent; @@ -54,6 +55,7 @@ public static void LoadSettings(NameValueCollection settings) Global.verbose_unit = settings.ValidateBool("verbose_unit"); Global.verbose_message = settings.ValidateBool("verbose_message"); Global.verbose_lock = settings.ValidateBool("verbose_lock"); + Global.verbose_audio = settings.ValidateBool("verbose_audio"); // mySQL Logging Global.mysql_logging = settings.ValidateBool("mysql_logging"); @@ -89,6 +91,8 @@ public static void LoadSettings(NameValueCollection settings) Global.mqtt_discovery_area_code_required = settings.ValidateRange("mqtt_discovery_area_code_required"); Global.mqtt_discovery_override_zone = settings.LoadOverrideZone("mqtt_discovery_override_zone"); Global.mqtt_discovery_override_unit = settings.LoadOverrideUnit("mqtt_discovery_override_unit"); + Global.mqtt_discovery_button_type = settings.ValidateType("mqtt_discovery_button_type", typeof(Switch), typeof(Button)); + Global.mqtt_audio_local_mute = settings.ValidateBool("mqtt_audio_local_mute"); } // Notifications @@ -367,6 +371,21 @@ private static bool ValidateBool (this NameValueCollection settings, string sect } } + private static Type ValidateType(this NameValueCollection settings, string section, params Type[] types) + { + string value = settings.CheckEnv(section); + + if (value == null) + return types[0]; + + foreach (Type type in types) + if (string.Compare(value, type.Name, true) == 0) + return type; + + log.Error("Invalid type specified for {section}", section); + throw new Exception(); + } + private static NameValueCollection LoadCollection(string[] lines) { NameValueCollection settings = new NameValueCollection(); diff --git a/OmniLinkBridgeTest/MQTTTest.cs b/OmniLinkBridgeTest/MQTTTest.cs index 1e7a239..caadbd4 100644 --- a/OmniLinkBridgeTest/MQTTTest.cs +++ b/OmniLinkBridgeTest/MQTTTest.cs @@ -1,9 +1,13 @@ using HAI_Shared; using Microsoft.VisualStudio.TestTools.UnitTesting; using OmniLinkBridge; +using OmniLinkBridge.Modules; using OmniLinkBridge.MQTT; using OmniLinkBridgeTest.Mock; +using Serilog; +using System; using System.Collections.Concurrent; +using System.Collections.Generic; namespace OmniLinkBridgeTest { @@ -16,8 +20,24 @@ public class MQTTTest [TestInitialize] public void Initialize() { + string log_format = "{Timestamp:yyyy-MM-dd HH:mm:ss} [{SourceContext} {Level:u3}] {Message:lj}{NewLine}{Exception}"; + + var log_config = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.FromLogContext() + .WriteTo.Console(outputTemplate: log_format); + + Log.Logger = log_config.CreateLogger(); + + Dictionary audioSources = new Dictionary + { + { "Radio", 1 }, + { "Streaming", 2 }, + { "TV", 4 } + }; + omniLink = new MockOmniLinkII(); - messageProcessor = new MessageProcessor(omniLink); + messageProcessor = new MessageProcessor(omniLink, audioSources, 8); omniLink.Controller.Units[395].Type = enuOL2UnitType.Flag; } @@ -313,6 +333,107 @@ void check(ushort id, string payload, enuUnitCommand command) // Check case insensitivity check(2, "LOCK", enuUnitCommand.Lock); } + + [TestMethod] + public void AudioCommand() + { + void check(ushort id, string payload, enuUnitCommand command, int value) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/audio{id}/command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = (byte)value, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "ON", enuUnitCommand.AudioZone, 1); + check(1, "OFF", enuUnitCommand.AudioZone, 0); + + check(2, "on", enuUnitCommand.AudioZone, 1); + } + + [TestMethod] + public void AudioMuteCommand() + { + void check(ushort id, string payload, enuUnitCommand command, int value) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/audio{id}/mute_command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = (byte)value, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "ON", enuUnitCommand.AudioZone, 3); + check(1, "OFF", enuUnitCommand.AudioZone, 2); + + Global.mqtt_audio_local_mute = true; + omniLink.Controller.AudioZones[2].Volume = 50; + + check(2, "on", enuUnitCommand.AudioVolume, 0); + check(2, "off", enuUnitCommand.AudioVolume, 50); + + omniLink.Controller.AudioZones[2].Volume = 0; + + check(2, "on", enuUnitCommand.AudioVolume, 0); + check(2, "off", enuUnitCommand.AudioVolume, 10); + } + + [TestMethod] + public void AudioSourceCommand() + { + void check(ushort id, string payload, enuUnitCommand command, int value) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/audio{id}/source_command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = (byte)value, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "Radio", enuUnitCommand.AudioSource, 1); + check(1, "Streaming", enuUnitCommand.AudioSource, 2); + + check(2, "TV", enuUnitCommand.AudioSource, 4); + } + + [TestMethod] + public void AudioVolumeCommand() + { + void check(ushort id, string payload, enuUnitCommand command, int value) + { + SendCommandEventArgs actual = null; + omniLink.OnSendCommand += (sender, e) => { actual = e; }; + messageProcessor.Process($"omnilink/audio{id}/volume_command", payload); + SendCommandEventArgs expected = new SendCommandEventArgs() + { + Cmd = command, + Par = (byte)value, + Pr2 = id + }; + Assert.AreEqual(expected, actual); + } + + check(1, "100", enuUnitCommand.AudioVolume, 100); + check(1, "75", enuUnitCommand.AudioVolume, 75); + + check(2, "0", enuUnitCommand.AudioVolume, 0); + } } } diff --git a/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs b/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs index 6715ca3..61a64b3 100644 --- a/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs +++ b/OmniLinkBridgeTest/Mock/MockOmniLinkII.cs @@ -1,11 +1,15 @@ using HAI_Shared; using OmniLinkBridge.OmniLink; +using Serilog; using System; +using System.Reflection; namespace OmniLinkBridgeTest.Mock { class MockOmniLinkII : IOmniLinkII { + private static readonly ILogger log = Log.Logger.ForContext(MethodBase.GetCurrentMethod().DeclaringType); + public clsHAC Controller { get; private set; } public event EventHandler OnSendCommand; @@ -21,6 +25,7 @@ public MockOmniLinkII() public bool SendCommand(enuUnitCommand Cmd, byte Par, ushort Pr2) { + log.Verbose("Sending: {command}, Par1: {par1}, Par2: {par2}", Cmd, Par, Pr2); OnSendCommand?.Invoke(null, new SendCommandEventArgs() { Cmd = Cmd, Par = Par, Pr2 = Pr2 }); return true; } diff --git a/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs b/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs index c6b332d..2097155 100644 --- a/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs +++ b/OmniLinkBridgeTest/Mock/SendCommandEventArgs.cs @@ -9,6 +9,18 @@ public class SendCommandEventArgs : EventArgs public byte Par; public ushort Pr2; + public SendCommandEventArgs() + { + + } + + public SendCommandEventArgs(enuUnitCommand cmd, byte par, ushort pr2) + { + Cmd = cmd; + Par = par; + Pr2 = pr2; + } + public override bool Equals(object other) { if (!(other is SendCommandEventArgs toCompareWith)) diff --git a/OmniLinkBridgeTest/SettingsTest.cs b/OmniLinkBridgeTest/SettingsTest.cs index 296acd2..43cbff4 100644 --- a/OmniLinkBridgeTest/SettingsTest.cs +++ b/OmniLinkBridgeTest/SettingsTest.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using OmniLinkBridge; +using OmniLinkBridge.MQTT.HomeAssistant; using System; using System.Collections.Generic; using ha = OmniLinkBridge.MQTT.HomeAssistant; @@ -36,8 +37,8 @@ public void TestControllerSettings() Settings.LoadSettings(lines.ToArray()); Assert.AreEqual("1.1.1.1", Global.controller_address); Assert.AreEqual(4369, Global.controller_port); - Assert.AreEqual("00-00-00-00-00-00-00-01", Global.controller_key1); - Assert.AreEqual("00-00-00-00-00-00-00-02", Global.controller_key2); + Assert.AreEqual("0000000000000001", Global.controller_key1); + Assert.AreEqual("0000000000000002", Global.controller_key2); Assert.AreEqual("MyController", Global.controller_name); } @@ -197,6 +198,17 @@ public void TestMQTTSettings() Global.mqtt_discovery_override_unit.TryGetValue(pair.Key, out OmniLinkBridge.MQTT.OverrideUnit value); Assert.AreEqual(override_unit[pair.Key].type, value.type); } + + Assert.AreEqual(Global.mqtt_discovery_button_type, typeof(Switch)); + + // Test additional settings + lines.AddRange(new string[] + { + "mqtt_discovery_button_type = button" + }); + Settings.LoadSettings(lines.ToArray()); + + Assert.AreEqual(Global.mqtt_discovery_button_type, typeof(Button)); } [TestMethod] diff --git a/README.md b/README.md index 18aa641..caa1a93 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,37 @@ PUB omnilink/lockX/command string lock, unlock ``` +### Audio Sources +``` +SUB omnilink/sourceXX/name +string Audio source name +``` + +### Audio Zones +``` +SUB omnilink/audioXX/name +string Audio zone name + +SUB omnilink/audioXX/state +PUB omnilink/audioXX/command +string OFF, ON +note Use audio 0 to change all audio zones + +SUB omnilink/audioXX/mute_state +PUB omnilink/audioXX/mute_command +string OFF, ON +note Use audio 0 to change all audio zones + +SUB omnilink/audioXX/source_state +PUB omnilink/audioXX/source_command +string Source name +note Refer to omnilink/sourceXX/name + +SUB omnilink/audioXX/volume_state +PUB omnilink/audioXX/volume_command +int Level from 0 to 100 percent +``` + ## Web API To test the web service API you can use your browser to view a page or PowerShell (see below) to change a value.