initial vibe coded stuff that seems to work
This commit is contained in:
parent
fbead48e8e
commit
7f46d40027
5 changed files with 373 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
zig-out
|
||||
.zig-cache/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 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.
|
||||
42
README.md
Normal file
42
README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# List Devices - Zig Port
|
||||
|
||||
This is a direct port of `../list-devices.js` to Zig.
|
||||
|
||||
## Features
|
||||
|
||||
- Authenticates with AWS Cognito
|
||||
- Fetches device list via GraphQL (AppSync)
|
||||
- Displays device information
|
||||
- Checks recirculation status for the first device
|
||||
- Includes `startRecirculation` function (not called during testing as requested)
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
zig build
|
||||
```
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
./zig-out/bin/list-devices
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
zig build test
|
||||
```
|
||||
|
||||
## Requirements
|
||||
|
||||
- Zig 0.15.2
|
||||
- `.credentials` file in parent directory with username and password (one per line)
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Uses Zig's standard library HTTP client
|
||||
- JSON parsing with `std.json`
|
||||
- Base64url decoding for JWT tokens
|
||||
- Proper memory management with allocators
|
||||
- Error handling with Zig's error unions
|
||||
38
build.zig
Normal file
38
build.zig
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "list-devices",
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
|
||||
const exe_unit_tests = b.addTest(.{
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
|
||||
const test_step = b.step("test", "Run unit tests");
|
||||
test_step.dependOn(&run_exe_unit_tests.step);
|
||||
}
|
||||
270
src/main.zig
Normal file
270
src/main.zig
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
const std = @import("std");
|
||||
const http = std.http;
|
||||
const json = std.json;
|
||||
|
||||
const CLIENT_ID = "5ghq3i6k4p9s7dfu34ckmec91";
|
||||
const COGNITO_URL = "https://cognito-idp.us-east-1.amazonaws.com/";
|
||||
const APPSYNC_URL = "https://s34ox7kri5dsvdr43bfgp6qh6i.appsync-api.us-east-1.amazonaws.com/graphql";
|
||||
const API_KEY = "da2-dm2g4rqvjbaoxcpo4eccs3k5he";
|
||||
const SHADOW_API_URL = "https://698suy4zs3.execute-api.us-east-1.amazonaws.com/Prod/thing";
|
||||
|
||||
const AuthResult = struct {
|
||||
id_token: []const u8,
|
||||
user_uuid: []const u8,
|
||||
};
|
||||
|
||||
fn readCredentials(allocator: std.mem.Allocator) !struct { username: []const u8, password: []const u8, content: []const u8 } {
|
||||
const file = try std.fs.cwd().openFile("../.credentials", .{});
|
||||
defer file.close();
|
||||
const content = try file.readToEndAlloc(allocator, 1024);
|
||||
var it = std.mem.splitScalar(u8, std.mem.trim(u8, content, &std.ascii.whitespace), '\n');
|
||||
const username = it.next() orelse return error.InvalidCredentials;
|
||||
const password = it.next() orelse return error.InvalidCredentials;
|
||||
return .{ .username = username, .password = password, .content = content };
|
||||
}
|
||||
|
||||
fn authenticate(allocator: std.mem.Allocator, username: []const u8, password: []const u8) !AuthResult {
|
||||
var client = http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"AuthFlow\":\"USER_PASSWORD_AUTH\",\"ClientId\":\"{s}\",\"AuthParameters\":{{\"USERNAME\":\"{s}\",\"PASSWORD\":\"{s}\"}}}}", .{ CLIENT_ID, username, password });
|
||||
defer allocator.free(body);
|
||||
|
||||
const uri = try std.Uri.parse(COGNITO_URL);
|
||||
|
||||
var response_buf: [1024 * 1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&response_buf);
|
||||
const result = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .POST,
|
||||
.payload = body,
|
||||
.response_writer = &writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "X-Amz-Target", .value = "AWSCognitoIdentityProviderService.InitiateAuth" },
|
||||
.{ .name = "Content-Type", .value = "application/x-amz-json-1.1" },
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status != .ok) return error.AuthFailed;
|
||||
|
||||
const response_body = response_buf[0..writer.end];
|
||||
|
||||
const parsed = try json.parseFromSlice(json.Value, allocator, response_body, .{});
|
||||
defer parsed.deinit();
|
||||
|
||||
const id_token = parsed.value.object.get("AuthenticationResult").?.object.get("IdToken").?.string;
|
||||
const id_token_copy = try allocator.dupe(u8, id_token);
|
||||
|
||||
var token_it = std.mem.splitScalar(u8, id_token, '.');
|
||||
_ = token_it.next();
|
||||
const payload_b64 = token_it.next() orelse return error.InvalidToken;
|
||||
|
||||
const decoder = std.base64.url_safe_no_pad.Decoder;
|
||||
const payload_size = try decoder.calcSizeForSlice(payload_b64);
|
||||
const payload_buf = try allocator.alloc(u8, payload_size);
|
||||
defer allocator.free(payload_buf);
|
||||
try decoder.decode(payload_buf, payload_b64);
|
||||
|
||||
const payload_parsed = try json.parseFromSlice(json.Value, allocator, payload_buf, .{});
|
||||
defer payload_parsed.deinit();
|
||||
|
||||
const user_uuid = payload_parsed.value.object.get("sub").?.string;
|
||||
const user_uuid_copy = try allocator.dupe(u8, user_uuid);
|
||||
|
||||
return .{ .id_token = id_token_copy, .user_uuid = user_uuid_copy };
|
||||
}
|
||||
|
||||
fn getDevices(allocator: std.mem.Allocator, id_token: []const u8, username: []const u8) !json.Parsed(json.Value) {
|
||||
var client = http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const query = "query GetUserByEmail($email: String!) { getUserByEmail(email: $email) { items { id email devices { items { id thing_name device_name dsn model info { serial_id } } } } } }";
|
||||
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"query\":\"{s}\",\"variables\":{{\"email\":\"{s}\"}}}}", .{ query, username });
|
||||
defer allocator.free(body);
|
||||
|
||||
const uri = try std.Uri.parse(APPSYNC_URL);
|
||||
|
||||
var response_buf: [1024 * 1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&response_buf);
|
||||
_ = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .POST,
|
||||
.payload = body,
|
||||
.response_writer = &writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Authorization", .value = id_token },
|
||||
.{ .name = "x-api-key", .value = API_KEY },
|
||||
.{ .name = "Content-Type", .value = "application/json" },
|
||||
},
|
||||
});
|
||||
|
||||
const response_body = response_buf[0..writer.end];
|
||||
return try json.parseFromSlice(json.Value, allocator, response_body, .{});
|
||||
}
|
||||
|
||||
fn startRecirculation(allocator: std.mem.Allocator, id_token: []const u8, serial_number: []const u8, duration_minutes: u32) !bool {
|
||||
var client = http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const url = try std.fmt.allocPrint(allocator, "{s}/{s}/shadow", .{ SHADOW_API_URL, serial_number });
|
||||
defer allocator.free(url);
|
||||
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"recirculation_duration\":{d},\"set_recirculation_enabled\":true}}", .{duration_minutes});
|
||||
defer allocator.free(body);
|
||||
|
||||
const uri = try std.Uri.parse(url);
|
||||
const auth_header = try std.fmt.allocPrint(allocator, "Bearer {s}", .{id_token});
|
||||
defer allocator.free(auth_header);
|
||||
|
||||
const result = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .PATCH,
|
||||
.payload = body,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Authorization", .value = auth_header },
|
||||
.{ .name = "Content-Type", .value = "application/json" },
|
||||
},
|
||||
});
|
||||
|
||||
return result.status == .ok;
|
||||
}
|
||||
|
||||
fn getRecirculationStatus(allocator: std.mem.Allocator, id_token: []const u8, serial_number: []const u8) !json.Parsed(json.Value) {
|
||||
var client = http.Client{ .allocator = allocator };
|
||||
defer client.deinit();
|
||||
|
||||
const query = "query GetDeviceShadow($heater_serial_number: ID!) { getDeviceShadow(heater_serial_number: $heater_serial_number) { heater_serial_number set_recirculation_enabled recirculation_enabled recirculation_duration set_domestic_temperature operation_enabled } }";
|
||||
|
||||
const body = try std.fmt.allocPrint(allocator, "{{\"query\":\"{s}\",\"variables\":{{\"heater_serial_number\":\"{s}\"}}}}", .{ query, serial_number });
|
||||
defer allocator.free(body);
|
||||
|
||||
const uri = try std.Uri.parse(APPSYNC_URL);
|
||||
|
||||
var response_buf: [1024 * 1024]u8 = undefined;
|
||||
var writer = std.Io.Writer.fixed(&response_buf);
|
||||
_ = try client.fetch(.{
|
||||
.location = .{ .uri = uri },
|
||||
.method = .POST,
|
||||
.payload = body,
|
||||
.response_writer = &writer,
|
||||
.extra_headers = &.{
|
||||
.{ .name = "Authorization", .value = id_token },
|
||||
.{ .name = "x-api-key", .value = API_KEY },
|
||||
.{ .name = "Content-Type", .value = "application/json" },
|
||||
},
|
||||
});
|
||||
|
||||
const response_body = response_buf[0..writer.end];
|
||||
return try json.parseFromSlice(json.Value, allocator, response_body, .{});
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
const creds = try readCredentials(allocator);
|
||||
defer allocator.free(creds.content);
|
||||
|
||||
std.debug.print("🔐 Authenticating...\n", .{});
|
||||
const auth = try authenticate(allocator, creds.username, creds.password);
|
||||
defer allocator.free(auth.id_token);
|
||||
defer allocator.free(auth.user_uuid);
|
||||
std.debug.print("✓ User UUID: {s}\n\n", .{auth.user_uuid});
|
||||
|
||||
std.debug.print("📱 Fetching devices...\n", .{});
|
||||
const result = try getDevices(allocator, auth.id_token, creds.username);
|
||||
defer result.deinit();
|
||||
|
||||
if (result.value.object.get("data")) |data| {
|
||||
if (data.object.get("getUserByEmail")) |user_data| {
|
||||
if (user_data.object.get("items")) |items| {
|
||||
if (items.array.items.len > 0) {
|
||||
if (items.array.items[0].object.get("devices")) |devices_obj| {
|
||||
if (devices_obj.object.get("items")) |devices| {
|
||||
std.debug.print("\n✓ Found {d} device(s):\n\n", .{devices.array.items.len});
|
||||
|
||||
for (devices.array.items) |device| {
|
||||
const device_name = if (device.object.get("device_name")) |n| if (n == .string) n.string else "Unnamed" else "Unnamed";
|
||||
const thing_name = if (device.object.get("thing_name")) |n| if (n == .string) n.string else "N/A" else "N/A";
|
||||
const dsn = if (device.object.get("dsn")) |n| if (n == .string) n.string else "N/A" else "N/A";
|
||||
const model = if (device.object.get("model")) |m| if (m == .string) m.string else "N/A" else "N/A";
|
||||
const serial_id = if (device.object.get("info")) |info| blk: {
|
||||
if (info.object.get("serial_id")) |sid| {
|
||||
if (sid == .string) break :blk sid.string else break :blk "N/A";
|
||||
} else break :blk "N/A";
|
||||
} else "N/A";
|
||||
|
||||
std.debug.print(" • {s}\n", .{device_name});
|
||||
std.debug.print(" Thing Name: {s}\n", .{thing_name});
|
||||
std.debug.print(" DSN: {s}\n", .{dsn});
|
||||
std.debug.print(" Serial ID: {s}\n", .{serial_id});
|
||||
std.debug.print(" Model: {s}\n\n", .{model});
|
||||
}
|
||||
|
||||
if (devices.array.items.len > 0) {
|
||||
const device = devices.array.items[0];
|
||||
const serial_id = if (device.object.get("info")) |info| blk: {
|
||||
if (info.object.get("serial_id")) |sid| {
|
||||
if (sid == .string) break :blk sid.string else break :blk null;
|
||||
} else break :blk null;
|
||||
} else null;
|
||||
|
||||
if (serial_id) |sid| {
|
||||
const device_name = if (device.object.get("device_name")) |n| if (n == .string) n.string else "Unnamed" else "Unnamed";
|
||||
std.debug.print("🔍 Checking recirculation status for {s}...\n", .{device_name});
|
||||
|
||||
const status = try getRecirculationStatus(allocator, auth.id_token, sid);
|
||||
defer status.deinit();
|
||||
|
||||
if (status.value.object.get("data")) |status_data| {
|
||||
if (status_data.object.get("getDeviceShadow")) |shadow| {
|
||||
std.debug.print("\nCurrent Shadow State:\n", .{});
|
||||
|
||||
if (shadow.object.get("heater_serial_number")) |v| {
|
||||
if (v == .string) std.debug.print(" heater_serial_number: {s}\n", .{v.string});
|
||||
}
|
||||
if (shadow.object.get("set_recirculation_enabled")) |v| {
|
||||
if (v == .bool) std.debug.print(" set_recirculation_enabled: {}\n", .{v.bool});
|
||||
}
|
||||
if (shadow.object.get("recirculation_enabled")) |v| {
|
||||
if (v == .bool) std.debug.print(" recirculation_enabled: {}\n", .{v.bool});
|
||||
}
|
||||
if (shadow.object.get("recirculation_duration")) |v| {
|
||||
if (v == .integer) std.debug.print(" recirculation_duration: {}\n", .{v.integer});
|
||||
}
|
||||
if (shadow.object.get("set_domestic_temperature")) |v| {
|
||||
if (v == .integer) std.debug.print(" set_domestic_temperature: {}\n", .{v.integer});
|
||||
}
|
||||
if (shadow.object.get("operation_enabled")) |v| {
|
||||
if (v == .bool) std.debug.print(" operation_enabled: {}\n", .{v.bool});
|
||||
}
|
||||
|
||||
const recirc_enabled = if (shadow.object.get("recirculation_enabled")) |re| if (re == .bool) re.bool else false else false;
|
||||
|
||||
if (recirc_enabled) {
|
||||
std.debug.print("\n✓ Recirculation is already active\n", .{});
|
||||
// Recirculation code commented out as requested
|
||||
} else {
|
||||
// Recirculation code would go here but not called during testing
|
||||
std.debug.print("\n(Recirculation start function available but not called during testing)\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std.debug.print("❌ No serial_id found for device\n", .{});
|
||||
return error.NoSerialId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const err_str = try std.fmt.allocPrint(allocator, "{}", .{result.value});
|
||||
defer allocator.free(err_str);
|
||||
std.debug.print("❌ Error: {s}\n", .{err_str});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue