first working thingy
This commit is contained in:
parent
0c56542913
commit
363efa1cca
14 changed files with 10038 additions and 0 deletions
32
.forgejo/workflows/build.yaml
Normal file
32
.forgejo/workflows/build.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
name: Alexa skill build
|
||||
run-name: ${{ github.actor }} building alexa skill
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: https://codeberg.org/mlugg/setup-zig@v2.2.1
|
||||
|
||||
- name: Build
|
||||
run: zig build --summary all
|
||||
|
||||
- name: Run tests
|
||||
run: zig build test --summary all
|
||||
|
||||
- name: Notify
|
||||
uses: https://git.lerch.org/lobo/action-notify-ntfy@v2
|
||||
if: always()
|
||||
with:
|
||||
host: ${{ secrets.NTFY_HOST }}
|
||||
topic: ${{ secrets.NTFY_TOPIC }}
|
||||
user: ${{ secrets.NTFY_USER }}
|
||||
password: ${{ secrets.NTFY_PASSWORD }}
|
||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Zig build artifacts
|
||||
zig-out/
|
||||
.zig-cache/
|
||||
|
||||
# Lambda deployment package
|
||||
lambda.zip
|
||||
function.zip
|
||||
|
||||
# Editor/IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
# Credentials (never commit)
|
||||
.credentials
|
||||
.env
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# ASK CLI state (account-specific)
|
||||
.ask/
|
||||
6
.mise.toml
Normal file
6
.mise.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[tools]
|
||||
zig = "0.15.2"
|
||||
bun = "1.3.8"
|
||||
pre-commit = "4.2.0"
|
||||
"ubi:DonIsaac/zlint" = "0.7.6"
|
||||
zls = "0.15.1"
|
||||
36
.pre-commit-config.yaml
Normal file
36
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.2.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- repo: https://github.com/batmac/pre-commit-zig
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: zig-fmt
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: zlint
|
||||
name: Run zlint
|
||||
entry: zlint
|
||||
args: ["--deny-warnings", "--fix"]
|
||||
language: system
|
||||
types: [zig]
|
||||
- repo: https://github.com/batmac/pre-commit-zig
|
||||
rev: v0.3.0
|
||||
hooks:
|
||||
- id: zig-build
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: test
|
||||
name: Run zig build test
|
||||
entry: zig
|
||||
# args: ["build", "coverage", "-Dcoverage-threshold=80"]
|
||||
args: ["build", "test"]
|
||||
language: system
|
||||
types: [file]
|
||||
pass_filenames: false
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2026 Emil Lerch
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
154
README.md
Normal file
154
README.md
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
# Water Recirculation Alexa Skill
|
||||
|
||||
An Alexa skill that triggers water recirculation on Rinnai tankless water heaters.
|
||||
|
||||
## Usage
|
||||
|
||||
> "Alexa, ask house to start the hot water"
|
||||
|
||||
This will authenticate with the Rinnai API and start a 15-minute recirculation cycle.
|
||||
|
||||
## Building
|
||||
|
||||
Requires [Zig 0.15](https://ziglang.org/) and [mise](https://mise.jdx.dev/) for version management.
|
||||
|
||||
The build defaults to `aarch64-linux` for AWS Lambda Graviton (arm64) deployment.
|
||||
|
||||
```bash
|
||||
# Debug build (arm64)
|
||||
zig build
|
||||
|
||||
# Release build (arm64)
|
||||
zig build -Doptimize=ReleaseFast
|
||||
|
||||
# Create Lambda deployment package
|
||||
zig build -Doptimize=ReleaseFast package
|
||||
|
||||
# Build for native target (e.g., for local testing)
|
||||
zig build -Dtarget=native
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [lambda-zig](../../lambda-zig) - AWS Lambda runtime for Zig
|
||||
- [controlr](../../controlr) - Rinnai API client (provides `rinnai` module)
|
||||
|
||||
## Deployment
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- AWS CLI configured with appropriate credentials
|
||||
- mise (for zig and bun version management)
|
||||
|
||||
### 1. Build the Package
|
||||
|
||||
```bash
|
||||
mise exec -- zig build -Doptimize=ReleaseFast package
|
||||
```
|
||||
|
||||
This creates `function.zip` containing the arm64 bootstrap executable.
|
||||
|
||||
### 2. Create Lambda Function (first time only)
|
||||
|
||||
```bash
|
||||
aws lambda create-function \
|
||||
--function-name water-recirculation \
|
||||
--runtime provided.al2023 \
|
||||
--handler bootstrap \
|
||||
--architectures arm64 \
|
||||
--role arn:aws:iam::ACCOUNT_ID:role/lambda_basic_execution \
|
||||
--zip-file fileb://function.zip \
|
||||
--timeout 30 \
|
||||
--memory-size 128
|
||||
```
|
||||
|
||||
### 3. Set Environment Variables
|
||||
|
||||
```bash
|
||||
aws lambda update-function-configuration \
|
||||
--function-name water-recirculation \
|
||||
--environment "Variables={COGNITO_USERNAME=your@email.com,COGNITO_PASSWORD=your_password}"
|
||||
```
|
||||
|
||||
### 4. Update Function Code (subsequent deploys)
|
||||
|
||||
```bash
|
||||
mise exec -- zig build -Doptimize=ReleaseFast package
|
||||
|
||||
aws lambda update-function-code \
|
||||
--function-name water-recirculation \
|
||||
--zip-file fileb://function.zip
|
||||
```
|
||||
|
||||
### 5. Deploy Alexa Skill
|
||||
|
||||
First time setup - configure ASK CLI (opens browser for Amazon login):
|
||||
|
||||
```bash
|
||||
mise exec -- bunx ask-cli configure
|
||||
```
|
||||
|
||||
Deploy the skill:
|
||||
|
||||
```bash
|
||||
mise exec -- bunx ask-cli deploy
|
||||
```
|
||||
|
||||
This will:
|
||||
- Create the Alexa skill in your developer account
|
||||
- Upload the interaction model
|
||||
- Link to the Lambda endpoint
|
||||
|
||||
After deployment, add the Alexa Skills Kit trigger permission to Lambda:
|
||||
|
||||
```bash
|
||||
aws lambda add-permission \
|
||||
--function-name water-recirculation \
|
||||
--statement-id alexa-skill \
|
||||
--action lambda:InvokeFunction \
|
||||
--principal alexa-appkit.amazon.com \
|
||||
--event-source-token amzn1.ask.skill.YOUR_SKILL_ID
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
water_recirculation/
|
||||
├── build.zig # Build configuration (defaults to arm64-linux)
|
||||
├── build.zig.zon # Dependencies (lambda-zig, controlr)
|
||||
├── ask-resources.json # ASK CLI deployment config
|
||||
├── src/
|
||||
│ └── main.zig # Alexa request handler
|
||||
├── skill-package/
|
||||
│ ├── skill.json # Alexa skill manifest
|
||||
│ └── interactionModels/
|
||||
│ └── custom/
|
||||
│ └── en-US.json # Interaction model
|
||||
└── function.zip # Lambda deployment package (after build)
|
||||
```
|
||||
|
||||
## Sample Utterances
|
||||
|
||||
- "start the hot water"
|
||||
- "turn on the hot water"
|
||||
- "heat the water"
|
||||
- "preheat the water"
|
||||
- "start recirculation"
|
||||
- "warm up the water"
|
||||
|
||||
## Lambda Details
|
||||
|
||||
- **Function**: `water-recirculation`
|
||||
- **Region**: us-west-2
|
||||
- **Architecture**: arm64 (Graviton)
|
||||
- **Runtime**: provided.al2023
|
||||
- **ARN**: `arn:aws:lambda:us-west-2:932028523435:function:water-recirculation`
|
||||
|
||||
## Alexa Skill
|
||||
|
||||
- **Skill ID**: `amzn1.ask.skill.c373c562-d574-4f38-bd06-001e96426d12`
|
||||
- **Invocation**: "Alexa, ask house to..."
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
10
ask-resources.json
Normal file
10
ask-resources.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"askcliResourcesVersion": "2020-03-31",
|
||||
"profiles": {
|
||||
"default": {
|
||||
"skillMetadata": {
|
||||
"src": "./skill-package"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
build.zig
Normal file
75
build.zig
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
// Default to aarch64-linux for Lambda Graviton deployment
|
||||
const target = b.standardTargetOptions(.{
|
||||
.default_target = .{
|
||||
.cpu_arch = .aarch64,
|
||||
.os_tag = .linux,
|
||||
},
|
||||
});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
// Get lambda-zig dependency
|
||||
const lambda_zig_dep = b.dependency("lambda_zig", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Get controlr dependency for rinnai module
|
||||
const controlr_dep = b.dependency("controlr", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Create the main module
|
||||
const main_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// Add lambda_runtime import
|
||||
main_module.addImport("lambda_runtime", lambda_zig_dep.module("lambda_runtime"));
|
||||
// Add rinnai import from controlr
|
||||
main_module.addImport("rinnai", controlr_dep.module("rinnai"));
|
||||
|
||||
// Create executable
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "bootstrap", // Lambda requires the executable to be named "bootstrap"
|
||||
.root_module = main_module,
|
||||
});
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
// Create a step to package for Lambda
|
||||
const package_step = b.step("package", "Package for AWS Lambda (arm64) deployment");
|
||||
|
||||
// After installing, create a zip
|
||||
const install_step = b.getInstallStep();
|
||||
|
||||
// Add a system command to create zip (requires zip to be installed)
|
||||
const zip_cmd = b.addSystemCommand(&.{
|
||||
"zip", "-j", "function.zip", "zig-out/bin/bootstrap",
|
||||
});
|
||||
zip_cmd.step.dependOn(install_step);
|
||||
package_step.dependOn(&zip_cmd.step);
|
||||
|
||||
// Test step - reuses the same target query, tests run via emulation or on native arm64
|
||||
const test_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_module.addImport("lambda_runtime", lambda_zig_dep.module("lambda_runtime"));
|
||||
test_module.addImport("rinnai", controlr_dep.module("rinnai"));
|
||||
|
||||
const main_tests = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_module = test_module,
|
||||
});
|
||||
|
||||
const run_main_tests = b.addRunArtifact(main_tests);
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_main_tests.step);
|
||||
}
|
||||
21
build.zig.zon
Normal file
21
build.zig.zon
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
.{
|
||||
.name = .water_recirculation_alexa,
|
||||
.version = "0.1.0",
|
||||
.fingerprint = 0xaff1623a413ed497,
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.controlr = .{
|
||||
.url = "git+https://git.lerch.org/lobo/controlr#4f5bd5b0607f73c9975ca41246fbfd5836cdfb98",
|
||||
.hash = "controlr-0.1.0-upFm0LtgAAAo85RiRDa0WmSrORIhAa5bCF8UdT9rDyUk",
|
||||
},
|
||||
.lambda_zig = .{
|
||||
.url = "git+https://git.lerch.org/lobo/lambda-zig#183d2d912c41ca721c8d18e5c258e4472d38db70",
|
||||
.hash = "lambda_zig-0.1.0-_G43_6YQAQD-ahqtf3DQpJroP__spvt4U_uI5TtMZ4Xv",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
9379
package-lock.json
generated
Normal file
9379
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
5
package.json
Normal file
5
package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"ask-cli": "^2.30.7"
|
||||
}
|
||||
}
|
||||
54
skill-package/interactionModels/custom/en-US.json
Normal file
54
skill-package/interactionModels/custom/en-US.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"interactionModel": {
|
||||
"languageModel": {
|
||||
"invocationName": "house",
|
||||
"intents": [
|
||||
{
|
||||
"name": "RecirculateWaterIntent",
|
||||
"slots": [],
|
||||
"samples": [
|
||||
"start the hot water",
|
||||
"start hot water",
|
||||
"turn on the hot water",
|
||||
"turn on hot water",
|
||||
"heat the water",
|
||||
"heat water",
|
||||
"preheat the water",
|
||||
"preheat water",
|
||||
"start recirculation",
|
||||
"start the recirculation",
|
||||
"start water recirculation",
|
||||
"run the hot water",
|
||||
"run hot water",
|
||||
"warm up the water",
|
||||
"warm the water up",
|
||||
"get hot water ready",
|
||||
"make the water hot",
|
||||
"I need hot water"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.HelpIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.StopIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.CancelIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.NavigateHomeIntent",
|
||||
"samples": []
|
||||
},
|
||||
{
|
||||
"name": "AMAZON.FallbackIntent",
|
||||
"samples": []
|
||||
}
|
||||
],
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
}
|
||||
53
skill-package/skill.json
Normal file
53
skill-package/skill.json
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
{
|
||||
"manifest": {
|
||||
"apis": {
|
||||
"custom": {
|
||||
"endpoint": {
|
||||
"uri": "arn:aws:lambda:us-west-2:932028523435:function:water-recirculation"
|
||||
},
|
||||
"interfaces": []
|
||||
}
|
||||
},
|
||||
"manifestVersion": "1.0",
|
||||
"publishingInformation": {
|
||||
"locales": {
|
||||
"en-US": {
|
||||
"name": "House Water Control",
|
||||
"summary": "Control water recirculation on your Rinnai tankless water heater",
|
||||
"description": "This skill allows you to start water recirculation on your Rinnai tankless water heater by voice command. Just say \"Alexa, ask house to start the hot water\" to begin a 15-minute recirculation cycle.",
|
||||
"examplePhrases": [
|
||||
"Alexa, ask house to start the hot water",
|
||||
"Alexa, ask house to heat the water",
|
||||
"Alexa, ask house to start recirculation"
|
||||
],
|
||||
"keywords": [
|
||||
"water heater",
|
||||
"rinnai",
|
||||
"recirculation",
|
||||
"hot water",
|
||||
"tankless"
|
||||
]
|
||||
}
|
||||
},
|
||||
"isAvailableWorldwide": false,
|
||||
"testingInstructions": "Sample testing instructions.",
|
||||
"category": "SMART_HOME",
|
||||
"distributionCountries": [
|
||||
"US"
|
||||
]
|
||||
},
|
||||
"privacyAndCompliance": {
|
||||
"allowsPurchases": false,
|
||||
"usesPersonalInfo": false,
|
||||
"isChildDirected": false,
|
||||
"isExportCompliant": true,
|
||||
"containsAds": false,
|
||||
"locales": {
|
||||
"en-US": {
|
||||
"privacyPolicyUrl": "",
|
||||
"termsOfUseUrl": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
165
src/main.zig
Normal file
165
src/main.zig
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
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 });
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue