From a016e1cd6450b824694b57f649ad45a1f658846b Mon Sep 17 00:00:00 2001 From: Ryan Wagoner Date: Thu, 20 Oct 2022 23:44:27 -0400 Subject: [PATCH] Implement MQTT thermostat offline status --- OmniLinkBridge/MQTT/Availability.cs | 7 +++ OmniLinkBridge/MQTT/Climate.cs | 2 + OmniLinkBridge/MQTT/Device.cs | 17 +++++++ OmniLinkBridge/MQTT/MappingExtensions.cs | 51 +++++++++++-------- OmniLinkBridge/MQTT/Topic.cs | 1 + OmniLinkBridge/Modules/LoggerModule.cs | 4 ++ OmniLinkBridge/Modules/MQTTModule.cs | 16 ++++-- OmniLinkBridge/Modules/OmniLinkII.cs | 36 +++++-------- .../OmniLink/ThermostatStatusEventArgs.cs | 5 ++ OmniLinkBridge/OmniLinkBridge.csproj | 1 + README.md | 16 +++++- 11 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 OmniLinkBridge/MQTT/Availability.cs diff --git a/OmniLinkBridge/MQTT/Availability.cs b/OmniLinkBridge/MQTT/Availability.cs new file mode 100644 index 0000000..1d19e45 --- /dev/null +++ b/OmniLinkBridge/MQTT/Availability.cs @@ -0,0 +1,7 @@ +namespace OmniLinkBridge.MQTT +{ + public class Availability + { + public string topic { get; set; } = $"{Global.mqtt_prefix}/status"; + } +} diff --git a/OmniLinkBridge/MQTT/Climate.cs b/OmniLinkBridge/MQTT/Climate.cs index aa5c070..6eb260b 100644 --- a/OmniLinkBridge/MQTT/Climate.cs +++ b/OmniLinkBridge/MQTT/Climate.cs @@ -4,6 +4,8 @@ namespace OmniLinkBridge.MQTT { public class Climate : Device { + public string status { get; set; } + public string action_topic { get; set; } public string current_temperature_topic { get; set; } diff --git a/OmniLinkBridge/MQTT/Device.cs b/OmniLinkBridge/MQTT/Device.cs index 60256a0..1a9e4fd 100644 --- a/OmniLinkBridge/MQTT/Device.cs +++ b/OmniLinkBridge/MQTT/Device.cs @@ -1,10 +1,20 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using OmniLinkBridge.Modules; +using System.Collections.Generic; namespace OmniLinkBridge.MQTT { public class Device { + [JsonConverter(typeof(StringEnumConverter))] + public enum AvailabilityMode + { + all, + any, + latest + } + public string unique_id { get; set; } public string name { get; set; } @@ -12,8 +22,15 @@ namespace OmniLinkBridge.MQTT [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string state_topic { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string availability_topic { get; set; } = $"{Global.mqtt_prefix}/status"; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public List availability { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public AvailabilityMode? availability_mode { get; set; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public DeviceRegistry device { get; set; } = MQTTModule.MqttDeviceRegistry; } diff --git a/OmniLinkBridge/MQTT/MappingExtensions.cs b/OmniLinkBridge/MQTT/MappingExtensions.cs index 5db0507..22288e4 100644 --- a/OmniLinkBridge/MQTT/MappingExtensions.cs +++ b/OmniLinkBridge/MQTT/MappingExtensions.cs @@ -484,6 +484,17 @@ namespace OmniLinkBridge.MQTT { Climate ret = new Climate { + unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}", + name = Global.mqtt_discovery_name_prefix + thermostat.Name, + + availability_topic = null, + availability_mode = Device.AvailabilityMode.all, + availability = new List() + { + new Availability(), + new Availability() { topic = thermostat.ToTopic(Topic.status) } + }, + modes = thermostat.Type switch { enuThermostatType.AutoHeatCool => new List(new string[] { "auto", "off", "cool", "heat" }), @@ -491,7 +502,25 @@ namespace OmniLinkBridge.MQTT enuThermostatType.HeatOnly => new List(new string[] { "off", "heat" }), enuThermostatType.CoolOnly => new List(new string[] { "off", "cool" }), _ => new List(new string[] { "off" }), - } + }, + + action_topic = thermostat.ToTopic(Topic.current_operation), + current_temperature_topic = thermostat.ToTopic(Topic.current_temperature), + + temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state), + temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command), + + temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state), + temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command), + + mode_state_topic = thermostat.ToTopic(Topic.mode_basic_state), + mode_command_topic = thermostat.ToTopic(Topic.mode_command), + + fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state), + fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command), + + preset_mode_state_topic = thermostat.ToTopic(Topic.hold_state), + preset_mode_command_topic = thermostat.ToTopic(Topic.hold_command) }; if (format == enuTempFormat.Celsius) @@ -500,26 +529,6 @@ namespace OmniLinkBridge.MQTT ret.max_temp = "35"; } - ret.unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}"; - ret.name = Global.mqtt_discovery_name_prefix + thermostat.Name; - - ret.action_topic = thermostat.ToTopic(Topic.current_operation); - ret.current_temperature_topic = thermostat.ToTopic(Topic.current_temperature); - - ret.temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state); - ret.temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command); - - ret.temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state); - ret.temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command); - - ret.mode_state_topic = thermostat.ToTopic(Topic.mode_basic_state); - ret.mode_command_topic = thermostat.ToTopic(Topic.mode_command); - - ret.fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state); - ret.fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command); - - ret.preset_mode_state_topic = thermostat.ToTopic(Topic.hold_state); - ret.preset_mode_command_topic = thermostat.ToTopic(Topic.hold_command); return ret; } diff --git a/OmniLinkBridge/MQTT/Topic.cs b/OmniLinkBridge/MQTT/Topic.cs index 48016d6..96f9af3 100644 --- a/OmniLinkBridge/MQTT/Topic.cs +++ b/OmniLinkBridge/MQTT/Topic.cs @@ -3,6 +3,7 @@ public enum Topic { name, + status, state, command, alarm_command, diff --git a/OmniLinkBridge/Modules/LoggerModule.cs b/OmniLinkBridge/Modules/LoggerModule.cs index 45a7917..fa9bc4c 100644 --- a/OmniLinkBridge/Modules/LoggerModule.cs +++ b/OmniLinkBridge/Modules/LoggerModule.cs @@ -246,6 +246,10 @@ namespace OmniLinkBridge.Modules humidity + "','" + humidify + "','" + dehumidify + "','" + e.Thermostat.ModeText() + "','" + e.Thermostat.FanModeText() + "','" + e.Thermostat.HoldStatusText() + "')"); + if (e.Offline) + log.Warning("Unknown temp for Thermostat {thermostatName}, verify thermostat is online", + e.Thermostat.Name); + // Ignore events fired by thermostat polling if (!e.EventTimer && Global.verbose_thermostat) log.Verbose("ThermostatStatus {id} {name}, Status: {temp} {status}, " + diff --git a/OmniLinkBridge/Modules/MQTTModule.cs b/OmniLinkBridge/Modules/MQTTModule.cs index 104fc9a..8ed796e 100644 --- a/OmniLinkBridge/Modules/MQTTModule.cs +++ b/OmniLinkBridge/Modules/MQTTModule.cs @@ -153,7 +153,7 @@ namespace OmniLinkBridge.Modules private void PublishControllerStatus(string status) { log.Information("Publishing controller {status}", status); - PublishAsync($"{Global.mqtt_prefix}/status", status); + PublishAsync($"{Global.mqtt_prefix}/{Topic.status}", status); } private void PublishConfig() @@ -357,6 +357,7 @@ namespace OmniLinkBridge.Modules PublishThermostatState(thermostat); PublishAsync(thermostat.ToTopic(Topic.name), thermostat.Name); + PublishAsync(thermostat.ToTopic(Topic.status), ONLINE); PublishAsync($"{Global.mqtt_discovery_prefix}/climate/{Global.mqtt_prefix}/thermostat{i}/config", JsonConvert.SerializeObject(thermostat.ToConfig(OmniLink.Controller.TempFormat))); PublishAsync($"{Global.mqtt_discovery_prefix}/number/{Global.mqtt_prefix}/thermostat{i}humidify/config", @@ -470,8 +471,17 @@ namespace OmniLinkBridge.Modules return; // Ignore events fired by thermostat polling - if (!e.EventTimer) - PublishThermostatState(e.Thermostat); + if (e.EventTimer) + return; + + if (e.Offline) + { + PublishAsync(e.Thermostat.ToTopic(Topic.status), OFFLINE); + return; + } + + PublishAsync(e.Thermostat.ToTopic(Topic.status), ONLINE); + PublishThermostatState(e.Thermostat); } private async void OmniLink_OnButtonStatus(object sender, ButtonStatusEventArgs e) diff --git a/OmniLinkBridge/Modules/OmniLinkII.cs b/OmniLinkBridge/Modules/OmniLinkII.cs index e7a8fe5..b5a5efb 100644 --- a/OmniLinkBridge/Modules/OmniLinkII.cs +++ b/OmniLinkBridge/Modules/OmniLinkII.cs @@ -636,19 +636,13 @@ namespace OmniLinkBridge.Modules { Controller.Thermostats[MSG.ObjectNumber(i)].CopyExtendedStatus(MSG, i); - // Don't fire event when invalid temperature of 0 is sometimes received - if (Controller.Thermostats[MSG.ObjectNumber(i)].Temp > 0) + OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() { - OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() - { - ID = MSG.ObjectNumber(i), - Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)], - EventTimer = false - }); - } - else if (Global.verbose_thermostat_timer) - log.Debug("Ignoring unsolicited unknown temp for Thermostat {thermostatName}", - Controller.Thermostats[MSG.ObjectNumber(i)].Name); + ID = MSG.ObjectNumber(i), + Thermostat = Controller.Thermostats[MSG.ObjectNumber(i)], + Offline = Controller.Thermostats[MSG.ObjectNumber(i)].Temp == 0, + EventTimer = false + }); if (!tstats.ContainsKey(MSG.ObjectNumber(i))) tstats.Add(MSG.ObjectNumber(i), DateTime.Now); @@ -732,19 +726,13 @@ namespace OmniLinkBridge.Modules (Controller.Connection.ConnectionState == enuOmniLinkConnectionState.Online || Controller.Connection.ConnectionState == enuOmniLinkConnectionState.OnlineSecure)) { - // Don't fire event when invalid temperature of 0 is sometimes received - if (Controller.Thermostats[tstat.Key].Temp > 0) + OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() { - OnThermostatStatus?.Invoke(this, new ThermostatStatusEventArgs() - { - ID = tstat.Key, - Thermostat = Controller.Thermostats[tstat.Key], - EventTimer = true - }); - } - else if (Global.verbose_thermostat_timer) - log.Warning("Ignoring unknown temp for Thermostat {thermostatName}", - Controller.Thermostats[tstat.Key].Name); + ID = tstat.Key, + Thermostat = Controller.Thermostats[tstat.Key], + Offline = Controller.Thermostats[tstat.Key].Temp == 0, + EventTimer = true + }); } else if (Global.verbose_thermostat_timer) log.Warning("Not logging out of date status for Thermostat {thermostatName}", diff --git a/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs b/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs index 4abcfff..ad055e7 100644 --- a/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs +++ b/OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs @@ -8,6 +8,11 @@ namespace OmniLinkBridge.OmniLink public ushort ID { get; set; } public clsThermostat Thermostat { get; set; } + /// + /// Set to true when thermostat is offline, indicated by a temperature of 0 + /// + public bool Offline { get; set; } + /// /// Set to true when fired by thermostat polling /// diff --git a/OmniLinkBridge/OmniLinkBridge.csproj b/OmniLinkBridge/OmniLinkBridge.csproj index cbc9ac5..ac643a2 100644 --- a/OmniLinkBridge/OmniLinkBridge.csproj +++ b/OmniLinkBridge/OmniLinkBridge.csproj @@ -87,6 +87,7 @@ + diff --git a/README.md b/README.md index c8fbbfa..bc75088 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ OmniLink Bridge is divided into the following modules and configurable settings. - Maintains connection to the OmniLink controller - Thermostats - If no status update has been received after 4 minutes a request is issued - - A status update containing a temperature of 0 is ignored + - A status update containing a temperature of 0 marks the thermostat offline - This can occur when a ZigBee thermostat has lost communication - Time Sync: time_ - Controller time is checked and compared to the local computer time disregarding time zones @@ -146,6 +146,17 @@ systemctl start omnilinkbridge.service ``` ## MQTT +``` +SUB omnilink/status +string online, offline + +SUB omnilink/model +string Controller model + +SUB omnilink/version +string Controller version +``` + ### System ``` SUB omnilink/system/phone/state @@ -231,6 +242,9 @@ string A-L SUB omnilink/thermostatX/name string Thermostat name +SUB omnilink/thermostatX/status +string online, offline + SUB omnilink/thermostatX/current_operation string idle, cooling, heating