diff --git a/src/EcoWitt.Controller.Tests/MqttPathBuilderTest.cs b/src/EcoWitt.Controller.Tests/MqttPathBuilderTest.cs index 12ffc5d..3f3477a 100644 --- a/src/EcoWitt.Controller.Tests/MqttPathBuilderTest.cs +++ b/src/EcoWitt.Controller.Tests/MqttPathBuilderTest.cs @@ -74,20 +74,38 @@ public void BuildMqttSubdeviceHACommandTopic_Wildcard() } [Test] - public void Sanitize_SpacesToHyphens() + public void SanitizeSegment_SpacesToHyphens() { - Assert.That(MqttPathBuilder.Sanitize("My Gateway Name"), Is.EqualTo("my-gateway-name")); + Assert.That(MqttPathBuilder.SanitizeSegment("My Gateway Name"), Is.EqualTo("my-gateway-name")); } [Test] - public void Sanitize_ToLowercase() + public void SanitizeSegment_ToLowercase() { - Assert.That(MqttPathBuilder.Sanitize("GW2000A"), Is.EqualTo("gw2000a")); + Assert.That(MqttPathBuilder.SanitizeSegment("GW2000A"), Is.EqualTo("gw2000a")); } [Test] - public void Sanitize_AlreadyClean() + public void SanitizeSegment_StripsSlash() { - Assert.That(MqttPathBuilder.Sanitize("gw1/sensors/tempf"), Is.EqualTo("gw1/sensors/tempf")); + Assert.That(MqttPathBuilder.SanitizeSegment("valve/test"), Is.EqualTo("valve-test")); + } + + [Test] + public void SanitizeSegment_StripsHash() + { + Assert.That(MqttPathBuilder.SanitizeSegment("sensor#1"), Is.EqualTo("sensor-1")); + } + + [Test] + public void SanitizeSegment_StripsPlus() + { + Assert.That(MqttPathBuilder.SanitizeSegment("temp+humidity"), Is.EqualTo("temp-humidity")); + } + + [Test] + public void SanitizeSegment_StripsAllWildcards() + { + Assert.That(MqttPathBuilder.SanitizeSegment("a/b+c#d E"), Is.EqualTo("a-b-c-d-e")); } } diff --git a/src/Ecowitt.Controller.sln b/src/Ecowitt.Controller.sln index 31a2804..71aae07 100644 --- a/src/Ecowitt.Controller.sln +++ b/src/Ecowitt.Controller.sln @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution", "Solution", "{B3 ..\diagrams.drawio = ..\diagrams.drawio ..\LICENSE = ..\LICENSE ..\README.md = ..\README.md - ..\.github\workflows\main.yml = ..\.github\workflows\main.yml ..\CONTRIBUTING.md = ..\CONTRIBUTING.md EndProjectSection EndProject @@ -31,6 +30,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{02EA681E-C ..\docs\architecture.md = ..\docs\architecture.md EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Github", "Github", "{BEB106CA-DDE3-4277-934E-A7E2910187D7}" + ProjectSection(SolutionItems) = preProject + ..\.github\workflows\dev.yml = ..\.github\workflows\dev.yml + ..\.github\workflows\main.yml = ..\.github\workflows\main.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Ecowitt.Controller/Dockerfile b/src/Ecowitt.Controller/Dockerfile deleted file mode 100644 index 34a79a7..0000000 --- a/src/Ecowitt.Controller/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. - -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base -WORKDIR /app -EXPOSE 8080 - -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build -ADD ./ /src/Ecowitt.Controller -WORKDIR "/src/Ecowitt.Controller" -RUN dotnet restore "./Ecowitt.Controller.csproj" - -FROM build AS publish -ARG BUILD_CONFIGURATION=Debug -WORKDIR "/src/Ecowitt.Controller" -RUN dotnet publish "./Ecowitt.Controller.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -ENV ASPNETCORE_URLS=http://+:8080 -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Ecowitt.Controller.dll"] diff --git a/src/Ecowitt.Controller/Ecowitt.Controller.csproj b/src/Ecowitt.Controller/Ecowitt.Controller.csproj index e80cbf2..b5c2db6 100644 --- a/src/Ecowitt.Controller/Ecowitt.Controller.csproj +++ b/src/Ecowitt.Controller/Ecowitt.Controller.csproj @@ -20,7 +20,6 @@ - @@ -36,11 +35,8 @@ - - - diff --git a/src/Ecowitt.Controller/Service/Mqtt/MqttPathBuilder.cs b/src/Ecowitt.Controller/Service/Mqtt/MqttPathBuilder.cs index 4827306..1aa3915 100644 --- a/src/Ecowitt.Controller/Service/Mqtt/MqttPathBuilder.cs +++ b/src/Ecowitt.Controller/Service/Mqtt/MqttPathBuilder.cs @@ -4,57 +4,61 @@ public static class MqttPathBuilder { public static string BuildMqttGatewayTopic(string gwName) { - return Sanitize(gwName); + return SanitizeSegment(gwName); } public static string BuildMqttGatewaySensorTopic(string gwName, string sensorName) { - return Sanitize($"{gwName}/sensors/{sensorName}"); + return $"{SanitizeSegment(gwName)}/sensors/{SanitizeSegment(sensorName)}"; } public static string BuildMqttGatewayDiagnosticTopic(string gwName, string sensorName) { - return Sanitize($"{gwName}/diag/{sensorName}"); + return $"{SanitizeSegment(gwName)}/diag/{SanitizeSegment(sensorName)}"; } public static string BuildMqttSubdeviceTopic(string gwName, string subdeviceName) { - return Sanitize($"{gwName}/subdevices/{subdeviceName}"); + return $"{SanitizeSegment(gwName)}/subdevices/{SanitizeSegment(subdeviceName)}"; } public static string BuildMqttSubdeviceSensorTopic(string gwName, string subdeviceName, string sensorName) { - return Sanitize($"{gwName}/subdevices/{subdeviceName}/sensors/{sensorName}"); + return $"{SanitizeSegment(gwName)}/subdevices/{SanitizeSegment(subdeviceName)}/sensors/{SanitizeSegment(sensorName)}"; } - public static string BuildMqttSubdeviceDiagnosticTopic(string gwName, string subdeviceName, string sensorName) { - return Sanitize($"{gwName}/subdevices/{subdeviceName}/diag/{sensorName}"); + return $"{SanitizeSegment(gwName)}/subdevices/{SanitizeSegment(subdeviceName)}/diag/{SanitizeSegment(sensorName)}"; } public static string BuildMqttSubdeviceCommandTopic(string gwName, string subdeviceName) { - return Sanitize($"{gwName}/subdevices/{subdeviceName}/cmd"); + return $"{SanitizeSegment(gwName)}/subdevices/{SanitizeSegment(subdeviceName)}/cmd"; } public static string BuildMqttSubdeviceCommandTopic() { - return Sanitize($"+/subdevices/+/cmd"); + return "+/subdevices/+/cmd"; } public static string BuildMqttSubdeviceHACommandTopic(string gwName, string subdeviceName) { - return Sanitize($"{gwName}/subdevices/{subdeviceName}/cmd/homeassistant"); + return $"{SanitizeSegment(gwName)}/subdevices/{SanitizeSegment(subdeviceName)}/cmd/homeassistant"; } public static string BuildMqttSubdeviceHACommandTopic() { - return Sanitize($"+/subdevices/+/cmd/homeassistant"); + return "+/subdevices/+/cmd/homeassistant"; } - public static string Sanitize(string input) + public static string SanitizeSegment(string input) { - return input.Replace(' ', '-').ToLowerInvariant(); + return input + .Replace(' ', '-') + .Replace('/', '-') + .Replace('#', '-') + .Replace('+', '-') + .ToLowerInvariant(); } -} \ No newline at end of file +} diff --git a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Consumer.cs b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Consumer.cs index 2126408..04d9b07 100644 --- a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Consumer.cs +++ b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Consumer.cs @@ -66,7 +66,7 @@ public async Task OnHandle(DiscoveryRemovalEvent message, CancellationToken canc foreach (var sensor in message.Sensors) { var sensorClassTopic = BuildSensorClassTopic(sensor.SensorClass); - var topic = $"homeassistant/{MqttPathBuilder.Sanitize($"{sensorClassTopic}/{message.DeviceName}_{sensor.Name}")}/config"; + var topic = $"homeassistant/{sensorClassTopic}/{MqttPathBuilder.SanitizeSegment($"{message.DeviceName}_{sensor.Name}")}/config"; await RemoveDiscoveryMessage(topic); _logger.LogInformation("Removed discovery for {DeviceName}/{SensorName}", message.DeviceName, sensor.Name); } diff --git a/src/Ecowitt.Controller/Service/Mqtt/MqttService.DiscoveryPublisher.cs b/src/Ecowitt.Controller/Service/Mqtt/MqttService.DiscoveryPublisher.cs index 7107700..f11127a 100644 --- a/src/Ecowitt.Controller/Service/Mqtt/MqttService.DiscoveryPublisher.cs +++ b/src/Ecowitt.Controller/Service/Mqtt/MqttService.DiscoveryPublisher.cs @@ -21,7 +21,7 @@ private async Task PublishGatewayDiscovery(Device gw) var config = DiscoveryBuilder.BuildGatewayConfig(device, _origin, "Availability", id, availabilityTopic, availabilityTopic); - await PublishDiscoveryMessage("sensor", MqttPathBuilder.Sanitize($"sensor/{gw.Name}"), config); + await PublishDiscoveryMessage("sensor", $"sensor/{MqttPathBuilder.SanitizeSegment(gw.Name)}", config); } private async Task PublishSubdeviceDiscovery(Device gw, Ecowitt.Controller.Model.Subdevice subdevice) @@ -33,7 +33,7 @@ private async Task PublishSubdeviceDiscovery(Device gw, Ecowitt.Controller.Model var config = DiscoveryBuilder.BuildGatewayConfig(device, _origin, "Availability", id, availabilityTopic, availabilityTopic); - await PublishDiscoveryMessage("sensor", MqttPathBuilder.Sanitize($"sensor/{subdevice.Nickname}"), config); + await PublishDiscoveryMessage("sensor", $"sensor/{MqttPathBuilder.SanitizeSegment(subdevice.Nickname)}", config); } private async Task PublishSubdeviceSwitchDiscovery(Device gw, Ecowitt.Controller.Model.Subdevice subdevice) @@ -47,7 +47,7 @@ private async Task PublishSubdeviceSwitchDiscovery(Device gw, Ecowitt.Controller var config = DiscoveryBuilder.BuildSwitchConfig(device, _origin, "switch", id, statetopic, cmdTopic, valueTemplate: valueTemplate); - await PublishDiscoveryMessage("switch", MqttPathBuilder.Sanitize($"switch/{subdevice.Nickname}"), config); + await PublishDiscoveryMessage("switch", $"switch/{MqttPathBuilder.SanitizeSegment(subdevice.Nickname)}", config); } private async Task PublishSensorDiscovery(Device gw, ISensor sensor) @@ -83,7 +83,7 @@ private async Task PublishSensorDiscovery(Model.Discovery.Device device, ISensor var sensorClassTopic = BuildSensorClassTopic(sensor.SensorClass); - await PublishDiscoveryMessage(sensorClassTopic, MqttPathBuilder.Sanitize($"{sensorClassTopic}/{device.Name}_{sensor.Name}"), config); + await PublishDiscoveryMessage(sensorClassTopic, $"{sensorClassTopic}/{MqttPathBuilder.SanitizeSegment($"{device.Name}_{sensor.Name}")}", config); } private async Task PublishDiscoveryMessage(string domain, string topic, Config config) diff --git a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Events.cs b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Events.cs index 9e2faa0..fe1f1d1 100644 --- a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Events.cs +++ b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Events.cs @@ -44,7 +44,11 @@ private async Task ClientOnApplicationMessageReceivedAsync(MqttApplicationMessag else if (topic.EndsWith("cmd/homeassistant")) { // commands coming from home assistant - if (int.TryParse(topic.Split('/')[3], out var result)) + // find the subdevice id relative to the "subdevices" segment, not a fixed index, + // so multi-segment base topics (e.g. "home/ecowitt") don't break parsing + var parts = topic.Split('/'); + var subdevicesIndex = Array.IndexOf(parts, "subdevices"); + if (subdevicesIndex >= 0 && subdevicesIndex + 1 < parts.Length && int.TryParse(parts[subdevicesIndex + 1], out var result)) { var cmd = payload.Equals("ON", StringComparison.InvariantCultureIgnoreCase) ? Command.Start : Command.Stop; //I know, everything that's not "ON" is "OFF" await _messageBus.Publish(new SubdeviceApiCommand() { Cmd = cmd, Id = result }); diff --git a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Publisher.cs b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Publisher.cs index 8d8c819..d130ab4 100644 --- a/src/Ecowitt.Controller/Service/Mqtt/MqttService.Publisher.cs +++ b/src/Ecowitt.Controller/Service/Mqtt/MqttService.Publisher.cs @@ -76,9 +76,9 @@ private async Task Publish(string topic, string message, bool retain = fal _logger.LogDebug("Message {message} ", message); return true; } - else _logger.LogWarning("Failed to publish message {Message} to topic {Topic}. Reason: {MqttClientPublishReasonCode}", message, topic, result.ReasonCode); + else _logger.LogWarning("Failed to publish message to topic {Topic}. Reason: {MqttClientPublishReasonCode}", topic, result.ReasonCode); } - else _logger.LogWarning("Can't publish message {Message} to topic {Topic}. Client not connected.", message, topic); + else _logger.LogWarning("Can't publish message to topic {Topic}. Client not connected.", topic); return false; } }