| | |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsPlaybackState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState; |
| | | import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore; |
| | |
| | | private SimulatorState state; |
| | | private boolean running = true; |
| | | private long observationSequence; |
| | | private final java.util.Map<String, java.util.List<SimBtsHistory>> btsHistory = new java.util.LinkedHashMap<>(); |
| | | private final java.util.Map<String, java.util.List<BtsHistoryRecord>> btsHistory = new java.util.LinkedHashMap<>(); |
| | | private static final long SIMULATED_RECORD_INTERVAL_SECONDS = 60L; |
| | | private static final DateTimeFormatter BTS_HISTORY_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss") |
| | | .withZone(ZoneId.of("Asia/Hong_Kong")); |
| | | |
| | |
| | | List<DeviceState> devices = new ArrayList<>(state.devices()); |
| | | devices.removeIf(item -> item.deviceId() == deviceId); |
| | | List<ChannelTombstone> tombstones = new ArrayList<>(state.tombstones()); |
| | | BtsDeviceProfile removedProfile = profileFor(removed); |
| | | for (ChannelState channel : removed.channels()) |
| | | { |
| | | upsertTombstone(tombstones, removed.deviceId(), removed.ip(), channel.channelId()); |
| | | String key = historyKey(removedProfile.devtype(), removed.deviceId(), removedProfile.subDeviceId(), |
| | | channel.channelId()); |
| | | stateStore.deleteBtsHistory(key); |
| | | stateStore.deleteBtsPlaybackState(key); |
| | | btsHistory.remove(key); |
| | | } |
| | | persist(new SimulatorState(state.version(), devices, state.collectors(), state.nextDeviceId(), |
| | | state.nextCollectorId(), tombstones)); |
| | | } |
| | |
| | | state.nextCollectorId(), state.tombstones()), historyAppends); |
| | | } |
| | | |
| | | /** 按墙上时间推进检测柜模拟播放;设备状态和测试接收状态互不影响。 */ |
| | | public synchronized void advanceBts(Instant now) |
| | | { |
| | | for (DeviceState device : state.devices()) |
| | | { |
| | | if (!device.online()) continue; |
| | | BtsDeviceProfile profile = profileFor(device); |
| | | for (ChannelState channel : device.channels()) |
| | | { |
| | | if (!channel.online() || channel.workStatus() != DeviceWorkStatus.WORKING) continue; |
| | | String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId()); |
| | | BtsPlaybackState playback = playbackState(key, now); |
| | | advanceBtsPlayback(key, profile, playback, |
| | | profile.currentMaxA().doubleValue() * 0.2d, 3.7d, now); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** 设置检测柜通道演示倍速,只接受 1x、60x 和 600x。 */ |
| | | public synchronized void setBtsSpeed(int deviceId, int channelId, int speedMultiplier) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | BtsPlaybackState current = playbackState(key, Instant.now()); |
| | | stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(current.playing(), speedMultiplier, |
| | | current.cycleNo(), current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(), |
| | | Instant.now(), current.nextSequence(), current.naturallyFinished())); |
| | | } |
| | | |
| | | /** 开始播放指定检测柜通道的演示时间线。 */ |
| | | public synchronized void playBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); } |
| | | /** 暂停模拟器时间线,不改变检测柜通道工作事实。 */ |
| | | public synchronized void pauseBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, false); } |
| | | /** 从暂停状态恢复模拟器时间线。 */ |
| | | public synchronized void resumeBts(int deviceId, int channelId) { setBtsPlaying(deviceId, channelId, true); } |
| | | |
| | | private void setBtsPlaying(int deviceId, int channelId, boolean playing) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | Instant now = Instant.now(); |
| | | BtsPlaybackState current = playbackState(key, now); |
| | | if (current.naturallyFinished() && playing) return; |
| | | stateStore.saveBtsPlaybackState(key, new BtsPlaybackState(playing, current.speedMultiplier(), current.cycleNo(), |
| | | current.stepNo(), current.stepName(), current.stepType(), current.simulatedAt(), now, |
| | | current.nextSequence(), current.naturallyFinished())); |
| | | } |
| | | |
| | | /** 跳转到下一实际工步;不向设备发送控制报文。 */ |
| | | public synchronized void nextBtsStep(int deviceId, int channelId) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | BtsPlaybackState current = playbackState(key, Instant.now()); |
| | | BtsExcelFixture.FixtureRecord next = nextFixtureStep(current.cycleNo(), current.stepNo()); |
| | | if (next == null) |
| | | { |
| | | BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount()); |
| | | savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(), |
| | | last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(), |
| | | BtsExcelFixture.recordCount() + 1L, true)); |
| | | return; |
| | | } |
| | | savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(), |
| | | next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false)); |
| | | } |
| | | |
| | | /** 跳转到下一循环的首工步;不补齐当前循环缺失的首尾工步。 */ |
| | | public synchronized void nextBtsCycle(int deviceId, int channelId) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | BtsPlaybackState current = playbackState(key, Instant.now()); |
| | | BtsExcelFixture.FixtureRecord next = BtsExcelFixture.records().stream() |
| | | .filter(item -> item.cycleNo() > current.cycleNo()).findFirst().orElse(null); |
| | | if (next == null) |
| | | { |
| | | BtsExcelFixture.FixtureRecord last = BtsExcelFixture.record(BtsExcelFixture.recordCount()); |
| | | savePlayback(key, new BtsPlaybackState(false, current.speedMultiplier(), last.cycleNo(), last.stepNo(), |
| | | last.stepName(), last.stepType(), current.simulatedAt(), Instant.now(), |
| | | BtsExcelFixture.recordCount() + 1L, true)); |
| | | return; |
| | | } |
| | | savePlayback(key, new BtsPlaybackState(current.playing(), current.speedMultiplier(), next.cycleNo(), next.stepNo(), |
| | | next.stepName(), next.stepType(), current.simulatedAt(), Instant.now(), next.sequenceId(), false)); |
| | | } |
| | | |
| | | /** 清空单通道播放和原始数据,恢复到首循环首工步。 */ |
| | | public synchronized void resetBts(int deviceId, int channelId) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | btsHistory.remove(key); |
| | | stateStore.deleteBtsHistory(key); |
| | | stateStore.deleteBtsPlaybackState(key); |
| | | savePlayback(key, defaultPlayback(Instant.now())); |
| | | } |
| | | |
| | | /** 返回播放状态及当前通道的 record、step、cycle 数据。 */ |
| | | public synchronized PlaybackSnapshot btsPlayback(int deviceId, int channelId) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | String key = btsKey(device, channelId); |
| | | BtsPlaybackState state = playbackState(key, Instant.now()); |
| | | return new PlaybackSnapshot(deviceId, channelId, state, btsRecords(deviceId, channelId), |
| | | btsSteps(deviceId, channelId), btsCycles(deviceId, channelId)); |
| | | } |
| | | |
| | | /** 返回只追加的检测柜原始 record。 */ |
| | | public synchronized List<BtsHistoryRecord> btsRecords(int deviceId, int channelId) |
| | | { |
| | | DeviceState device = requireDevice(deviceId); |
| | | requireChannel(device, channelId); |
| | | return List.copyOf(btsHistoryFor(btsKey(device, channelId))); |
| | | } |
| | | |
| | | /** 按实际出现的循环和工步汇总 record,不补齐缺失行。 */ |
| | | public synchronized List<BtsStepRecord> btsSteps(int deviceId, int channelId) |
| | | { |
| | | java.util.Map<String, BtsStepAccumulator> grouped = new java.util.LinkedHashMap<>(); |
| | | for (BtsHistoryRecord item : btsRecords(deviceId, channelId)) |
| | | { |
| | | String key = item.cycleNo() + ":" + item.stepNo(); |
| | | grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item)).add(item); |
| | | } |
| | | return grouped.values().stream().map(BtsStepAccumulator::finish).toList(); |
| | | } |
| | | |
| | | /** 按实际出现的循环汇总 record,不增加测试结束统计行。 */ |
| | | public synchronized List<BtsCycleRecord> btsCycles(int deviceId, int channelId) |
| | | { |
| | | java.util.Map<Integer, BtsCycleAccumulator> grouped = new java.util.LinkedHashMap<>(); |
| | | for (BtsHistoryRecord item : btsRecords(deviceId, channelId)) |
| | | grouped.computeIfAbsent(item.cycleNo(), ignored -> new BtsCycleAccumulator(item)).add(item); |
| | | return grouped.entrySet().stream().map(entry -> { |
| | | BtsCycleRecord calculated = entry.getValue().finish(); |
| | | BtsExcelFixture.CycleSummary expected = BtsExcelFixture.cycle(entry.getKey()); |
| | | if (expected == null || entry.getValue().count < expected.recordCount()) return calculated; |
| | | return new BtsCycleRecord(calculated.cycleNo(), expected.recordCount(), calculated.startedAt(), |
| | | calculated.endedAt(), expected.chargeCapacityAh(), expected.dischargeCapacityAh(), |
| | | expected.chargeEnergyWh(), expected.dischargeEnergyWh()); |
| | | }).toList(); |
| | | } |
| | | |
| | | /** 返回采集器最新实时快照;快照与分页历史保持不同读取路径。 */ |
| | | public synchronized CollectorRealtimeSnapshot collectorRealtime(int collectorId) |
| | | { |
| | |
| | | { |
| | | SimulatorState defaultState = defaultState(); |
| | | stateStore.reset(defaultState); |
| | | btsHistory.clear(); |
| | | state = defaultState; |
| | | } |
| | | |
| | |
| | | profile.currentMinA().doubleValue(), profile.currentMaxA().doubleValue()) : 0d; |
| | | double voltage = clamp(3.7d + wave * 0.01d, profile.voltageMinV().doubleValue(), |
| | | profile.voltageMaxV().doubleValue()); |
| | | double capacity = working ? Math.max(0d, tick * 0.001d * current) : 0d; |
| | | String key = historyKey(profile.devtype(), observed.deviceId(), profile.subDeviceId(), channel.channelId()); |
| | | BtsPlaybackState playback = playbackState(key, Instant.now()); |
| | | double capacity = working ? latestCapacity(key) : 0d; |
| | | double energy = capacity * voltage; |
| | | if (working) |
| | | { |
| | | String key = historyKey(profile.devtype(), observed.deviceId(), profile.subDeviceId(), channel.channelId()); |
| | | java.util.List<SimBtsHistory> history = btsHistory.computeIfAbsent(key, ignored -> new ArrayList<>()); |
| | | history.add(new SimBtsHistory(history.size() + 1L, Instant.now(), current, voltage, capacity, energy)); |
| | | SimBtsHistory latest = history.get(history.size() - 1); |
| | | stateStore.appendBtsHistory(key, new BtsHistoryRecord(latest.sequence(), latest.at(), latest.current(), |
| | | latest.voltage(), latest.capacity(), latest.energy())); |
| | | if (history.size() > BtsSimulatorStateStore.MAX_HISTORY_RECORDS) history.remove(0); |
| | | playback = advanceBtsPlayback(key, profile, playback, current, voltage); |
| | | BtsHistoryRecord latest = latestRecord(key); |
| | | if (latest != null) |
| | | { |
| | | capacity = latest.capacityAh(); |
| | | energy = latest.energyWh(); |
| | | } |
| | | } |
| | | int cycleNo = playback.cycleNo(); |
| | | int stepNo = playback.stepNo(); |
| | | String stepType = playback.stepType(); |
| | | response.append("<inquire dev=\"").append(profile.devtype()).append('-').append(observed.deviceId()) |
| | | .append('-').append(profile.subDeviceId()).append('-').append(channel.channelId()) |
| | | .append("-0\" cycle_id=\"1\" step_id=\"1\" step_type=\"cc\" workstatus=\"") |
| | | .append("-0\" cycle_id=\"").append(cycleNo).append("\" step_id=\"").append(stepNo) |
| | | .append("\" step_type=\"").append(stepType).append("\" workstatus=\"") |
| | | .append(toBtsWorkStatus(channel.workStatus())) |
| | | .append("\" current=\"").append(decimal(current)).append("\" voltage=\"") |
| | | .append(decimal(voltage)).append("\" capacity=\"").append(decimal(capacity)) |
| | |
| | | int devid = Integer.parseInt(attribute(attrs, "devid")); |
| | | int subdevid = Integer.parseInt(attribute(attrs, "subdevid")); |
| | | int chlid = Integer.parseInt(attribute(attrs, "chlid")); |
| | | int count = btsHistory.getOrDefault(historyKey(devtype, devid, subdevid, chlid), List.of()).size(); |
| | | int count = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid)).size(); |
| | | response.append("<chl devtype=\"").append(devtype).append("\" devid=\"").append(devid) |
| | | .append("\" subdevid=\"").append(subdevid).append("\" chlid=\"").append(chlid) |
| | | .append("\" testid=\"SIM-").append(devid).append('-').append(chlid).append("\" count=\"") |
| | |
| | | String testId = attribute(request, "testid"); |
| | | long start = Long.parseLong(attribute(request, "startpos")); |
| | | int count = Integer.parseInt(attribute(request, "count")); |
| | | List<SimBtsHistory> history = btsHistory.getOrDefault(historyKey(devtype, devid, subdevid, chlid), List.of()); |
| | | List<BtsHistoryRecord> history = btsHistoryFor(historyKey(devtype, devid, subdevid, chlid)); |
| | | int from = (int) Math.min(Math.max(0L, start - 1L), history.size()); |
| | | int to = Math.min(history.size(), from + Math.max(1, count)); |
| | | StringBuilder response = new StringBuilder("<bts version=\"1.0\"><cmd>download_resp</cmd><download devtype=\"") |
| | | .append(devtype).append("\" devid=\"").append(devid).append("\" subdevid=\"").append(subdevid) |
| | | .append("\" chlid=\"").append(chlid).append("\" testid=\"").append(testId) |
| | | .append("\"><list>"); |
| | | for (SimBtsHistory item : history.subList(from, to)) |
| | | response.append("<data seqid=\"").append(item.sequence()).append("\" stepid=\"1\" cycleid=\"1\" steptype=\"cc\" atime=\"") |
| | | .append(BTS_HISTORY_TIME.format(item.at())).append("\" testtime=\"1000\" volt=\"") |
| | | .append(decimal(item.voltage())).append("\" curr=\"").append(decimal(item.current())) |
| | | .append("\" cap=\"").append(decimal(item.capacity())).append("\" eng=\"").append(decimal(item.energy())).append("\"/>"); |
| | | for (BtsHistoryRecord item : history.subList(from, to)) |
| | | response.append("<data seqid=\"").append(item.sequenceId()).append("\" stepid=\"").append(item.stepNo()) |
| | | .append("\" cycleid=\"").append(item.cycleNo()).append("\" steptype=\"").append(item.stepType()).append("\" atime=\"") |
| | | .append(BTS_HISTORY_TIME.format(item.deviceTime())).append("\" testtime=\"60000\" volt=\"") |
| | | .append(decimal(item.voltageV())).append("\" curr=\"").append(decimal(item.currentA())) |
| | | .append("\" cap=\"").append(decimal(item.capacityAh())).append("\" eng=\"").append(decimal(item.energyWh())).append("\"/>"); |
| | | return response.append("</list></download></bts>").toString(); |
| | | } |
| | | |
| | |
| | | for (ChannelState channel : device.channels()) |
| | | { |
| | | String key = historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channel.channelId()); |
| | | ArrayList<SimBtsHistory> restored = new ArrayList<>(); |
| | | ArrayList<BtsHistoryRecord> restored = new ArrayList<>(); |
| | | long offset = 0; |
| | | while (true) |
| | | { |
| | | var page = stateStore.loadBtsHistoryPage(key, offset, BtsSimulatorStateStore.MAX_HISTORY_BATCH_SIZE); |
| | | restored.addAll(page.records().stream().map(item -> new SimBtsHistory(item.sequenceId(), |
| | | item.deviceTime(), item.currentA(), item.voltageV(), item.capacityAh(), item.energyWh())).toList()); |
| | | restored.addAll(page.records().stream().map(this::normalizeHistoryRecord).toList()); |
| | | if (!page.hasMore()) break; |
| | | if (page.nextOffset() <= offset) throw new IllegalStateException("Invalid BTS history page"); |
| | | offset = page.nextOffset(); |
| | |
| | | } |
| | | } |
| | | |
| | | private record SimBtsHistory(long sequence, Instant at, double current, double voltage, double capacity, double energy) { } |
| | | private List<BtsHistoryRecord> btsHistoryFor(String key) |
| | | { |
| | | return btsHistory.getOrDefault(key, List.of()); |
| | | } |
| | | |
| | | private BtsHistoryRecord normalizeHistoryRecord(BtsHistoryRecord item) |
| | | { |
| | | return item.stepName() == null || item.stepName().isBlank() |
| | | ? new BtsHistoryRecord(item.sequenceId(), item.deviceTime(), item.currentA(), item.voltageV(), |
| | | item.capacityAh(), item.energyWh()) : item; |
| | | } |
| | | |
| | | private String btsKey(DeviceState device, int channelId) |
| | | { |
| | | BtsDeviceProfile profile = profileFor(device); |
| | | return historyKey(profile.devtype(), device.deviceId(), profile.subDeviceId(), channelId); |
| | | } |
| | | |
| | | private void requireChannel(DeviceState device, int channelId) |
| | | { |
| | | if (channelId < 1 || channelId > device.channels().size()) |
| | | throw new IllegalArgumentException("channelId must be between 1 and " + device.channels().size()); |
| | | } |
| | | |
| | | private BtsPlaybackState playbackState(String key, Instant now) |
| | | { |
| | | BtsPlaybackState stored = stateStore.loadBtsPlaybackState(key); |
| | | if (stored != null) return stored; |
| | | List<BtsHistoryRecord> records = btsHistoryFor(key); |
| | | long next = records.stream().mapToLong(BtsHistoryRecord::sequenceId).max().orElse(0L) + 1L; |
| | | BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1); |
| | | BtsPlaybackState created = new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(), |
| | | now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, next, false); |
| | | stateStore.saveBtsPlaybackState(key, created); |
| | | return created; |
| | | } |
| | | |
| | | private BtsHistoryRecord latestRecord(String key) |
| | | { |
| | | List<BtsHistoryRecord> records = btsHistoryFor(key); |
| | | return records.isEmpty() ? null : records.get(records.size() - 1); |
| | | } |
| | | |
| | | private double latestCapacity(String key) |
| | | { |
| | | BtsHistoryRecord latest = latestRecord(key); |
| | | return latest == null ? 0d : latest.capacityAh(); |
| | | } |
| | | |
| | | private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current, |
| | | double currentA, double voltageV) |
| | | { |
| | | return advanceBtsPlayback(key, profile, current, currentA, voltageV, Instant.now()); |
| | | } |
| | | |
| | | private BtsPlaybackState advanceBtsPlayback(String key, BtsDeviceProfile profile, BtsPlaybackState current, |
| | | double currentA, double voltageV, Instant now) |
| | | { |
| | | if (!current.playing() || current.naturallyFinished()) return current; |
| | | long elapsedMillis = Math.max(0L, java.time.Duration.between(current.wallClockAt(), now).toMillis()) |
| | | * current.speedMultiplier(); |
| | | List<BtsHistoryRecord> history = new ArrayList<>(btsHistoryFor(key)); |
| | | List<BtsHistoryRecord> appended = new ArrayList<>(); |
| | | Instant simulated = current.simulatedAt(); |
| | | long sequence = current.nextSequence(); |
| | | if (sequence < 1) sequence = 1; |
| | | if (sequence > BtsExcelFixture.recordCount() + 1L) |
| | | sequence = BtsExcelFixture.recordCount() + 1L; |
| | | long lastOffset = sequence == 1 ? -SIMULATED_RECORD_INTERVAL_SECONDS |
| | | : BtsExcelFixture.record(sequence - 1).offsetSeconds(); |
| | | long budgetMillis = elapsedMillis; |
| | | while (sequence <= BtsExcelFixture.recordCount()) |
| | | { |
| | | BtsExcelFixture.FixtureRecord fixture = BtsExcelFixture.record(sequence); |
| | | long deltaSeconds = fixture.offsetSeconds() - lastOffset; |
| | | long deltaMillis = Math.max(1L, deltaSeconds * 1000L); |
| | | boolean firstRecord = history.isEmpty() && sequence == 1L; |
| | | if (!firstRecord && budgetMillis < deltaMillis) break; |
| | | budgetMillis = Math.max(0L, budgetMillis - deltaMillis); |
| | | simulated = simulated.plusSeconds(deltaSeconds); |
| | | BtsHistoryRecord record = new BtsHistoryRecord(fixture.sequenceId(), simulated, fixture.currentA(), fixture.voltageV(), |
| | | fixture.capacityAh(), fixture.energyWh(), fixture.cycleNo(), fixture.stepNo(), fixture.stepName(), fixture.stepType()); |
| | | history.add(record); |
| | | appended.add(record); |
| | | lastOffset = fixture.offsetSeconds(); |
| | | sequence++; |
| | | } |
| | | if (appended.isEmpty()) return current; |
| | | if (history.size() > BtsSimulatorStateStore.MAX_HISTORY_RECORDS) |
| | | history = new ArrayList<>(history.subList(history.size() - (int) BtsSimulatorStateStore.MAX_HISTORY_RECORDS, |
| | | history.size())); |
| | | BtsExcelFixture.FixtureRecord latest = BtsExcelFixture.record(sequence - 1L); |
| | | boolean finished = sequence > BtsExcelFixture.recordCount(); |
| | | BtsPlaybackState next = new BtsPlaybackState(!finished && current.playing(), current.speedMultiplier(), |
| | | latest.cycleNo(), latest.stepNo(), latest.stepName(), latest.stepType(), simulated, now, sequence, finished); |
| | | stateStore.saveBtsPlaybackAndAppendHistory(key, next, appended); |
| | | btsHistory.put(key, history); |
| | | return next; |
| | | } |
| | | |
| | | private BtsPlaybackState defaultPlayback(Instant now) |
| | | { |
| | | BtsExcelFixture.FixtureRecord first = BtsExcelFixture.record(1); |
| | | return new BtsPlaybackState(true, 1, first.cycleNo(), first.stepNo(), first.stepName(), first.stepType(), |
| | | now.minusSeconds(SIMULATED_RECORD_INTERVAL_SECONDS), now, 1, false); |
| | | } |
| | | |
| | | private BtsExcelFixture.FixtureRecord nextFixtureStep(int cycleNo, int stepNo) |
| | | { |
| | | return BtsExcelFixture.records().stream() |
| | | .filter(item -> item.cycleNo() > cycleNo || (item.cycleNo() == cycleNo && item.stepNo() > stepNo)) |
| | | .findFirst().orElse(null); |
| | | } |
| | | |
| | | private void savePlayback(String key, BtsPlaybackState playback) |
| | | { |
| | | stateStore.saveBtsPlaybackState(key, playback); |
| | | } |
| | | |
| | | public record BtsStepRecord(int cycleNo, int stepNo, String stepName, String stepType, long recordCount, |
| | | Instant startedAt, Instant endedAt, double capacityAh, double energyWh) { } |
| | | |
| | | public record BtsCycleRecord(int cycleNo, long recordCount, Instant startedAt, Instant endedAt, |
| | | double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh) { } |
| | | |
| | | public record PlaybackSnapshot(int deviceId, int channelId, BtsPlaybackState playback, |
| | | List<BtsHistoryRecord> records, List<BtsStepRecord> steps, List<BtsCycleRecord> cycles) { } |
| | | |
| | | private static final class BtsStepAccumulator |
| | | { |
| | | private final int cycle; private final int step; private final String name; private final String type; |
| | | private long count; private Instant first; private Instant last; private double capacity; private double energy; |
| | | BtsStepAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); step = first.stepNo(); name = first.stepName(); type = first.stepType(); } |
| | | void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); capacity = item.capacityAh(); energy = item.energyWh(); } |
| | | BtsStepRecord finish() { return new BtsStepRecord(cycle, step, name, type, count, first, last, capacity, energy); } |
| | | } |
| | | |
| | | private static final class BtsCycleAccumulator |
| | | { |
| | | private final int cycle; private long count; private Instant first; private Instant last; |
| | | private double maximumCapacity = Double.NEGATIVE_INFINITY; private double minimumCapacity = Double.POSITIVE_INFINITY; |
| | | private double chargeEnergy; private double dischargeEnergy; |
| | | BtsCycleAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); } |
| | | void add(BtsHistoryRecord item) { count++; if (first == null) first = item.deviceTime(); last = item.deviceTime(); maximumCapacity = Math.max(maximumCapacity, item.capacityAh()); minimumCapacity = Math.min(minimumCapacity, item.capacityAh()); if (item.currentA() >= 0) chargeEnergy = Math.max(chargeEnergy, item.energyWh()); else dischargeEnergy = Math.max(dischargeEnergy, Math.abs(item.energyWh())); } |
| | | BtsCycleRecord finish() { return new BtsCycleRecord(cycle, count, first, last, Math.max(0d, maximumCapacity), Math.max(0d, maximumCapacity - minimumCapacity), chargeEnergy, dischargeEnergy); } |
| | | } |
| | | |
| | | private java.util.Set<String> requestedChannels(String request) |
| | | { |