Implement MQTT thermostat offline status

This commit is contained in:
Ryan Wagoner 2022-10-20 23:44:27 -04:00
parent 1ce5e3dab9
commit a016e1cd64
11 changed files with 107 additions and 49 deletions

View file

@ -0,0 +1,7 @@
namespace OmniLinkBridge.MQTT
{
public class Availability
{
public string topic { get; set; } = $"{Global.mqtt_prefix}/status";
}
}

View file

@ -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; }

View file

@ -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> availability { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public AvailabilityMode? availability_mode { get; set; }
[JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
public DeviceRegistry device { get; set; } = MQTTModule.MqttDeviceRegistry;
}

View file

@ -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<Availability>()
{
new Availability(),
new Availability() { topic = thermostat.ToTopic(Topic.status) }
},
modes = thermostat.Type switch
{
enuThermostatType.AutoHeatCool => new List<string>(new string[] { "auto", "off", "cool", "heat" }),
@ -491,7 +502,25 @@ namespace OmniLinkBridge.MQTT
enuThermostatType.HeatOnly => new List<string>(new string[] { "off", "heat" }),
enuThermostatType.CoolOnly => new List<string>(new string[] { "off", "cool" }),
_ => new List<string>(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;
}

View file

@ -3,6 +3,7 @@
public enum Topic
{
name,
status,
state,
command,
alarm_command,

View file

@ -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}, " +

View file

@ -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)

View file

@ -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}",

View file

@ -8,6 +8,11 @@ namespace OmniLinkBridge.OmniLink
public ushort ID { get; set; }
public clsThermostat Thermostat { get; set; }
/// <summary>
/// Set to true when thermostat is offline, indicated by a temperature of 0
/// </summary>
public bool Offline { get; set; }
/// <summary>
/// Set to true when fired by thermostat polling
/// </summary>

View file

@ -87,6 +87,7 @@
<Compile Include="MQTT\AreaCommandCode.cs" />
<Compile Include="MQTT\AreaCommands.cs" />
<Compile Include="MQTT\AreaState.cs" />
<Compile Include="MQTT\Availability.cs" />
<Compile Include="MQTT\BinarySensor.cs" />
<Compile Include="MQTT\CommandTypes.cs" />
<Compile Include="MQTT\Device.cs" />

View file

@ -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