165 lines
7 KiB
Zig
165 lines
7 KiB
Zig
const std = @import("std");
|
|
const json = std.json;
|
|
const lambda = @import("lambda_runtime");
|
|
const rinnai = @import("rinnai");
|
|
|
|
const log = std.log.scoped(.alexa);
|
|
|
|
pub fn main() !u8 {
|
|
lambda.run(null, handler) catch |err| {
|
|
log.err("Lambda runtime error: {}", .{err});
|
|
return 1;
|
|
};
|
|
return 0;
|
|
}
|
|
|
|
/// Main Alexa request handler
|
|
fn handler(allocator: std.mem.Allocator, event_data: []const u8) anyerror![]const u8 {
|
|
log.info("Received Alexa request: {d} bytes", .{event_data.len});
|
|
|
|
// Parse the Alexa request
|
|
const parsed = json.parseFromSlice(json.Value, allocator, event_data, .{}) catch |err| {
|
|
log.err("Failed to parse Alexa request: {}", .{err});
|
|
return buildAlexaResponse(allocator, "I couldn't understand that request.", true);
|
|
};
|
|
defer parsed.deinit();
|
|
|
|
// Get request type
|
|
const request_obj = parsed.value.object.get("request") orelse {
|
|
log.err("No 'request' field in Alexa event", .{});
|
|
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
|
};
|
|
|
|
const request_type = request_obj.object.get("type") orelse {
|
|
log.err("No 'type' field in request", .{});
|
|
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
|
};
|
|
|
|
const request_type_str = if (request_type == .string) request_type.string else {
|
|
log.err("Request type is not a string", .{});
|
|
return buildAlexaResponse(allocator, "Invalid request format.", true);
|
|
};
|
|
|
|
log.info("Request type: {s}", .{request_type_str});
|
|
|
|
// Handle different request types
|
|
if (std.mem.eql(u8, request_type_str, "LaunchRequest")) {
|
|
return buildAlexaResponse(allocator, "What would you like me to do? You can ask me to start the hot water.", false);
|
|
} else if (std.mem.eql(u8, request_type_str, "IntentRequest")) {
|
|
return handleIntentRequest(allocator, request_obj);
|
|
} else if (std.mem.eql(u8, request_type_str, "SessionEndedRequest")) {
|
|
return buildAlexaResponse(allocator, "", true);
|
|
}
|
|
|
|
return buildAlexaResponse(allocator, "I didn't understand that.", true);
|
|
}
|
|
|
|
/// Handle Alexa intent requests
|
|
fn handleIntentRequest(allocator: std.mem.Allocator, request_obj: json.Value) ![]const u8 {
|
|
const intent_obj = request_obj.object.get("intent") orelse {
|
|
log.err("No 'intent' field in IntentRequest", .{});
|
|
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
|
};
|
|
|
|
const intent_name_val = intent_obj.object.get("name") orelse {
|
|
log.err("No 'name' field in intent", .{});
|
|
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
|
};
|
|
|
|
const intent_name = if (intent_name_val == .string) intent_name_val.string else {
|
|
log.err("Intent name is not a string", .{});
|
|
return buildAlexaResponse(allocator, "I couldn't understand your intent.", true);
|
|
};
|
|
|
|
log.info("Intent: {s}", .{intent_name});
|
|
|
|
if (std.mem.eql(u8, intent_name, "RecirculateWaterIntent")) {
|
|
return handleRecirculateWater(allocator);
|
|
} else if (std.mem.eql(u8, intent_name, "AMAZON.HelpIntent")) {
|
|
return buildAlexaResponse(allocator, "You can ask me to start the hot water to begin recirculation. This will preheat your water for about 15 minutes.", false);
|
|
} else if (std.mem.eql(u8, intent_name, "AMAZON.StopIntent") or std.mem.eql(u8, intent_name, "AMAZON.CancelIntent")) {
|
|
return buildAlexaResponse(allocator, "Okay, goodbye.", true);
|
|
}
|
|
|
|
return buildAlexaResponse(allocator, "I don't know how to do that.", true);
|
|
}
|
|
|
|
/// Handle the main recirculate water intent
|
|
fn handleRecirculateWater(allocator: std.mem.Allocator) ![]const u8 {
|
|
// Get credentials from environment variables
|
|
const username = std.posix.getenv("COGNITO_USERNAME") orelse {
|
|
log.err("COGNITO_USERNAME environment variable not set", .{});
|
|
return buildAlexaResponse(allocator, "I'm not configured properly. Please set up my credentials.", true);
|
|
};
|
|
|
|
const password = std.posix.getenv("COGNITO_PASSWORD") orelse {
|
|
log.err("COGNITO_PASSWORD environment variable not set", .{});
|
|
return buildAlexaResponse(allocator, "I'm not configured properly. Please set up my credentials.", true);
|
|
};
|
|
|
|
// Authenticate with Cognito
|
|
log.info("Authenticating with Cognito...", .{});
|
|
var auth = rinnai.authenticate(allocator, username, password) catch |err| {
|
|
log.err("Authentication failed: {}", .{err});
|
|
return buildAlexaResponse(allocator, "I couldn't log in to your water heater account.", true);
|
|
};
|
|
defer auth.deinit();
|
|
log.info("Authenticated successfully", .{});
|
|
|
|
// Get device list
|
|
log.info("Fetching device list...", .{});
|
|
var devices = rinnai.getDevices(allocator, auth.id_token, username) catch |err| {
|
|
log.err("Failed to get devices: {}", .{err});
|
|
return buildAlexaResponse(allocator, "I couldn't find your water heater.", true);
|
|
};
|
|
defer devices.deinit();
|
|
|
|
if (devices.devices.len == 0) {
|
|
return buildAlexaResponse(allocator, "I couldn't find any water heaters on your account.", true);
|
|
}
|
|
|
|
const device = devices.devices[0];
|
|
if (device.thing_name == null) {
|
|
return buildAlexaResponse(allocator, "Your water heater isn't properly configured.", true);
|
|
}
|
|
|
|
// Start recirculation
|
|
log.info("Starting recirculation for device: {?s}", .{device.device_name});
|
|
rinnai.setRecirculation(allocator, auth.id_token, device.thing_name.?, 15) catch |err| {
|
|
log.err("Failed to start recirculation: {}", .{err});
|
|
return buildAlexaResponse(allocator, "I couldn't start the water recirculation. Please try again.", true);
|
|
};
|
|
|
|
return buildAlexaResponse(allocator, "Starting water recirculation. Hot water should be ready in about 2 minutes.", true);
|
|
}
|
|
|
|
/// Build an Alexa skill response JSON
|
|
fn buildAlexaResponse(allocator: std.mem.Allocator, speech: []const u8, end_session: bool) ![]const u8 {
|
|
// Escape speech for JSON
|
|
var escaped_speech: std.ArrayList(u8) = .{};
|
|
defer escaped_speech.deinit(allocator);
|
|
|
|
for (speech) |c| {
|
|
switch (c) {
|
|
'"' => try escaped_speech.appendSlice(allocator, "\\\""),
|
|
'\\' => try escaped_speech.appendSlice(allocator, "\\\\"),
|
|
'\n' => try escaped_speech.appendSlice(allocator, "\\n"),
|
|
'\r' => try escaped_speech.appendSlice(allocator, "\\r"),
|
|
'\t' => try escaped_speech.appendSlice(allocator, "\\t"),
|
|
else => try escaped_speech.append(allocator, c),
|
|
}
|
|
}
|
|
|
|
const end_session_str = if (end_session) "true" else "false";
|
|
|
|
if (speech.len == 0) {
|
|
// Empty response for SessionEndedRequest
|
|
return try std.fmt.allocPrint(allocator,
|
|
\\{{"version":"1.0","response":{{"shouldEndSession":{s}}}}}
|
|
, .{end_session_str});
|
|
}
|
|
|
|
return try std.fmt.allocPrint(allocator,
|
|
\\{{"version":"1.0","response":{{"outputSpeech":{{"type":"PlainText","text":"{s}"}},"shouldEndSession":{s}}}}}
|
|
, .{ escaped_speech.items, end_session_str });
|
|
}
|