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