package com.battery.device.simulator; import java.math.BigDecimal; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; import com.battery.device.api.model.DeviceWorkStatus; import com.battery.device.simulator.BtsSimulatorStateStore.*; /** 提供请求时的采集器当前值;S 通道仍是待现场验证的开发合同。 */ public final class DevelopmentCollectorSimulation { public static final int DEFAULT_SAMPLE_INTERVAL_MS = 1000; private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong"); private static final DateTimeFormatter DEVICE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); private DevelopmentCollectorSimulation() { } /** 创建带稳定 S 通道和三项固定演示指标的采集器状态。 */ public static List defaultChannels(int channelCount) { List channels = new ArrayList<>(channelCount); for (int index = 1; index <= channelCount; index++) { channels.add(new CollectorChannelState(channelCode(index), List.of( new CollectorMetricState("temperature", new BigDecimal("25.0") .add(BigDecimal.valueOf(index, 1)), "℃"), new CollectorMetricState("positive_potential", new BigDecimal("3.700"), "V"), new CollectorMetricState("negative_potential", new BigDecimal("-0.100"), "V")))); } return List.copyOf(channels); } /** 调整设备拓扑,保留已有通道的配置。 */ public static CollectorState resize(CollectorState current, String ip, int channelCount, boolean online, Instant changedAt) { var defaults = defaultChannels(channelCount); List channels = new ArrayList<>(); for (int index = 0; index < channelCount; index++) channels.add(index < current.channels().size() ? current.channels().get(index) : defaults.get(index)); return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(), current.name(), current.manufacturer(), current.model(), channelCount, online, current.sampleIntervalMs(), channels, List.of()); } /** 保存当前测量配置,等待下一次实时请求。 */ public static CollectorState configureChannel(CollectorState current, String channelCode, List metrics) { validateMetrics(metrics); int index = parseChannelIndex(channelCode, current.channelCount()); List channels = new ArrayList<>(current.channels()); channels.set(index, new CollectorChannelState(channelCode, metrics, channels.get(index).workStatus())); return copy(current, channels, List.of()); } /** 保存通道工作状态,等待下一次实时请求。 */ public static CollectorState configureStatus(CollectorState current, String channelCode, DeviceWorkStatus status) { int index = parseChannelIndex(channelCode, current.channelCount()); List channels = new ArrayList<>(current.channels()); channels.set(index, new CollectorChannelState(channelCode, channels.get(index).metrics(), status)); return copy(current, channels, List.of()); } /** 仅生成本次请求的当前值,不补算上一次请求与本次之间的空档。 */ public static CollectorState sample(CollectorState current, Instant now, long sequence) { Instant observedAt = now.truncatedTo(java.time.temporal.ChronoUnit.MILLIS); List measurements = new ArrayList<>(); for (CollectorChannelState channel : current.channels()) { if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) continue; for (CollectorMetricState metric : channel.metrics()) measurements.add(new CollectorRealtimeMeasurement("dev-sim", current.deviceCode(), channel.channelCode(), sequence, observedAt, DEVICE_TIME_FORMATTER.format(observedAt.atZone(DEVICE_ZONE)), observedAt, metric.metricCode(), metric.value(), metric.unit())); } return copy(current, current.channels(), measurements); } /** 校验恢复后的稳定通道和测量配置。 */ public static void validate(CollectorState state) { if (state.channels().size() != state.channelCount()) throw new IllegalStateException("Invalid collector topology"); for (int index = 0; index < state.channels().size(); index++) { var channel = state.channels().get(index); if (!channelCode(index + 1).equals(channel.channelCode())) throw new IllegalStateException("Invalid collector channel identity"); validateMetrics(channel.metrics()); } } private static CollectorState copy(CollectorState current, List channels, List measurements) { return new CollectorState(current.collectorId(), current.deviceCode(), current.ip(), current.mac(), current.name(), current.manufacturer(), current.model(), current.channelCount(), current.online(), current.sampleIntervalMs(), channels, measurements); } private static void validateMetrics(List metrics) { if (metrics == null || metrics.isEmpty()) throw new IllegalArgumentException("Collector channel must contain at least one metric"); java.util.HashSet codes = new java.util.HashSet<>(); for (CollectorMetricState metric : metrics) { if (metric.metricCode() == null || metric.metricCode().isBlank() || metric.value() == null || !metric.metricCode().equals(metric.metricCode().trim()) || metric.unit() == null || metric.unit().isBlank() || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode())) throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique"); } } private static int parseChannelIndex(String value, int channelCount) { if (value == null || !value.matches("S-\\d{2}")) throw new IllegalArgumentException("Collector channel must use S-01...S-N identity"); int index = Integer.parseInt(value.substring(2)) - 1; if (index < 0 || index >= channelCount || !channelCode(index + 1).equals(value)) throw new IllegalArgumentException("Unknown collector channel: " + value); return index; } private static String channelCode(int index) { return String.format("S-%02d", index); } public record CollectorRealtimeSnapshot(String collectorCode, boolean online, int sampleIntervalMs, List measurements) { public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); } } }