From 96093fbebd479e74da1681df7eb84b079c055a15 Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Tue, 23 Oct 2018 23:51:05 -0400 Subject: [PATCH] 1.1.2 - Add min and max climate temperatures - Update docker run command to use local time zone - Improve area and zone MQTT support - Add option to change MQTT prefix to support multiple instances - Add detailed zone sensor and thermostat humidity sensor - Add prefix for MQTT discovery entity name - Request zone status update on area status change --- OmniLinkBridge/Global.cs | 2 + OmniLinkBridge/MQTT/Alarm.cs | 2 + OmniLinkBridge/MQTT/Climate.cs | 3 + OmniLinkBridge/MQTT/Device.cs | 2 +- OmniLinkBridge/MQTT/MappingExtensions.cs | 129 ++++++++++++--- OmniLinkBridge/MQTT/Sensor.cs | 6 + OmniLinkBridge/MQTT/Topics.cs | 2 + OmniLinkBridge/Modules/MQTTModule.cs | 183 ++++++++++++++-------- OmniLinkBridge/Modules/OmniLinkII.cs | 2 +- OmniLinkBridge/OmniLinkBridge.ini | 9 +- OmniLinkBridge/Properties/AssemblyInfo.cs | 4 +- OmniLinkBridge/Settings.cs | 8 +- README.md | 52 +++++- 13 files changed, 310 insertions(+), 94 deletions(-) diff --git a/OmniLinkBridge/Global.cs b/OmniLinkBridge/Global.cs index 9be454c..bf5a4ca 100644 --- a/OmniLinkBridge/Global.cs +++ b/OmniLinkBridge/Global.cs @@ -48,7 +48,9 @@ namespace OmniLinkBridge public static int mqtt_port; public static string mqtt_username; public static string mqtt_password; + public static string mqtt_prefix; public static string mqtt_discovery_prefix; + public static string mqtt_discovery_name_prefix; public static HashSet mqtt_discovery_ignore_zones; public static HashSet mqtt_discovery_ignore_units; public static ConcurrentDictionary mqtt_discovery_override_zone; diff --git a/OmniLinkBridge/MQTT/Alarm.cs b/OmniLinkBridge/MQTT/Alarm.cs index 4a3679e..069290e 100644 --- a/OmniLinkBridge/MQTT/Alarm.cs +++ b/OmniLinkBridge/MQTT/Alarm.cs @@ -8,6 +8,8 @@ namespace OmniLinkBridge.MQTT { public class Alarm : Device { + public string basic_state_topic { get; set; } + public string command_topic { get; set; } //public string code { get; set; } = string.Empty; diff --git a/OmniLinkBridge/MQTT/Climate.cs b/OmniLinkBridge/MQTT/Climate.cs index 0816864..5e4a9f6 100644 --- a/OmniLinkBridge/MQTT/Climate.cs +++ b/OmniLinkBridge/MQTT/Climate.cs @@ -16,6 +16,9 @@ namespace OmniLinkBridge.MQTT public string temperature_high_state_topic { get; set; } public string temperature_high_command_topic { get; set; } + public string min_temp { get; set; } = "45"; + public string max_temp { get; set; } = "95"; + public string mode_state_topic { get; set; } public string mode_command_topic { get; set; } public List modes { get; set; } = new List(new string[] { "auto", "off", "cool", "heat" }); diff --git a/OmniLinkBridge/MQTT/Device.cs b/OmniLinkBridge/MQTT/Device.cs index 3012d37..319b986 100644 --- a/OmniLinkBridge/MQTT/Device.cs +++ b/OmniLinkBridge/MQTT/Device.cs @@ -12,6 +12,6 @@ namespace OmniLinkBridge.MQTT public string state_topic { get; set; } - public string availability_topic { get; set; } = "omnilink/status"; + public string availability_topic { get; set; } = $"{Global.mqtt_prefix}/status"; } } diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index c13fa8b..4517730 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -11,19 +11,46 @@ namespace OmniLinkBridge.MQTT { public static string ToTopic(this clsArea area, Topic topic) { - return $"omnilink/area{area.Number.ToString()}/{topic.ToString()}"; + return $"{Global.mqtt_prefix}/area{area.Number.ToString()}/{topic.ToString()}"; } public static Alarm ToConfig(this clsArea area) { Alarm ret = new Alarm(); - ret.name = area.Name; - ret.state_topic = area.ToTopic(Topic.state); + ret.name = Global.mqtt_discovery_name_prefix + area.Name; + ret.state_topic = area.ToTopic(Topic.basic_state); ret.command_topic = area.ToTopic(Topic.command); return ret; } public static string ToState(this clsArea area) + { + if (area.AreaBurglaryAlarmText != "OK") + return "triggered"; + else if (area.ExitTimer > 0) + return "pending"; + + switch (area.AreaMode) + { + case enuSecurityMode.Night: + return "armed_night"; + case enuSecurityMode.NightDly: + return "armed_night_delay"; + case enuSecurityMode.Day: + return "armed_home"; + case enuSecurityMode.DayInst: + return "armed_home_instant"; + case enuSecurityMode.Away: + return "armed_away"; + case enuSecurityMode.Vacation: + return "armed_vacation"; + case enuSecurityMode.Off: + default: + return "disarmed"; + } + } + + public static string ToBasicState(this clsArea area) { if (area.AreaBurglaryAlarmText != "OK") return "triggered"; @@ -49,15 +76,15 @@ namespace OmniLinkBridge.MQTT public static string ToTopic(this clsZone zone, Topic topic) { - return $"omnilink/zone{zone.Number.ToString()}/{topic.ToString()}"; + return $"{Global.mqtt_prefix}/zone{zone.Number.ToString()}/{topic.ToString()}"; } public static Sensor ToConfigTemp(this clsZone zone) { Sensor ret = new Sensor(); - ret.name = zone.Name; + ret.name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Temp"; ret.device_class = Sensor.DeviceClass.temperature; - ret.state_topic = zone.ToTopic(Topic.state); + ret.state_topic = zone.ToTopic(Topic.current_temperature); ret.unit_of_measurement = "°F"; return ret; } @@ -65,17 +92,56 @@ namespace OmniLinkBridge.MQTT public static Sensor ToConfigHumidity(this clsZone zone) { Sensor ret = new Sensor(); - ret.name = zone.Name; + ret.name = $"{Global.mqtt_discovery_name_prefix}{zone.Name} Humidity"; ret.device_class = Sensor.DeviceClass.humidity; - ret.state_topic = zone.ToTopic(Topic.state); + ret.state_topic = zone.ToTopic(Topic.current_humidity); ret.unit_of_measurement = "%"; return ret; } + public static Sensor ToConfigSensor(this clsZone zone) + { + Sensor ret = new Sensor(); + ret.name = Global.mqtt_discovery_name_prefix + zone.Name; + + switch (zone.ZoneType) + { + case enuZoneType.EntryExit: + case enuZoneType.X2EntryDelay: + case enuZoneType.X4EntryDelay: + ret.icon = "mdi:door"; + break; + case enuZoneType.Perimeter: + ret.icon = "mdi:window-closed"; + break; + case enuZoneType.Tamper: + ret.icon = "mdi:shield"; + break; + case enuZoneType.AwayInt: + case enuZoneType.NightInt: + ret.icon = "mdi:walk"; + break; + case enuZoneType.Water: + ret.icon = "mdi:water"; + break; + case enuZoneType.Fire: + ret.icon = "mdi:fire"; + break; + case enuZoneType.Gas: + ret.icon = "mdi:gas-cylinder"; + break; + } + + ret.value_template = @"{{ value|replace(""_"", "" "")|title }}"; + + ret.state_topic = zone.ToTopic(Topic.state); + return ret; + } + public static BinarySensor ToConfig(this clsZone zone) { BinarySensor ret = new BinarySensor(); - ret.name = zone.Name; + ret.name = Global.mqtt_discovery_name_prefix + zone.Name; Global.mqtt_discovery_override_zone.TryGetValue(zone.Number, out OverrideZone override_zone); @@ -114,27 +180,40 @@ namespace OmniLinkBridge.MQTT } } - ret.state_topic = zone.ToTopic(Topic.state); + ret.state_topic = zone.ToTopic(Topic.basic_state); return ret; } public static string ToState(this clsZone zone) { - if (zone.IsTemperatureZone() || zone.IsHumidityZone()) - return zone.TempText(); + if (zone.Status.IsBitSet(5)) + return "bypassed"; + else if (zone.Status.IsBitSet(2)) + return "tripped"; + else if (zone.Status.IsBitSet(4)) + return "armed"; + else if (zone.Status.IsBitSet(1)) + return "trouble"; + else if (zone.Status.IsBitSet(0)) + return "not_ready"; else - return zone.Status.IsBitSet(0) ? "ON" : "OFF"; + return "secure"; + } + + public static string ToBasicState(this clsZone zone) + { + return zone.Status.IsBitSet(0) ? "ON" : "OFF"; } public static string ToTopic(this clsUnit unit, Topic topic) { - return $"omnilink/unit{unit.Number.ToString()}/{topic.ToString()}"; + return $"{Global.mqtt_prefix}/unit{unit.Number.ToString()}/{topic.ToString()}"; } public static Light ToConfig(this clsUnit unit) { Light ret = new Light(); - ret.name = unit.Name; + ret.name = Global.mqtt_discovery_name_prefix + unit.Name; ret.state_topic = unit.ToTopic(Topic.state); ret.command_topic = unit.ToTopic(Topic.command); ret.brightness_state_topic = unit.ToTopic(Topic.brightness_state); @@ -145,7 +224,7 @@ namespace OmniLinkBridge.MQTT public static Switch ToConfigSwitch(this clsUnit unit) { Switch ret = new Switch(); - ret.name = unit.Name; + ret.name = Global.mqtt_discovery_name_prefix + unit.Name; ret.state_topic = unit.ToTopic(Topic.state); ret.command_topic = unit.ToTopic(Topic.command); return ret; @@ -168,13 +247,23 @@ namespace OmniLinkBridge.MQTT public static string ToTopic(this clsThermostat thermostat, Topic topic) { - return $"omnilink/thermostat{thermostat.Number.ToString()}/{topic.ToString()}"; + return $"{Global.mqtt_prefix}/thermostat{thermostat.Number.ToString()}/{topic.ToString()}"; + } + + public static Sensor ToConfigHumidity(this clsThermostat zone) + { + Sensor ret = new Sensor(); + ret.name = Global.mqtt_discovery_name_prefix + zone.Name; + ret.device_class = Sensor.DeviceClass.humidity; + ret.state_topic = zone.ToTopic(Topic.current_humidity); + ret.unit_of_measurement = "%"; + return ret; } public static Climate ToConfig(this clsThermostat thermostat) { Climate ret = new Climate(); - ret.name = thermostat.Name; + ret.name = Global.mqtt_discovery_name_prefix + thermostat.Name; ret.current_temperature_topic = thermostat.ToTopic(Topic.current_temperature); ret.temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state); @@ -208,13 +297,13 @@ namespace OmniLinkBridge.MQTT public static string ToTopic(this clsButton button, Topic topic) { - return $"omnilink/button{button.Number.ToString()}/{topic.ToString()}"; + return $"{Global.mqtt_prefix}/button{button.Number.ToString()}/{topic.ToString()}"; } public static Switch ToConfig(this clsButton button) { Switch ret = new Switch(); - ret.name = button.Name; + ret.name = Global.mqtt_discovery_name_prefix + button.Name; ret.state_topic = button.ToTopic(Topic.state); ret.command_topic = button.ToTopic(Topic.command); return ret; diff --git a/OmniLinkBridge/MQTT/Sensor.cs b/OmniLinkBridge/MQTT/Sensor.cs index bda9a03..717ab17 100644 --- a/OmniLinkBridge/MQTT/Sensor.cs +++ b/OmniLinkBridge/MQTT/Sensor.cs @@ -20,7 +20,13 @@ namespace OmniLinkBridge.MQTT [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; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public string value_template { get; set; } } } diff --git a/OmniLinkBridge/MQTT/Topics.cs b/OmniLinkBridge/MQTT/Topics.cs index 8f759e0..3af3003 100644 --- a/OmniLinkBridge/MQTT/Topics.cs +++ b/OmniLinkBridge/MQTT/Topics.cs @@ -25,6 +25,8 @@ namespace OmniLinkBridge.MQTT public static Topic state { get { return new Topic("state"); } } public static Topic command { get { return new Topic("command"); } } + public static Topic basic_state { get { return new Topic("basic_state"); } } + public static Topic brightness_state { get { return new Topic("brightness_state"); } } public static Topic brightness_command { get { return new Topic("brightness_command"); } } diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index 32157fb..e27bd65 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -23,7 +23,7 @@ namespace OmniLinkBridge.Modules private OmniLinkII OmniLink { get; set; } private IManagedMqttClient MqttClient { get; set; } - private Regex regexTopic = new Regex("omnilink/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); + private Regex regexTopic = new Regex(Global.mqtt_prefix + "/([A-Za-z]+)([0-9]+)/(.*)", RegexOptions.Compiled); private readonly AutoResetEvent trigger = new AutoResetEvent(false); @@ -59,20 +59,20 @@ namespace OmniLinkBridge.Modules MqttClient.ApplicationMessageReceived += MqttClient_ApplicationMessageReceived; - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.brightness_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.temperature_heat_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.temperature_cool_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.humidify_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.dehumidify_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.mode_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.fan_mode_command).Build()); - MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic("omnilink/+/" + Topic.hold_command).Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.brightness_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.temperature_heat_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.temperature_cool_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.humidify_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.dehumidify_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.mode_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.fan_mode_command}").Build()); + MqttClient.SubscribeAsync(new TopicFilterBuilder().WithTopic($"{Global.mqtt_prefix}/+/{Topic.hold_command}").Build()); // Wait until shutdown trigger.WaitOne(); - MqttClient.PublishAsync("omnilink/status", "offline", MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_prefix}/status", "offline", MqttQualityOfServiceLevel.AtMostOnce, true); } private void MqttClient_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e) @@ -90,6 +90,10 @@ namespace OmniLinkBridge.Modules { ProcessAreaReceived(OmniLink.Controller.Areas[areaId], match.Groups[3].Value, payload); } + if (match.Groups[1].Value == "zone" && ushort.TryParse(match.Groups[2].Value, out ushort zoneId) && zoneId < OmniLink.Controller.Zones.Count) + { + ProcessZoneReceived(OmniLink.Controller.Zones[zoneId], match.Groups[3].Value, payload); + } else if (match.Groups[1].Value == "unit" && ushort.TryParse(match.Groups[2].Value, out ushort unitId) && unitId < OmniLink.Controller.Units.Count) { ProcessUnitReceived(OmniLink.Controller.Units[unitId], match.Groups[3].Value, payload); @@ -108,38 +112,58 @@ namespace OmniLinkBridge.Modules { if (string.Compare(command, Topic.command.ToString()) == 0) { - switch(payload) + if(string.Compare(payload, "arm_home", true) == 0) { - case "ARM_HOME": - log.Debug("SetArea: " + area.Number + " to home"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDay, 0, (ushort)area.Number); - break; - case "ARM_AWAY": - log.Debug("SetArea: " + area.Number + " to away"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityAway, 0, (ushort)area.Number); - break; - case "ARM_NIGHT": - log.Debug("SetArea: " + area.Number + " to night"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNight, 0, (ushort)area.Number); - break; - case "DISARM": - log.Debug("SetArea: " + area.Number + " to disarm"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityOff, 0, (ushort)area.Number); - break; + log.Debug("SetArea: " + area.Number + " to home"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDay, 0, (ushort)area.Number); + } + else if (string.Compare(payload, "arm_away", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to away"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityAway, 0, (ushort)area.Number); + } + else if (string.Compare(payload, "arm_night", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to night"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNight, 0, (ushort)area.Number); + } + else if (string.Compare(payload, "disarm", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to disarm"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityOff, 0, (ushort)area.Number); + } + // The below aren't supported by Home Assistant + else if (string.Compare(payload, "arm_home_instant", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to home instant"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDyi, 0, (ushort)area.Number); + } + else if (string.Compare(payload, "arm_night_delay", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to night delay"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNtd, 0, (ushort)area.Number); + } + else if (string.Compare(payload, "arm_vacation", true) == 0) + { + log.Debug("SetArea: " + area.Number + " to vacation"); + OmniLink.Controller.SendCommand(enuUnitCommand.SecurityVac, 0, (ushort)area.Number); + } + } + } - // The below aren't supported by Home Assistant - case "ARM_HOME_INSTANT": - log.Debug("SetArea: " + area.Number + " to home instant"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityDyi, 0, (ushort)area.Number); - break; - case "ARM_NIGHT_DELAY": - log.Debug("SetArea: " + area.Number + " to night delay"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityNtd, 0, (ushort)area.Number); - break; - case "ARM_VACATION": - log.Debug("SetArea: " + area.Number + " to vacation"); - OmniLink.Controller.SendCommand(enuUnitCommand.SecurityVac, 0, (ushort)area.Number); - break; + private void ProcessZoneReceived(clsZone zone, string command, string payload) + { + if (string.Compare(command, Topic.command.ToString()) == 0) + { + if (string.Compare(payload, "bypass", true) == 0) + { + log.Debug("SetZone: " + zone.Number + " to " + payload); + OmniLink.Controller.SendCommand(enuUnitCommand.Bypass, 0, (ushort)zone.Number); + } + else if (string.Compare(payload, "restore", true) == 0) + { + log.Debug("SetZone: " + zone.Number + " to " + payload); + OmniLink.Controller.SendCommand(enuUnitCommand.Restore, 0, (ushort)zone.Number); } } } @@ -232,7 +256,7 @@ namespace OmniLinkBridge.Modules { PublishConfig(); - MqttClient.PublishAsync("omnilink/status", "online", MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_prefix}/status", "online", MqttQualityOfServiceLevel.AtMostOnce, true); } private void PublishConfig() @@ -254,13 +278,13 @@ namespace OmniLinkBridge.Modules if (area.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/omnilink/area{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); continue; } PublishAreaState(area); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/omnilink/area{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/alarm_control_panel/{Global.mqtt_prefix}/area{i.ToString()}/config", JsonConvert.SerializeObject(area.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); } } @@ -275,28 +299,26 @@ namespace OmniLinkBridge.Modules if (zone.DefaultProperties == true || Global.mqtt_discovery_ignore_zones.Contains(zone.Number)) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/omnilink/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); continue; } PublishZoneState(zone); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}/config", + JsonConvert.SerializeObject(zone.ToConfigSensor()), MqttQualityOfServiceLevel.AtMostOnce, true); + if (zone.IsTemperatureZone()) - { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}temp/config", JsonConvert.SerializeObject(zone.ToConfigTemp()), MqttQualityOfServiceLevel.AtMostOnce, true); - } else if (zone.IsHumidityZone()) - { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/omnilink/zone{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/zone{i.ToString()}humidity/config", JsonConvert.SerializeObject(zone.ToConfigHumidity()), MqttQualityOfServiceLevel.AtMostOnce, true); - } - else - { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/binary_sensor/omnilink/zone{i.ToString()}/config", - JsonConvert.SerializeObject(zone.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); - } } } @@ -312,13 +334,13 @@ namespace OmniLinkBridge.Modules if (unit.DefaultProperties == true || Global.mqtt_discovery_ignore_units.Contains(unit.Number)) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/omnilink/unit{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); continue; } PublishUnitState(unit); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/omnilink/unit{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/{type}/{Global.mqtt_prefix}/unit{i.ToString()}/config", JsonConvert.SerializeObject(unit.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); } } @@ -333,14 +355,17 @@ namespace OmniLinkBridge.Modules if (thermostat.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/omnilink/thermostat{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); continue; } PublishThermostatState(thermostat); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/omnilink/thermostat{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i.ToString()}/config", JsonConvert.SerializeObject(thermostat.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/sensor/{Global.mqtt_prefix}/thermostat{i.ToString()}humidity/config", + JsonConvert.SerializeObject(thermostat.ToConfigHumidity()), MqttQualityOfServiceLevel.AtMostOnce, true); } } @@ -354,14 +379,14 @@ namespace OmniLinkBridge.Modules if (button.DefaultProperties == true) { - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/omnilink/button{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", null, MqttQualityOfServiceLevel.AtMostOnce, true); continue; } // Buttons are always off MqttClient.PublishAsync(button.ToTopic(Topic.state), "OFF", MqttQualityOfServiceLevel.AtMostOnce, true); - MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/omnilink/button{i.ToString()}/config", + MqttClient.PublishAsync($"{Global.mqtt_discovery_prefix}/switch/{Global.mqtt_prefix}/button{i.ToString()}/config", JsonConvert.SerializeObject(button.ToConfig()), MqttQualityOfServiceLevel.AtMostOnce, true); } } @@ -369,6 +394,31 @@ namespace OmniLinkBridge.Modules private void Omnilink_OnAreaStatus(object sender, AreaStatusEventArgs e) { PublishAreaState(e.Area); + + // Since the controller doesn't fire zone status change on area status change + // request update so armed, tripped, and secure statuses are correct + for (ushort i = 1; i < OmniLink.Controller.Zones.Count; i++) + { + clsZone zone = OmniLink.Controller.Zones[i]; + + if (zone.DefaultProperties == false && zone.Area == e.Area.Number) + OmniLink.Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(OmniLink.Controller.Connection, enuObjectType.Zone, i, i), HandleRequestZoneStatus); + } + } + + private void HandleRequestZoneStatus(clsOmniLinkMessageQueueItem M, byte[] B, bool Timeout) + { + if (Timeout) + return; + + clsOL2MsgExtendedStatus MSG = new clsOL2MsgExtendedStatus(OmniLink.Controller.Connection, B); + + for (byte i = 0; i < MSG.ZoneStatusCount(); i++) + { + clsZone zone = OmniLink.Controller.Zones[MSG.ObjectNumber(i)]; + zone.CopyExtendedStatus(MSG, i); + MqttClient.PublishAsync(zone.ToTopic(Topic.state), zone.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + } } private void Omnilink_OnZoneStatus(object sender, ZoneStatusEventArgs e) @@ -390,11 +440,18 @@ namespace OmniLinkBridge.Modules private void PublishAreaState(clsArea area) { MqttClient.PublishAsync(area.ToTopic(Topic.state), area.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(area.ToTopic(Topic.basic_state), area.ToBasicState(), MqttQualityOfServiceLevel.AtMostOnce, true); } private void PublishZoneState(clsZone zone) { MqttClient.PublishAsync(zone.ToTopic(Topic.state), zone.ToState(), MqttQualityOfServiceLevel.AtMostOnce, true); + MqttClient.PublishAsync(zone.ToTopic(Topic.basic_state), zone.ToBasicState(), MqttQualityOfServiceLevel.AtMostOnce, true); + + if(zone.IsTemperatureZone()) + MqttClient.PublishAsync(zone.ToTopic(Topic.current_temperature), zone.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); + else if (zone.IsHumidityZone()) + MqttClient.PublishAsync(zone.ToTopic(Topic.current_humidity), zone.TempText(), MqttQualityOfServiceLevel.AtMostOnce, true); } private void PublishUnitState(clsUnit unit) diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs index b7cfba7..5929177 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -391,7 +391,7 @@ namespace OmniLinkBridge.Modules case enuObjectType.Zone: Controller.Zones.CopyProperties(MSG); - if (Controller.Zones[MSG.ObjectNumber].IsTemperatureZone()) + if (Controller.Zones[MSG.ObjectNumber].IsTemperatureZone() || Controller.Zones[MSG.ObjectNumber].IsHumidityZone()) Controller.Connection.Send(new clsOL2MsgRequestExtendedStatus(Controller.Connection, enuObjectType.Auxillary, MSG.ObjectNumber, MSG.ObjectNumber), HandleRequestAuxillaryStatus); break; diff --git a/OmniLinkBridge/OmniLinkBridge.ini b/OmniLinkBridge/OmniLinkBridge.ini index 032eb8b..bb3cd42 100644 --- a/OmniLinkBridge/OmniLinkBridge.ini +++ b/OmniLinkBridge/OmniLinkBridge.ini @@ -36,8 +36,15 @@ mqtt_server = mqtt_port = 1883 mqtt_username = mqtt_password = +# If you have multiple Omni Controllers you will want to change the +# mqtt_prefix and mqtt_discovery_name_prefix to prevent collisions. +# Prefix for MQTT state / command topics +mqtt_prefix = omnilink +# Prefix for Home Assistant discovery mqtt_discovery_prefix = homeassistant -# specify a range of numbers like 1,2,3,5-10 +# Prefix for Home Assistant entity names +mqtt_discovery_name_prefix = +# Specify a range of numbers like 1,2,3,5-10 mqtt_discovery_ignore_zones = mqtt_discovery_ignore_units = # device_class must be battery, door, garage_door, gas, moisture, motion, problem, smoke, or window diff --git a/OmniLinkBridge/Properties/AssemblyInfo.cs b/OmniLinkBridge/Properties/AssemblyInfo.cs index 068e912..87493de 100644 --- a/OmniLinkBridge/Properties/AssemblyInfo.cs +++ b/OmniLinkBridge/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ using System.Runtime.InteropServices; // 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.1.0")] -[assembly: AssemblyFileVersion("1.1.1.0")] +[assembly: AssemblyVersion("1.1.2.0")] +[assembly: AssemblyFileVersion("1.1.2.0")] diff --git a/OmniLinkBridge/Settings.cs b/OmniLinkBridge/Settings.cs index 2e54e56..2ae5d19 100644 --- a/OmniLinkBridge/Settings.cs +++ b/OmniLinkBridge/Settings.cs @@ -55,7 +55,13 @@ namespace OmniLinkBridge Global.mqtt_port = ValidatePort(settings, "mqtt_port"); Global.mqtt_username = settings["mqtt_username"]; Global.mqtt_password = settings["mqtt_password"]; - Global.mqtt_discovery_prefix = settings["mqtt_discovery_prefix"]; + Global.mqtt_prefix = settings["mqtt_prefix"] ?? "omnilink"; + Global.mqtt_discovery_prefix = settings["mqtt_discovery_prefix"] ?? "homeassistant"; + Global.mqtt_discovery_name_prefix = settings["mqtt_discovery_name_prefix"] ?? string.Empty; + + if (!string.IsNullOrEmpty(Global.mqtt_discovery_name_prefix)) + Global.mqtt_discovery_name_prefix = Global.mqtt_discovery_name_prefix + " "; + Global.mqtt_discovery_ignore_zones = ValidateRange(settings, "mqtt_discovery_ignore_zones"); Global.mqtt_discovery_ignore_units = ValidateRange(settings, "mqtt_discovery_ignore_units"); Global.mqtt_discovery_override_zone = LoadOverrideZone(settings, "mqtt_discovery_override_zone"); diff --git a/README.md b/README.md index b46e3af..18d3916 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # OmniLink Bridge -Provides time sync, logging, web service API, and MQTT bridge for HAI/Leviton OmniPro II controllers +Provides time sync, logging, web service API, and MQTT bridge for HAI/Leviton OmniPro II controllers. Provides integration with Samsung SmarthThings via Web Service API and Home Assistant via MQTT. ## Download -You can download the [binary here](http://www.excalibur-partners.com/downloads/OmniLinkBridge_1_1_1.zip) or use docker to build an image from git. +You can download the [binary here](http://www.excalibur-partners.com/downloads/OmniLinkBridge_1_1_2.zip) or use docker to build an image from git. ## Requirements - .NET Framework 4.5.2 (or Mono equivalent) @@ -53,7 +53,7 @@ You can download the [binary here](http://www.excalibur-partners.com/downloads/O - cp OmniLinkBridge/OmniLinkBridge.ini /opt/omnilink-bridge - vim /opt/omnilink-bridge/OmniLinkBridge.ini 3. Start docker container - - docker run -d --name="omnilink-bridge" -v /opt/omnilink-bridge:/config --net=host --restart unless-stopped omnilink-bridge + - docker run -d --name="omnilink-bridge" -v /opt/omnilink-bridge:/config -v /etc/localtime:/etc/localtime:ro --net=host --restart unless-stopped omnilink-bridge 4. Verify connectivity by looking at logs - docker container logs omnilink-bridge @@ -86,12 +86,38 @@ To test the API you can use your browser to view a page or PowerShell (see below ## MQTT This module will also publish discovery topics for Home Assistant to auto configure devices. +### Areas +``` SUB omnilink/areaX/state +string triggered, pending, armed_night, armed_night_delay, armed_home, armed_home_instant, armed_away, armed_vacation, disarmed + +SUB omnilink/areaX/basic_state string triggered, pending, armed_night, armed_home, armed_away, disarmed PUB omnilink/areaX/command -string ARM_HOME, ARM_AWAY, ARM_NIGHT, DISARM, ARM_HOME_INSTANT, ARM_NIGHT_DELAY, ARM_VACATION +string(insensitive) arm_home, arm_away, arm_night, disarm, arm_home_instant, arm_night_delay, arm_vacation +``` +### Zones +``` +SUB omnilink/zoneX/state +string secure, not_ready, trouble, armed, tripped, bypassed + +SUB omnilink/zoneX/basic_state +string OFF, ON + +SUB omnilink/zoneX/current_temperature (optional) +int Current temperature in degrees fahrenheit + +SUB omnilink/zoneX/current_humidity (optional) +int Current relative humidity + +PUB omnilink/zoneX/command +string(insensitive) bypass, restore +``` + +### Units +``` SUB omnilink/unitX/state PUB omnilink/unitX/command string OFF, ON @@ -99,7 +125,10 @@ string OFF, ON SUB omnilink/unitX/brightness_state PUB omnilink/unitX/brightness_command int Level from 0 to 100 percent +``` +### Thermostats +``` SUB omnilink/thermostatX/current_operation string idle, cool, heat @@ -132,14 +161,27 @@ string auto, on, cycle SUB omnilink/thermostatX/hold_state PUB omnilink/thermostatX/hold_command string off, hold +``` +### Buttons +``` SUB omnilink/buttonX/state string OFF PUB omnilink/buttonX/command string ON +``` ## Change Log +Version 1.1.2 - 2018-10-23 +- Add min and max climate temperatures +- Update docker run command to use local time zone +- Improve area and zone MQTT support +- Add option to change MQTT prefix to support multiple instances +- Add detailed zone sensor and thermostat humidity sensor +- Add prefix for MQTT discovery entity name +- Request zone status update on area status change + Version 1.1.1 - 2018-10-18 - Added docker support - Save subscriptions on change @@ -189,4 +231,4 @@ Version 1.0.1 - 2012-12-30 - Fixed crash when controller time not initially set Version 1.0.0 - 2012-12-29 -- Initial release \ No newline at end of file +- Initial release