From aad54b0b938201124c1efb8dc5f3e0c8c159514c Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Sat, 20 Sep 2025 13:02:00 -0700 Subject: [PATCH] working wemo implementation --- src/main.zig | 186 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 22 deletions(-) diff --git a/src/main.zig b/src/main.zig index 002a14b..2cff5a0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,35 +1,177 @@ const std = @import("std"); const pos = @import("pos"); +const DeviceAction = enum { + on, + off, + toggle, +}; + +fn sendWemoCommand(ip: []const u8, action: DeviceAction, allocator: std.mem.Allocator) !void { + const state = switch (action) { + .on => "1", + .off => "0", + .toggle => return error.ToggleNotImplemented, + }; + // port 49152 or 49153 + // curl -0 -A '' -X POST -H 'Accept: ' -H 'Content-type: text/xml; charset="utf-8"' -H "SOAPACTION: \"urn:Belkin:service:basicevent:1#SetBinaryState\"" --data '1' -s http://$IP:$PORT/upnp/control/basicevent1 | + const soap_body = try std.fmt.allocPrint(allocator, + \\ + \\ + \\ + \\ + \\{s} + \\ + \\ + \\ + , .{state}); + defer allocator.free(soap_body); + + const url = try std.fmt.allocPrint(allocator, "http://{s}/upnp/control/basicevent1", .{ip}); + defer allocator.free(url); + + var client = std.http.Client{ .allocator = allocator }; + defer client.deinit(); + + std.log.debug("sending wemo command to {s}, action={s}", .{ ip, @tagName(action) }); + const res = try client.fetch(.{ + .method = .POST, + .payload = soap_body, + .location = .{ .url = url }, + .headers = .{ .content_type = .{ .override = "text/html; charset=\"utf-8\"" } }, + .extra_headers = &.{ + .{ .name = "SOAPACTION", .value = "urn:Belkin:service:basicevent:1#SetBinaryState" }, + }, + }); + + if (res.status == .ok) { + var stdout_writer = std.fs.File.stdout().writer(&.{}); + const stdout = &stdout_writer.interface; + try stdout.print("WeMo command sent: IP={s}, Action={s}\n", .{ ip, @tagName(action) }); + } else { + var stderr_writer = std.fs.File.stderr().writer(&.{}); + const stderr = &stderr_writer.interface; + try stderr.print("WeMo command sent: IP={s}, Action={s}\n", .{ ip, @tagName(action) }); + } +} + +fn loadDeviceConfig(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) { + var devices = std.StringHashMap([]const u8).init(allocator); + + const file = try std.fs.cwd().openFile("devices.txt", .{}); + defer file.close(); + + const content = try file.readToEndAlloc(allocator, 1024 * 1024); + defer allocator.free(content); + + var lines = std.mem.splitScalar(u8, content, '\n'); + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r\n"); + if (trimmed.len == 0 or trimmed[0] == '#') continue; + + if (std.mem.indexOf(u8, trimmed, "=")) |eq_pos| { + const name = std.mem.trim(u8, trimmed[0..eq_pos], " \t"); + const ip = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t"); + if (name.len > 0 and ip.len > 0) { + try devices.put(try allocator.dupe(u8, name), try allocator.dupe(u8, ip)); + } + } + } + + return devices; +} + +fn parseAction(action_words: [][]const u8) ?DeviceAction { + if (action_words.len >= 2) { + if (std.mem.eql(u8, action_words[0], "turn") and std.mem.eql(u8, action_words[1], "on")) { + return .on; + } else if (std.mem.eql(u8, action_words[0], "turn") and std.mem.eql(u8, action_words[1], "off")) { + return .off; + } + } + return null; +} + +fn extractDevice(allocator: std.mem.Allocator, object_words: [][]const u8, devices: *std.StringHashMap([]const u8)) ?[]const u8 { + if (object_words.len == 0) return null; + + if (object_words.len == 1) { + return devices.get(object_words[0]); + } else if (object_words.len == 2) { + var device_name = std.ArrayList(u8){}; + defer device_name.deinit(allocator); + device_name.appendSlice(allocator, object_words[0]) catch return null; + device_name.append(allocator, ' ') catch return null; + device_name.appendSlice(allocator, object_words[1]) catch return null; + return devices.get(device_name.items); + } + return null; +} + pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); + const allocator = gpa.allocator(); - var parser = try pos.Parser.init(gpa.allocator()); + const args = try std.process.argsAlloc(gpa.allocator()); + defer std.process.argsFree(gpa.allocator(), args); + + var stdout_writer = std.fs.File.stdout().writer(&.{}); + const stdout = &stdout_writer.interface; + var stderr_writer = std.fs.File.stderr().writer(&.{}); + const stderr = &stderr_writer.interface; + + if (args.len < 2) { + stderr.print("Usage: {s} \n", .{args[0]}) catch {}; + std.process.exit(1); + } + + std.log.debug("loading device config", .{}); + var devices = loadDeviceConfig(gpa.allocator()) catch |err| { + stderr.print("Failed to load device configuration: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + defer { + var iterator = devices.iterator(); + while (iterator.next()) |entry| { + gpa.allocator().free(entry.key_ptr.*); + gpa.allocator().free(entry.value_ptr.*); + } + devices.deinit(); + } + + std.log.debug("initializing parser", .{}); + var parser = pos.Parser.init(gpa.allocator()) catch |err| { + stderr.print("Failed to initialize parser: {}\n", .{err}) catch {}; + std.process.exit(1); + }; defer parser.deinit(); - var tree = try parser.parse(pos.sentence); + var tree = parser.parse(args[1]) catch |err| { + stderr.print("Failed to parse sentence: {}\n", .{err}) catch {}; + std.process.exit(1); + }; defer tree.deinit(); - std.debug.print("Parsed sentence: {s}\n", .{pos.sentence}); - std.debug.print("Words: {d}, Links: {d}\n", .{ tree.words.len, tree.links.len }); -} - -test "simple test" { - const gpa = std.testing.allocator; - var list: std.ArrayList(i32) = .empty; - defer list.deinit(gpa); // Try commenting this out and see if zig detects the memory leak! - try list.append(gpa, 42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); -} - -test "fuzz example" { - const Context = struct { - fn testOne(context: @This(), input: []const u8) anyerror!void { - _ = context; - // Try passing `--fuzz` to `zig build test` and see if it manages to fail this test case! - try std.testing.expect(!std.mem.eql(u8, "canyoufindme", input)); - } + const action_words = tree.sentenceAction() catch |err| { + stderr.print("Failed to extract action: {}\n", .{err}) catch {}; + std.process.exit(1); }; - try std.testing.fuzz(Context{}, Context.testOne, .{}); + defer gpa.allocator().free(action_words); + + const object_words = tree.sentenceObject() catch |err| { + stderr.print("Failed to extract object: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + defer gpa.allocator().free(object_words); + + if (parseAction(action_words)) |action| { + if (extractDevice(allocator, object_words, &devices)) |ip| { + try sendWemoCommand(ip, action, gpa.allocator()); + } else { + try stdout.print("Device not found in configuration\n", .{}); + } + } else { + try stdout.print("Unrecognized sentence: {s}\n", .{args[1]}); + } }