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]});
+ }
}