684 lines
27 KiB
Zig
684 lines
27 KiB
Zig
const builtin = @import("builtin");
|
|
const std = @import("std");
|
|
const pos = @import("pos");
|
|
|
|
fn loadReplacements(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) {
|
|
var replacements = std.StringHashMap([]const u8).init(allocator);
|
|
|
|
// Try current directory first
|
|
if (std.fs.cwd().openFile("replacements.json", .{})) |file| {
|
|
defer file.close();
|
|
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
|
|
defer allocator.free(content);
|
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
defer parsed.deinit();
|
|
const obj = parsed.value.object;
|
|
var it = obj.iterator();
|
|
while (it.next()) |entry| {
|
|
try replacements.put(try allocator.dupe(u8, entry.key_ptr.*), try allocator.dupe(u8, entry.value_ptr.*.string));
|
|
}
|
|
return replacements;
|
|
} else |_| {}
|
|
|
|
// Try XDG_CONFIG_HOME or HOME/.config
|
|
const config_dir = std.posix.getenv("XDG_CONFIG_HOME") orelse blk: {
|
|
const home = std.posix.getenv("HOME") orelse {
|
|
std.log.warn("No replacement configuration file found", .{});
|
|
return replacements;
|
|
};
|
|
break :blk try std.fs.path.join(allocator, &.{ home, ".config" });
|
|
};
|
|
const config_path = if (std.posix.getenv("XDG_CONFIG_HOME") != null)
|
|
try std.fs.path.join(allocator, &.{ config_dir, "pos", "replacements.json" })
|
|
else blk: {
|
|
defer allocator.free(config_dir);
|
|
break :blk try std.fs.path.join(allocator, &.{ config_dir, "pos", "replacements.json" });
|
|
};
|
|
defer allocator.free(config_path);
|
|
|
|
if (std.fs.openFileAbsolute(config_path, .{})) |file| {
|
|
defer file.close();
|
|
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
|
|
defer allocator.free(content);
|
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
defer parsed.deinit();
|
|
const obj = parsed.value.object;
|
|
var it = obj.iterator();
|
|
while (it.next()) |entry| {
|
|
try replacements.put(try allocator.dupe(u8, entry.key_ptr.*), try allocator.dupe(u8, entry.value_ptr.*.string));
|
|
}
|
|
return replacements;
|
|
} else |_| {}
|
|
|
|
std.log.warn("No replacement configuration file found", .{});
|
|
return replacements;
|
|
}
|
|
|
|
const DeviceAction = enum {
|
|
on,
|
|
off,
|
|
toggle,
|
|
};
|
|
|
|
fn sendWemoCommand(allocator: std.mem.Allocator, device_entry: std.hash_map.StringHashMap([]const u8).Entry, action: DeviceAction) !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", .{device_entry.value_ptr.*});
|
|
defer allocator.free(url);
|
|
|
|
var client = std.http.Client{ .allocator = allocator };
|
|
defer client.deinit();
|
|
|
|
std.log.debug("sending WeMo {s} command to '{s}' at {s}\n", .{
|
|
@tagName(action),
|
|
device_entry.key_ptr.*,
|
|
device_entry.value_ptr.*,
|
|
});
|
|
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 {s} command sent to '{s}' at {s}\n", .{
|
|
@tagName(action),
|
|
device_entry.key_ptr.*,
|
|
device_entry.value_ptr.*,
|
|
});
|
|
} else {
|
|
var stderr_writer = std.fs.File.stderr().writer(&.{});
|
|
const stderr = &stderr_writer.interface;
|
|
try stderr.print("ERROR: Could not send WeMo {s} command to '{s}' at {s}\n", .{
|
|
@tagName(action),
|
|
device_entry.key_ptr.*,
|
|
device_entry.value_ptr.*,
|
|
});
|
|
}
|
|
}
|
|
|
|
fn loadDeviceConfig(allocator: std.mem.Allocator, bin_dir: []const u8) !std.StringHashMap([]const u8) {
|
|
// Try current directory first
|
|
if (loadConfigFromPath(allocator, "controlData.json")) |config| {
|
|
return config;
|
|
} else |_| {}
|
|
|
|
// Try controlData.json in bin directory
|
|
const json_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_dir, "controlData.json" });
|
|
defer allocator.free(json_path);
|
|
if (loadConfigFromPath(allocator, json_path)) |config| {
|
|
return config;
|
|
} else |_| {}
|
|
|
|
// Try ../share/pos relative to bin directory
|
|
const share_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_dir, "../share/pos/controlData.json" });
|
|
defer allocator.free(share_path);
|
|
if (loadConfigFromPath(allocator, share_path)) |config| {
|
|
return config;
|
|
} else |_| {}
|
|
|
|
return loadDevicesFromTxt(allocator, bin_dir);
|
|
}
|
|
|
|
fn loadConfigFromPath(allocator: std.mem.Allocator, path: []const u8) !std.StringHashMap([]const u8) {
|
|
var devices = std.StringHashMap([]const u8).init(allocator);
|
|
|
|
const file = if (std.fs.path.isAbsolute(path))
|
|
std.fs.openFileAbsolute(path, .{}) catch return error.FileNotFound
|
|
else
|
|
std.fs.cwd().openFile(path, .{}) catch return error.FileNotFound;
|
|
defer file.close();
|
|
|
|
const content = try file.readToEndAlloc(allocator, 1024 * 1024);
|
|
defer allocator.free(content);
|
|
|
|
var stderr_writer = std.fs.File.stderr().writer(&.{});
|
|
const stderr = &stderr_writer.interface;
|
|
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 error.ParseError;
|
|
};
|
|
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}", .{ name, url });
|
|
}
|
|
}
|
|
|
|
return devices;
|
|
}
|
|
|
|
fn loadDevicesFromTxt(allocator: std.mem.Allocator, bin_dir: []const u8) !std.StringHashMap([]const u8) {
|
|
var devices = std.StringHashMap([]const u8).init(allocator);
|
|
|
|
const txt_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_dir, "devices.txt" });
|
|
defer allocator.free(txt_path);
|
|
|
|
const file = std.fs.openFileAbsolute(txt_path, .{}) 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), replacements: *std.StringHashMap([]const u8)) !?std.hash_map.StringHashMap([]const u8).Entry {
|
|
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;
|
|
|
|
// Try original words first
|
|
for (0..object_words.len) |removed_words| {
|
|
defer allocating_writer.clearRetainingCapacity();
|
|
const total_words = object_words.len - removed_words;
|
|
for (object_words, 1..) |word, i| {
|
|
if (i >= total_words + 1) break;
|
|
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 - removed_words) " " else "",
|
|
});
|
|
}
|
|
|
|
std.log.debug("Attempting device match '{s}'", .{allocating_writer.written()});
|
|
if (devices.getEntry(allocating_writer.written())) |entry| return entry;
|
|
}
|
|
|
|
// Try with word replacements
|
|
for (0..object_words.len) |removed_words| {
|
|
defer allocating_writer.clearRetainingCapacity();
|
|
const total_words = object_words.len - removed_words;
|
|
for (object_words, 1..) |word, i| {
|
|
if (i >= total_words + 1) break;
|
|
var buf: [256]u8 = undefined;
|
|
const lower_string = std.ascii.lowerString(&buf, word);
|
|
const final_word = replacements.get(lower_string) orelse lower_string;
|
|
try writer.print("{s}{s}", .{
|
|
final_word,
|
|
if (i < object_words.len - removed_words) " " else "",
|
|
});
|
|
}
|
|
|
|
std.log.debug("Attempting device match with replacements '{s}'", .{allocating_writer.written()});
|
|
if (devices.getEntry(allocating_writer.written())) |entry| return entry;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
pub fn main() !u8 {
|
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
|
defer _ = gpa.deinit();
|
|
const allocator = gpa.allocator();
|
|
|
|
const args = try std.process.argsAlloc(allocator);
|
|
defer std.process.argsFree(allocator, args);
|
|
|
|
// Check for --sentence-parse-only option
|
|
var sentence_parse_only: enum { sentence, command, none } = .none;
|
|
var sentence_arg: ?[]const u8 = null;
|
|
|
|
for (args[1..]) |arg| {
|
|
if (std.mem.eql(u8, arg, "--sentence-parse-only"))
|
|
sentence_parse_only = .sentence
|
|
else if (std.mem.eql(u8, arg, "--command-parse-only"))
|
|
sentence_parse_only = .command
|
|
else if (sentence_arg == null)
|
|
sentence_arg = arg;
|
|
}
|
|
|
|
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 (sentence_arg == null) {
|
|
try stderr.print("Usage: {s} [--sentence-parse-only] [--command-parse-only] <sentence>\n", .{args[0]});
|
|
return 1;
|
|
}
|
|
if (sentence_parse_only == .none and std.mem.count(u8, sentence_arg.?, " ") == 0) {
|
|
// a command cannot be made from a single word
|
|
try stdout.print("Ignoring attempt at single word command\n", .{});
|
|
return 0;
|
|
}
|
|
const bin_dir = std.fs.selfExeDirPathAlloc(allocator) catch |err| {
|
|
stderr.print("Failed to get binary path: {}\n", .{err}) catch {};
|
|
return 1;
|
|
};
|
|
defer allocator.free(bin_dir);
|
|
|
|
std.log.debug("initializing parser", .{});
|
|
var parser = pos.Parser.initWithDataDir(allocator, bin_dir) catch |err| {
|
|
std.debug.print("Failed to initialize parser: {}\n", .{err});
|
|
return 1;
|
|
};
|
|
defer parser.deinit();
|
|
if (sentence_parse_only != .none) {
|
|
const sentence_z = try allocator.dupeZ(u8, sentence_arg.?);
|
|
defer allocator.free(sentence_z);
|
|
|
|
var tree = if (sentence_parse_only == .command) blk: {
|
|
var replacements = try loadReplacements(allocator);
|
|
defer {
|
|
var iterator = replacements.iterator();
|
|
while (iterator.next()) |entry| {
|
|
allocator.free(entry.key_ptr.*);
|
|
allocator.free(entry.value_ptr.*);
|
|
}
|
|
replacements.deinit();
|
|
}
|
|
break :blk parser.adaptiveCommandParse(sentence_z, &replacements) catch {
|
|
std.log.err("Failed to parse sentence: {s}", .{sentence_z});
|
|
return 1;
|
|
};
|
|
} else parser.parse(sentence_z) catch {
|
|
std.log.err("Failed to parse sentence: {s}", .{sentence_z});
|
|
return 1;
|
|
};
|
|
defer tree.deinit();
|
|
|
|
try stdout.print("{f}", .{tree});
|
|
} else {
|
|
std.log.debug("loading device config", .{});
|
|
|
|
var devices = loadDeviceConfig(allocator, bin_dir) catch |err| {
|
|
stderr.print("Failed to load device configuration: {}\n", .{err}) catch {};
|
|
return 1;
|
|
};
|
|
defer {
|
|
var iterator = devices.iterator();
|
|
while (iterator.next()) |entry| {
|
|
allocator.free(entry.key_ptr.*);
|
|
allocator.free(entry.value_ptr.*);
|
|
}
|
|
devices.deinit();
|
|
}
|
|
|
|
var replacements = try loadReplacements(allocator);
|
|
defer {
|
|
var iterator = replacements.iterator();
|
|
while (iterator.next()) |entry| {
|
|
allocator.free(entry.key_ptr.*);
|
|
allocator.free(entry.value_ptr.*);
|
|
}
|
|
replacements.deinit();
|
|
}
|
|
|
|
processCommand(allocator, args[1], &parser, &devices, &replacements) catch |err| {
|
|
switch (err) {
|
|
error.UnrecognizedSentence => {
|
|
try stderr.print("Unrecognized sentence: {s}\n", .{args[1]});
|
|
return 1;
|
|
},
|
|
else => return err,
|
|
}
|
|
};
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
fn processCommand(allocator: std.mem.Allocator, sentence: [:0]const u8, parser: *pos.Parser, devices: *std.StringHashMap([]const u8), replacements: *std.StringHashMap([]const u8)) !void {
|
|
var tree = parser.adaptiveCommandParse(sentence, replacements) catch |err| {
|
|
std.log.err("Failed to parse sentence: {}\n", .{err});
|
|
return error.UnrecognizedSentence;
|
|
};
|
|
defer tree.deinit();
|
|
|
|
const action_words = try tree.sentenceAction();
|
|
defer allocator.free(action_words);
|
|
|
|
const object_words = try tree.sentenceObject();
|
|
defer allocator.free(object_words);
|
|
|
|
std.log.debug("{f}", .{tree});
|
|
var aw = std.Io.Writer.Allocating.init(allocator);
|
|
defer aw.deinit();
|
|
const aw_writer = &aw.writer;
|
|
try aw_writer.writeAll("Object words: [");
|
|
var first = true;
|
|
for (object_words) |word| {
|
|
try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word });
|
|
first = false;
|
|
}
|
|
std.log.debug("{s}]", .{aw.written()});
|
|
|
|
aw.clearRetainingCapacity();
|
|
try aw_writer.writeAll("Action words: [");
|
|
first = true;
|
|
for (action_words) |word| {
|
|
try aw_writer.print("{s}\"{s}\"", .{ if (!first) ", " else "", word });
|
|
first = false;
|
|
}
|
|
std.log.debug("{s}]", .{aw.written()});
|
|
|
|
if (parseAction(action_words)) |action| {
|
|
if (try extractDevice(allocator, object_words, devices, replacements)) |entry| {
|
|
if (builtin.is_test)
|
|
testSendCommand(entry, action)
|
|
else
|
|
try sendWemoCommand(allocator, entry, action);
|
|
return; // Success
|
|
}
|
|
}
|
|
|
|
return error.UnrecognizedSentence;
|
|
}
|
|
|
|
// SAFETY: only used for testing, and in each case this is set before checking results
|
|
var test_device_entry: std.hash_map.StringHashMap([]const u8).Entry = undefined;
|
|
// SAFETY: only used for testing, and in each case this is set before checking results
|
|
var test_action: DeviceAction = undefined;
|
|
fn testSendCommand(device_entry: std.hash_map.StringHashMap([]const u8).Entry, action: DeviceAction) void {
|
|
test_device_entry = device_entry;
|
|
test_action = action;
|
|
}
|
|
test "extractDevice single word exact match" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
try devices.put("bedroom", "192.168.1.101");
|
|
|
|
var object_words = [_][]const u8{"kitchen"};
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
|
|
test "extractDevice single word case insensitive" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{"Kitchen"};
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
|
|
test "extractDevice two words exact match" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("bedroom light", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{ "bedroom", "light" };
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
|
|
test "extractDevice two words case insensitive" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("bedroom light", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{ "Bedroom", "Light" };
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
|
|
test "extractDevice not found" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{"bathroom"};
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expect(result == null);
|
|
}
|
|
|
|
test "extractDevice empty words" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
var object_words = [_][]const u8{};
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expect(result == null);
|
|
}
|
|
|
|
test "extractDevice fallback to shorter match" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{ "kitchen", "ceiling", "light" };
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
test "extractDevice word replacement lake to light" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
try replacements.put("lake", "light");
|
|
|
|
try devices.put("bedroom light", "192.168.1.100");
|
|
|
|
var object_words = [_][]const u8{ "bedroom", "lake" };
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", result.?.value_ptr.*);
|
|
}
|
|
|
|
test "extractDevice word replacement like to light" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
try replacements.put("like", "light");
|
|
|
|
try devices.put("kitchen light", "192.168.1.101");
|
|
|
|
var object_words = [_][]const u8{ "kitchen", "like" };
|
|
const result = try extractDevice(std.testing.allocator, object_words[0..], &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.101", result.?.value_ptr.*);
|
|
}
|
|
test "processCommand successful match" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
|
|
var parser = try pos.Parser.init(std.testing.allocator);
|
|
defer parser.deinit();
|
|
|
|
const sentence = "turn on the kitchen";
|
|
const sentence_z = try std.testing.allocator.dupeZ(u8, sentence);
|
|
defer std.testing.allocator.free(sentence_z);
|
|
|
|
try processCommand(std.testing.allocator, sentence_z, &parser, &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", test_device_entry.value_ptr.*);
|
|
try std.testing.expectEqual(DeviceAction.on, test_action);
|
|
}
|
|
|
|
test "processCommand with word replacement lake to light" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
try replacements.put("lake", "light");
|
|
|
|
try devices.put("kitchen light", "192.168.1.100");
|
|
|
|
var parser = try pos.Parser.init(std.testing.allocator);
|
|
defer parser.deinit();
|
|
|
|
const sentence = "turn on the kitchen lake";
|
|
const sentence_z = try std.testing.allocator.dupeZ(u8, sentence);
|
|
defer std.testing.allocator.free(sentence_z);
|
|
|
|
try processCommand(std.testing.allocator, sentence_z, &parser, &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.100", test_device_entry.value_ptr.*);
|
|
try std.testing.expectEqual(DeviceAction.on, test_action);
|
|
}
|
|
|
|
test "processCommand with word replacement like to light" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
try replacements.put("like", "light");
|
|
|
|
try devices.put("bedroom light", "192.168.1.101");
|
|
|
|
var parser = try pos.Parser.init(std.testing.allocator);
|
|
defer parser.deinit();
|
|
|
|
const sentence = "turn off the bedroom like";
|
|
const sentence_z = try std.testing.allocator.dupeZ(u8, sentence);
|
|
defer std.testing.allocator.free(sentence_z);
|
|
|
|
try processCommand(std.testing.allocator, sentence_z, &parser, &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.101", test_device_entry.value_ptr.*);
|
|
try std.testing.expectEqual(DeviceAction.off, test_action);
|
|
}
|
|
test "processCommand with word replacement like to light - three words" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
try replacements.put("like", "light");
|
|
|
|
try devices.put("jack bedroom light", "192.168.1.101");
|
|
|
|
var parser = try pos.Parser.init(std.testing.allocator);
|
|
defer parser.deinit();
|
|
|
|
const sentence = "turn off jack bedroom like";
|
|
const sentence_z = try std.testing.allocator.dupeZ(u8, sentence);
|
|
defer std.testing.allocator.free(sentence_z);
|
|
|
|
try processCommand(std.testing.allocator, sentence_z, &parser, &devices, &replacements);
|
|
|
|
try std.testing.expectEqualStrings("192.168.1.101", test_device_entry.value_ptr.*);
|
|
try std.testing.expectEqual(DeviceAction.off, test_action);
|
|
}
|
|
|
|
test "processCommand no match found" {
|
|
var devices = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer devices.deinit();
|
|
var replacements = std.StringHashMap([]const u8).init(std.testing.allocator);
|
|
defer replacements.deinit();
|
|
|
|
try devices.put("kitchen", "192.168.1.100");
|
|
|
|
var parser = try pos.Parser.init(std.testing.allocator);
|
|
defer parser.deinit();
|
|
|
|
const sentence = "turn on the bathroom";
|
|
const sentence_z = try std.testing.allocator.dupeZ(u8, sentence);
|
|
defer std.testing.allocator.free(sentence_z);
|
|
|
|
try std.testing.expectError(error.UnrecognizedSentence, processCommand(std.testing.allocator, sentence_z, &parser, &devices, &replacements));
|
|
}
|