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