pos/src/main.zig

243 lines
9.3 KiB
Zig

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 '<?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:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1"><BinaryState>1</BinaryState></u:SetBinaryState></s:Body></s:Envelope>' -s http://$IP:$PORT/upnp/control/basicevent1 |
const soap_body = try std.fmt.allocPrint(allocator,
\\<?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:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">
\\<BinaryState>{s}</BinaryState>
\\</u:SetBinaryState>
\\</s:Body>
\\</s:Envelope>
, .{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} <sentence>\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]});
}
}