srctree

Gregory Mullen parent 23548660
init commit

filename was Deleted added: 547, removed: 4, total 543
@@ -0,0 +1,2 @@
.zig-cache/
zig-out/
 
filename was Deleted added: 547, removed: 4, total 543
@@ -0,0 +1,40 @@
const std = @import("std");
 
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
 
const dep_mqtt = b.dependency("mqtt", .{ .target = target, .optimize = optimize });
 
const exe = b.addExecutable(.{
.name = "herbomony",
.root_source_file = b.path("src/zigbee.zig"),
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("mqtt", dep_mqtt.module("mqtt"));
 
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_source_file = b.path("src/zigbee.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);
}
 
filename was Deleted added: 547, removed: 4, total 543
@@ -0,0 +1,16 @@
.{
.name = "herbomony",
.version = "0.0.0",
//.minimum_zig_version = "0.11.0",
.dependencies = .{
.mqtt = .{
.url = "git+https://github.com/GrayHatter/mqtt.git#b98e366f4eba92448f08092459d9c7cd5cc4b17d",
.hash = "122003852280b8cd47be5c5745c6a9964e8fbf526346214e5fafbf5024d730f9572a",
},
},
.paths = .{
"build.zig",
"build.zig.zon",
"src",
},
}
 
filename was Deleted added: 547, removed: 4, total 543
@@ -0,0 +1,485 @@
const mqtt = @import("mqtt");
 
pub fn main() !void {
log.err("startup", .{});
const a = std.heap.page_allocator;
 
var client = mqtt.Client.init(a, "localhost", 1883) catch |e| {
log.err("unable to connect to host", .{});
return e;
};
try client.connect();
 
var zigbee = Zigbee.init(a, &client);
 
while (client.recv()) |pkt| {
switch (pkt) {
.CONNACK => {
log.err("loop", .{});
log.err("CONNACK", .{});
try client.send(mqtt.Subscribe{ .channels = &.{"zigbee2mqtt/#"} });
},
.PUBLISH => |publ| {
//log.err("PUBLISH [{s}]", .{publ.topic_name});
if (std.mem.startsWith(u8, publ.topic_name, "zigbee2mqtt")) {
try zigbee.publish(publ);
}
},
.SUBACK => {
log.err("SUBACK ", .{});
},
else => |tag| {
log.err("", .{});
log.err("", .{});
log.err("", .{});
log.err("read [{s}]", .{@tagName(tag)});
//log.err("discarding {}", .{@min(ready, reported)});
log.err("", .{});
log.err("", .{});
log.err("", .{});
//fifo.discard(@min(ready, reported));
},
}
} else |err| {
log.err("", .{});
log.err("", .{});
log.err("", .{});
log.err("recv error [{any}]", .{err});
}
 
log.err("end going to exit", .{});
}
 
const Z2m = struct {
const bridge = struct {
const state = struct {};
const info = struct {};
const logging = struct {
level: []const u8,
message: []const u8,
};
const groups = struct {
friendly_name: []const u8,
id: usize,
members: []struct {
endpoint: usize,
ieee_address: []const u8,
},
scenes: []struct {
id: usize,
name: []const u8,
},
};
const definitions = struct {};
const extensions = struct {};
 
const devices = struct {
definition: ?Definition = null,
date_code: ?[]const u8 = null,
disabled: ?bool = null,
endpoints: ?Endpoints = null,
friendly_name: ?[]const u8 = null,
ieee_address: ?[]const u8 = null,
interview_completed: ?bool = null,
interviewing: ?bool = null,
network_address: ?usize = null,
supported: ?bool = null,
type: ?[]const u8 = null,
manufacturer: ?[]const u8 = null,
model_id: ?[]const u8 = null,
power_source: ?[]const u8 = null,
software_build_id: ?[]const u8 = null,
 
pub const Definition = struct {
description: ?[]const u8,
exposes: ?[]Exposed = null,
model: ?[]const u8 = null,
options: ?[]struct {} = null,
supports_ota: ?bool = null,
vendor: ?[]const u8 = null,
};
 
pub const Exposed = struct {
features: ?[]struct {
access: ?usize = null,
description: ?[]const u8 = null,
label: ?[]const u8 = null,
name: ?[]const u8 = null,
property: ?[]const u8 = null,
type: ?[]const u8 = null,
value_off: ?[]const u8 = null,
value_on: ?[]const u8 = null,
value_toggle: ?[]const u8 = null,
} = null,
description: ?[]const u8 = null,
name: ?[]const u8 = null,
};
 
pub const Endpoints = struct {
@"1": ?EndpointObject = null,
@"2": ?EndpointObject = null,
@"3": ?EndpointObject = null,
@"4": ?EndpointObject = null,
@"242": ?EndpointObject = null,
};
 
pub const EndpointObject = struct {
bindings: ?[]struct {
cluster: ?[]const u8,
target: ?struct {
endpoint: ?usize,
ieee_address: ?[]const u8,
type: ?[]const u8,
},
configured_reportings: ?[]struct {
attribute: []const u8,
cluster: []const u8,
maximum_report_interval: usize,
minimum_report_interval: usize,
reportable_change: usize,
} = null,
scenes: ?[]struct {} = null,
} = null,
clusters: ?struct {
input: ?[][]const u8,
output: ?[][]const u8,
} = null,
};
};
};
};
 
pub const Rules = struct {
// edge falling device/name/attribute
//
// do <complex target>
// set <device> [verb] <noun>
// when office/mmw0/presence goes false set office/lights off
// when office/mmw0/presence goes true set office/lights on
//
// sequence
//
//
 
pub const Rule = struct {
rule: Kind,
target: []const u8,
action: Action = .{},
 
pub const Kind = union(enum) {
edge: Direction,
slope: struct {
direction: Direction,
vect: f64,
},
};
 
pub const Direction = enum {
falling,
rising,
};
 
pub const Action = struct {};
};
 
pub fn parse(a: Allocator, str: []const u8) ![]Rule {
var list = std.ArrayList(Rule).init(a);
errdefer list.deinit();
var tokens = std.mem.tokenizeScalar(u8, str, ' ');
 
while (tokens.next()) |next| {
if (eqlAny(next, "edge")) {
const peek = tokens.peek() orelse return error.InvalidSyntax;
 
if (eqlAny(peek, "falling")) {
try list.append(.{
.rule = .{ .edge = .falling },
.target = "",
});
_ = tokens.next();
} else {
try list.append(.{
.rule = .{ .edge = .rising },
.target = "",
});
_ = tokens.next();
}
}
}
return try list.toOwnedSlice();
}
 
test parse {
const a = std.testing.allocator;
 
const empty = try parse(a, "");
try std.testing.expectEqual(&[0]Rule{}, empty);
 
const one = try parse(a, "edge falling");
defer a.free(one);
try std.testing.expectEqualDeep(&[1]Rule{.{
.rule = .{ .edge = .falling },
.target = "",
}}, one);
 
const one_ri = try parse(a, "edge rising");
defer a.free(one_ri);
try std.testing.expectEqualDeep(&[1]Rule{.{
.rule = .{ .edge = .rising },
.target = "",
}}, one_ri);
}
};
 
pub const Device = struct {
name: []const u8,
address: []const u8,
 
state: State = .{},
 
pub const State = struct {
presence: ?bool = null,
detection_delay: ?bool = null,
fading_time: ?bool = null,
illuminance_lux: ?bool = null,
interval_time: ?bool = null,
linkquality: ?usize = null,
minimum_range: ?f64 = null,
maximum_range: ?f64 = null,
target_distance: ?f64 = null,
sensitivity: ?usize = null,
power_on_behavior: PowerOn = .nos,
 
voltage: ?f64 = null,
ac_frequency: ?usize = null,
state: PowerOn = .nos,
power: ?f64 = null,
current: ?f64 = null,
power_factor: ?f64 = null,
energy: ?f64 = null,
};
 
pub const PowerOn = enum {
nos,
on,
off,
toggle,
previous,
};
 
pub fn initZ2m(zb: *Zigbee, z2m_bd: Z2m.bridge.devices) !Device {
return .{
.name = try zb.alloc.dupe(u8, z2m_bd.friendly_name orelse return error.InvalidDevice),
.address = try zb.alloc.dupe(u8, z2m_bd.ieee_address orelse return error.InvalidDevice),
};
}
 
pub fn updateTyped(d: *Device, T: type, comptime fname: []const u8, payload: []const u8) bool {
const field: *T = &@field(d.state, fname);
switch (T) {
?bool => {
const next = if (eql(u8, payload, "true")) true else false;
const edge = field.* == null or field.*.? != next;
field.* = next;
return edge;
},
?usize => {
const next: ?usize = parseInt(usize, payload, 0) catch |err| brk: {
log.err(
"unable to parseInt on {s} with [{s}]{any} because {}",
.{ fname, payload, payload, err },
);
break :brk null;
};
 
if (next) |nxt| {
const edge = field.* == null or field.*.? != nxt;
field.* = nxt;
return edge;
} else {
const edge = field.* != null;
field.* = null;
return edge;
}
},
?f64 => {
const next: ?f64 = parseFloat(f64, payload) catch |err| brk: {
log.err(
"unable to parseFloat on {s} with [{s}]{any} because {}",
.{ fname, payload, payload, err },
);
break :brk null;
};
 
if (next) |nxt| {
const edge = field.* == null or field.*.? != nxt;
field.* = nxt;
return edge;
} else {
const edge = field.* != null;
field.* = null;
return edge;
}
},
PowerOn => {
inline for (@typeInfo(T).Enum.fields) |en| {
if (eqlAny(en.name, payload)) {
defer field.* = @enumFromInt(en.value);
return field.* != @as(T, @enumFromInt(en.value));
}
} else {
log.err("unable to parse enum on {s} with [{s}]{any}", .{ fname, payload, payload });
return false;
}
},
else => comptime unreachable,
}
}
 
pub fn update(d: *Device, zb: *Zigbee, name: []const u8, payload: []const u8) !void {
const target = name[d.name.len..];
if (target.len == 0) return;
inline for (@typeInfo(State).Struct.fields) |field| {
if (eql(u8, target[1..], field.name)) {
if (d.updateTyped(field.type, field.name, payload)) {
log.err("edge on [{s}] at {s} with {s}", .{ d.name, target, payload });
if (eql(u8, "office/mmw0", d.name) and eql(u8, target, "/presence")) {
log.err("sendable because {any}", .{payload});
try zb.client.send(mqtt.Publish{
.topic_name = "zigbee2mqtt/office/lights/set",
.packet_ident = null,
.properties = "",
.payload = if (d.state.presence.?) "ON" else "OFF",
});
}
}
return;
}
}
// Sorry, trying to filter messages
if (std.mem.startsWith(u8, target, "/update")) {
if (target.len > 7 and (target[7] == '-' or target[7] == '_')) {
return;
}
}
log.warn("device({s}) unsupported field {s} [{s}]-{any}", .{ d.name, target, payload, payload });
}
};
 
pub const Zigbee = struct {
alloc: Allocator,
client: *mqtt.Client,
devices: std.ArrayList(Device),
 
pub fn init(a: Allocator, client: *mqtt.Client) Zigbee {
return .{
.alloc = a,
.client = client,
.devices = std.ArrayList(Device).init(a),
};
}
 
pub fn publish(zb: *Zigbee, p: mqtt.Publish) !void {
if (std.mem.startsWith(u8, p.topic_name[11..], "/bridge")) {
try zb.bridge(p);
} else if (std.mem.startsWith(u8, p.topic_name[11..], "/bridge")) {} else {
for (zb.devices.items) |*item| {
if (std.mem.startsWith(u8, p.topic_name[12..], item.name)) {
//log.err("zigbee updating {s}", .{p.topic_name[12..]});
try item.update(zb, p.topic_name[12..], p.payload);
break;
}
} else {
log.err("zigbee (maybe device?) {s} && {s}", .{ p.topic_name[11..], p.payload });
}
}
}
 
pub fn bridge(zb: *Zigbee, p: mqtt.Publish) !void {
if (std.mem.startsWith(u8, p.topic_name[18..], "/info")) {
const res = try std.json.parseFromSlice(
Z2m.bridge.info,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = true },
);
 
log.err("info {any}", .{res.value});
} else if (std.mem.startsWith(u8, p.topic_name[18..], "/devices")) {
const res = try std.json.parseFromSlice(
[]Z2m.bridge.devices,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = true },
);
 
for (res.value) |r| {
log.err("devices {s}", .{r.friendly_name orelse "name is empty"});
try zb.devices.append(try Device.initZ2m(zb, r));
if (r.definition) |def| {
if (def.exposes) |exps| for (exps) |exp| {
if (exp.name) |name| log.err(" exp {s}", .{name});
};
}
}
} else if (std.mem.startsWith(u8, p.topic_name[18..], "/groups")) {
const res = try std.json.parseFromSlice(
[]Z2m.bridge.groups,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = true },
);
for (res.value) |r| {
log.err("{}", .{r});
}
} else if (std.mem.startsWith(u8, p.topic_name[18..], "/definitions")) {
const res = std.json.parseFromSlice(
Z2m.bridge.definitions,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = true },
) catch {
log.err("ERROR FOR {s}", .{p.topic_name[18..]});
log.err("DUMP {s}", .{p.payload[0..]});
return;
};
log.err("{}", .{res.value});
} else if (std.mem.startsWith(u8, p.topic_name[18..], "/extensions")) {
const res = try std.json.parseFromSlice(
[]Z2m.bridge.extensions,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = false },
);
for (res.value) |r| {
log.err("{}", .{r});
}
} else if (std.mem.startsWith(u8, p.topic_name[18..], "/logging")) {
const res = try std.json.parseFromSlice(
Z2m.bridge.logging,
std.heap.page_allocator,
p.payload,
.{ .ignore_unknown_fields = false },
);
_ = res;
//log.warn("[{s}] -- {s}", .{ res.value.level, res.value.message });
} else {
log.err("skipped {s}", .{p.topic_name[18..]});
log.err("pre {s}", .{p.payload[0..]});
}
}
};
 
test "main" {
std.testing.refAllDecls(@This());
}
 
const std = @import("std");
const Allocator = std.mem.Allocator;
const eql = std.mem.eql;
const eqlAny = std.ascii.eqlIgnoreCase;
const parseInt = std.fmt.parseInt;
const parseFloat = std.fmt.parseFloat;
const log = std.log.scoped(.zigbee);
const AnyReader = std.io.AnyReader;
const AnyWriter = std.io.AnyWriter;