如何在C#中构建本地uPnP应用实现Alexa/Echo无AWS控制(Windows服务)
Alright, let's break down exactly how to build this local-only C# uPnP app that mimics WeMo for Alexa control—no AWS Skill required, and packaged as a Windows service. This approach leans into the same local discovery and control mechanisms Belkin uses, so Alexa will treat your app just like a real WeMo device.
WeMo devices rely on two key local network protocols that Alexa understands natively:
- SSDP (Simple Service Discovery Protocol): A UDP-based system where devices announce themselves (and respond to search queries) on the local network.
- SOAP (Simple Object Access Protocol): An XML-based API that Alexa uses to send control commands (like "turn on") and retrieve device state.
Your app needs to implement both to trick Alexa into seeing it as a WeMo device.
Alexa periodically sends M-SEARCH requests over UDP to the multicast address 239.255.255.250 on port 1900, looking for WeMo devices. Your app needs to:
- Listen for these requests and send a properly formatted SSDP response.
- (Optional) Send periodic NOTIFY messages to speed up initial discovery.
C# SSDP Listener Snippet
using System.Net; using System.Net.Sockets; using System.Text; public class SsdpDiscoveryService { private UdpClient _udpListener; private const int SsdpPort = 1900; private const string MulticastAddr = "239.255.255.250"; private readonly string _deviceUuid = $"uuid:{Guid.NewGuid()}"; // Generate a unique ID once and reuse it private readonly string _localIp; public SsdpDiscoveryService() { // Get your local IPv4 address (skip loopback addresses) _localIp = Dns.GetHostEntry(Dns.GetHostName()) .AddressList .First(ip => ip.AddressFamily == AddressFamily.InterNetwork && !IPAddress.IsLoopback(ip)) .ToString(); } public async Task StartAsync(CancellationToken cancellationToken) { _udpListener = new UdpClient(SsdpPort); _udpListener.JoinMulticastGroup(IPAddress.Parse(MulticastAddr)); while (!cancellationToken.IsCancellationRequested) { var receiveResult = await _udpListener.ReceiveAsync(cancellationToken); var request = Encoding.UTF8.GetString(receiveResult.Buffer); // Check if this is an Alexa search for WeMo devices if (request.Contains("M-SEARCH") && request.Contains("urn:Belkin:device:**")) { var response = BuildSsdpResponse(); await _udpListener.SendAsync(Encoding.UTF8.GetBytes(response), response.Length, receiveResult.RemoteEndPoint); } } } private string BuildSsdpResponse() { // The location points to your HTTP service's device description XML var locationUrl = $"http://{_localIp}:8080/setup.xml"; return $"HTTP/1.1 200 OK\r\n" + "CACHE-CONTROL: max-age=86400\r\n" + $"LOCATION: {locationUrl}\r\n" + "SERVER: Unspecified, UPnP/1.0, Unspecified\r\n" + "ST: urn:Belkin:device:**\r\n" + $"USN: {_deviceUuid}::urn:Belkin:device:**\r\n" + "EXT:\r\n" + "\r\n"; } public void Stop() => _udpListener?.Close(); }
Once Alexa discovers your device via SSDP, it will fetch a device description XML from the LOCATION URL you provided, then send SOAP commands to control it. You need:
- A local HTTP server to serve the XML and handle SOAP requests.
- Strict adherence to WeMo's XML/SOAP schema (Alexa is picky about formatting).
1. Serve the Device Description XML (setup.xml)
Create this XML file and serve it from /setup.xml:
<?xml version="1.0"?> <root xmlns="urn:Belkin:device-1-0"> <specVersion> <major>1</major> <minor>0</minor> </specVersion> <device> <deviceType>urn:Belkin:device:controllee:1</deviceType> <friendlyName>My C# Smart Plug</friendlyName> <manufacturer>Belkin International Inc.</manufacturer> <manufacturerURL>http://www.belkin.com</manufacturerURL> <modelDescription>Belkin Plugin Socket 1.0</modelDescription> <modelName>Socket</modelName> <modelNumber>1.0</modelNumber> <modelURL>http://www.belkin.com/plugin/</modelURL> <serialNumber>123456789</serialNumber> <UDN>uuid:your-unique-device-id-here</UDN> <!-- Match the UUID from SSDP --> <UPC>123456789012</UPC> <serviceList> <service> <serviceType>urn:Belkin:service:basicevent:1</serviceType> <serviceId>urn:Belkin:serviceId:basicevent1</serviceId> <controlURL>/upnp/control/basicevent1</controlURL> <eventSubURL>/upnp/event/basicevent1</eventSubURL> <SCPDURL>/eventservice.xml</SCPDURL> </service> </serviceList> </device> </root>
2. Handle SOAP Control Requests
Use ASP.NET Core (great for Windows services) to build the SOAP endpoint. Here's a minimal example:
var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); // Serve the device description XML app.MapGet("/setup.xml", () => Results.File("setup.xml", "text/xml")); // Handle WeMo's basic event control (on/off commands) app.MapPost("/upnp/control/basicevent1", async (HttpContext context) => { using var reader = new StreamReader(context.Request.Body); var soapRequest = await reader.ReadToEndAsync(); // Handle "turn on/off" requests if (soapRequest.Contains("SetBinaryState")) { // Extract the desired state (0 = off, 1 = on) var stateMatch = System.Text.RegularExpressions.Regex.Match(soapRequest, "<BinaryState>(\\d)</BinaryState>"); if (stateMatch.Success) { var targetState = stateMatch.Groups[1].Value; // Add your custom logic here (e.g., control a local app, toggle a virtual switch) Console.WriteLine($"Received command to set state to: {targetState}"); } // Send a success response back to Alexa var soapResponse = @"<?xml version=""1.0"" encoding=""utf-8""?> <s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"" s:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/""> <s:Body> <u:SetBinaryStateResponse xmlns:u=""urn:Belkin:service:basicevent:1""> <BinaryState>1</BinaryState> </u:SetBinaryStateResponse> </s:Body> </s:Envelope>"; context.Response.ContentType = "text/xml"; await context.Response.WriteAsync(soapResponse); } // Handle state check requests else if (soapRequest.Contains("GetBinaryState")) { var currentState = "1"; // Replace with your actual device state var soapResponse = @"<?xml version=""1.0"" encoding=""utf-8""?> <s:Envelope xmlns:s=""http://schemas.xmlsoap.org/soap/envelope/"" s:encodingStyle=""http://schemas.xmlsoap.org/soap/encoding/""> <s:Body> <u:GetBinaryStateResponse xmlns:u=""urn:Belkin:service:basicevent:1""> <BinaryState>" + currentState + @"</BinaryState> </u:GetBinaryStateResponse> </s:Body> </s:Envelope>"; context.Response.ContentType = "text/xml"; await context.Response.WriteAsync(soapResponse); } }); // Run the HTTP server on port 8080 await app.RunAsync("http://*:8080");
Wrap your SSDP and HTTP logic into a Windows service using .NET's BackgroundService (works for .NET Core 3.0+):
Service Implementation
public class WemoEmulatorService : BackgroundService { private readonly SsdpDiscoveryService _ssdpService; private IHost _httpHost; public WemoEmulatorService(SsdpDiscoveryService ssdpService) { _ssdpService = ssdpService; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Start the SSDP listener var ssdpTask = _ssdpService.StartAsync(stoppingToken); // Start the HTTP service var httpBuilder = WebApplication.CreateBuilder(); // Add your HTTP endpoints here (copy the ASP.NET Core logic from Step 2) _httpHost = httpBuilder.Build(); var httpTask = _httpHost.RunAsync(stoppingToken); // Wait for either service to stop await Task.WhenAny(ssdpTask, httpTask); } public override async Task StopAsync(CancellationToken cancellationToken) { _ssdpService.Stop(); await _httpHost.StopAsync(cancellationToken); await base.StopAsync(cancellationToken); } }
Install the Service
- Use the .NET CLI to publish your app as a self-contained executable:
dotnet publish -c Release -r win-x64 --self-contained true - Install the service using the
sccommand (run Command Prompt as Administrator):sc create "Wemo Emulator" binPath="C:\path\to\your\published\WemoEmulator.exe" start=auto - Start the service:
sc start "Wemo Emulator"
- Ensure your Windows service is running and port 8080 is open in your local firewall.
- Open the Alexa app on your phone: Go to Devices > + > Add Device > Plug > WeMo > Continue. Alexa should discover your C# device.
- Test voice commands like "Alexa, turn on My C# Smart Plug"—your service should receive and process the request.
- Discovery Failures: Use Wireshark to capture UDP traffic on port 1900. Verify your SSDP responses are being sent and received by Alexa.
- SOAP Errors: Enable logging for your HTTP service to see the exact requests Alexa sends. Double-check XML namespaces and response formatting—Alexa rejects malformed SOAP.
- Permission Issues: If the service can't listen on ports, make sure it's running under an account with network permissions (LocalService usually works, but you may need to adjust firewall rules).
内容的提问来源于stack exchange,提问作者Paul

