const std = @import("std"); const pos = @import("pos"); const DeviceAction = enum { on, off, toggle, }; fn sendWemoCommand(url_base: []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, "{s}/upnp/control/basicevent1", .{url_base}); defer allocator.free(url); var client = std.http.Client{ .allocator = allocator }; defer client.deinit(); std.log.debug("sending wemo command to {s}, action={s}", .{ url_base, @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: url={s}, Action={s}\n", .{ url_base, @tagName(action) }); } else { var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; try stderr.print("WeMo command sent: url={s}, Action={s}\n", .{ url_base, @tagName(action) }); } } fn loadDeviceConfig(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) { var devices = std.StringHashMap([]const u8).init(allocator); var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; // Try controlData.json first if (std.fs.cwd().openFile("controlData.json", .{})) |file| { defer file.close(); const content = try file.readToEndAlloc(allocator, 1024 * 1024); defer allocator.free(content); const parsed = std.json.parseFromSlice(std.json.Value, allocator, content, .{}) catch |err| { try stderr.print( "Failed to parse controlData.json: {}. Ignoring controlData.json, looking for devices.txt", .{err}, ); return loadDevicesFromTxt(allocator); }; defer parsed.deinit(); const root = parsed.value.object; const device_array = root.get("devices").?.array; for (device_array.items) |device| { const device_obj = device.object; const name = device_obj.get("name").?.string; const url = device_obj.get("url").?.string; if (name.len > 0) { const name_copy = try allocator.alloc(u8, name.len); _ = std.ascii.lowerString(name_copy, name); try devices.put(name_copy, try allocator.dupe(u8, url)); std.log.debug("Loaded device: '{s}' -> '{s}'\n", .{ name, url }); } } return devices; } else |_| { return loadDevicesFromTxt(allocator); } } fn loadDevicesFromTxt(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) { var devices = std.StringHashMap([]const u8).init(allocator); const file = std.fs.cwd().openFile("devices.txt", .{}) catch |err| switch (err) { error.FileNotFound => { var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; try stderr.print( \\Error: could not load configuration. Please make sure controlData.json or \\devices.txt is available in the directory with this program , .{}); return error.ConfigurationNotAvailable; }, else => return err, }; 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 url_base = std.mem.trim(u8, trimmed[eq_pos + 1 ..], " \t"); if (name.len > 0 and url_base.len > 0) { const name_copy = try allocator.alloc(u8, name.len); _ = std.ascii.lowerString(name_copy, name); try devices.put(name, try allocator.dupe(u8, url_base)); } } } 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; var total_size: usize = 0; for (object_words) |word| total_size += word.len + 1; var allocating_writer = try std.Io.Writer.Allocating.initCapacity(allocator, total_size); defer allocating_writer.deinit(); const writer = &allocating_writer.writer; for (object_words, 1..) |word, i| { var buf: [256]u8 = undefined; const lower_string = std.ascii.lowerString(&buf, word); try writer.print("{s}{s}", .{ lower_string, if (i < object_words.len) " " else "" }); } if (devices.get(allocating_writer.written())) |url_base| return url_base; std.log.warn("Could not find device '{s}'", .{allocating_writer.written()}); return null; } pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = 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 = parser.parse(args[1]) catch |err| { stderr.print("Failed to parse sentence: {}\n", .{err}) catch {}; std.process.exit(1); }; defer tree.deinit(); const action_words = tree.sentenceAction() catch |err| { stderr.print("Failed to extract action: {}\n", .{err}) catch {}; std.process.exit(1); }; 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); std.debug.print("Object words: ", .{}); for (object_words) |word| { std.debug.print("'{s}' ", .{word}); } std.debug.print("\n", .{}); if (parseAction(action_words)) |action| { if (try extractDevice(allocator, object_words, &devices)) |url_base| { try sendWemoCommand(url_base, action, gpa.allocator()); } else { try stdout.print("Device not found in configuration\n", .{}); } } else { try stdout.print("Unrecognized sentence: {s}\n", .{args[1]}); } }