Skip to content

Commit

Permalink
Implement MQTT thermostat offline status
Browse files Browse the repository at this point in the history
  • Loading branch information
rwagoner committed Oct 21, 2022
1 parent 1ce5e3d commit a016e1c
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 49 deletions.
7 changes: 7 additions & 0 deletions OmniLinkBridge/MQTT/Availability.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace OmniLinkBridge.MQTT
{
public class Availability
{
public string topic { get; set; } = $"{Global.mqtt_prefix}/status";
}
}
2 changes: 2 additions & 0 deletions OmniLinkBridge/MQTT/Climate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }

Expand Down
17 changes: 17 additions & 0 deletions OmniLinkBridge/MQTT/Device.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,36 @@
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; }

[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;
}
Expand Down
51 changes: 30 additions & 21 deletions OmniLinkBridge/MQTT/MappingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,42 +484,51 @@ public static Climate ToConfig(this clsThermostat thermostat, enuTempFormat form
{
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" }),
enuThermostatType.HeatCool => new List<string>(new string[] { "off", "cool", "heat" }),
enuThermostatType.HeatOnly => new List<string>(new string[] { "off", "heat" }),
enuThermostatType.CoolOnly => new List<string>(new string[] { "off", "cool" }),
_ => new List<string>(new string[] { "off" }),
}
};
},

if (format == enuTempFormat.Celsius)
{
ret.min_temp = "7";
ret.max_temp = "35";
}
action_topic = thermostat.ToTopic(Topic.current_operation),
current_temperature_topic = thermostat.ToTopic(Topic.current_temperature),

ret.unique_id = $"{Global.mqtt_prefix}thermostat{thermostat.Number}";
ret.name = Global.mqtt_discovery_name_prefix + thermostat.Name;
temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state),
temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command),

ret.action_topic = thermostat.ToTopic(Topic.current_operation);
ret.current_temperature_topic = thermostat.ToTopic(Topic.current_temperature);
temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state),
temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command),

ret.temperature_low_state_topic = thermostat.ToTopic(Topic.temperature_heat_state);
ret.temperature_low_command_topic = thermostat.ToTopic(Topic.temperature_heat_command);
mode_state_topic = thermostat.ToTopic(Topic.mode_basic_state),
mode_command_topic = thermostat.ToTopic(Topic.mode_command),

ret.temperature_high_state_topic = thermostat.ToTopic(Topic.temperature_cool_state);
ret.temperature_high_command_topic = thermostat.ToTopic(Topic.temperature_cool_command);
fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state),
fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command),

ret.mode_state_topic = thermostat.ToTopic(Topic.mode_basic_state);
ret.mode_command_topic = thermostat.ToTopic(Topic.mode_command);
preset_mode_state_topic = thermostat.ToTopic(Topic.hold_state),
preset_mode_command_topic = thermostat.ToTopic(Topic.hold_command)
};

ret.fan_mode_state_topic = thermostat.ToTopic(Topic.fan_mode_state);
ret.fan_mode_command_topic = thermostat.ToTopic(Topic.fan_mode_command);
if (format == enuTempFormat.Celsius)
{
ret.min_temp = "7";
ret.max_temp = "35";
}

ret.preset_mode_state_topic = thermostat.ToTopic(Topic.hold_state);
ret.preset_mode_command_topic = thermostat.ToTopic(Topic.hold_command);
return ret;
}

Expand Down
1 change: 1 addition & 0 deletions OmniLinkBridge/MQTT/Topic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum Topic
{
name,
status,
state,
command,
alarm_command,
Expand Down
4 changes: 4 additions & 0 deletions OmniLinkBridge/Modules/LoggerModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ INSERT INTO log_thermostats (timestamp, id, name,
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}, " +
Expand Down
16 changes: 13 additions & 3 deletions OmniLinkBridge/Modules/MQTTModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ private void OmniLink_OnDisconnect(object sender, EventArgs e)
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()
Expand Down Expand Up @@ -357,6 +357,7 @@ private void PublishThermostats()
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",
Expand Down Expand Up @@ -470,8 +471,17 @@ private void Omnilink_OnThermostatStatus(object sender, ThermostatStatusEventArg
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)
Expand Down
36 changes: 12 additions & 24 deletions OmniLinkBridge/Modules/OmniLinkII.cs
Original file line number Diff line number Diff line change
Expand Up @@ -636,19 +636,13 @@ private void HandleUnsolicitedExtendedStatus(byte[] B)
{
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);
Expand Down Expand Up @@ -732,19 +726,13 @@ private void tstat_timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e
(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}",
Expand Down
5 changes: 5 additions & 0 deletions OmniLinkBridge/OmniLink/ThermostatStatusEventArgs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ public class ThermostatStatusEventArgs : EventArgs
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>
Expand Down
1 change: 1 addition & 0 deletions OmniLinkBridge/OmniLinkBridge.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" />
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a016e1c

Please sign in to comment.