package com.battery.device.model; import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.LinkedHashMap; /** 厂商无关的一次通道观测结果。不得携带测试、持久化或传输层对象。 */ public record DeviceObservation( DeviceIdentity device, ChannelIdentity channel, DeviceConnectionStatus connectionStatus, DeviceWorkStatus channelWorkStatus, List plannedSteps, Instant deviceTime, Map measurements, ObservationFailureReason failureReason) { public DeviceObservation { device = Objects.requireNonNull(device, "device"); channel = Objects.requireNonNull(channel, "channel"); if (!device.equals(DeviceIdentity.from(channel))) throw new IllegalArgumentException("device and channel identities do not match"); connectionStatus = Objects.requireNonNull(connectionStatus, "connectionStatus"); channelWorkStatus = channelWorkStatus == null ? DeviceWorkStatus.UNOBSERVABLE : channelWorkStatus; plannedSteps = plannedSteps == null ? List.of() : List.copyOf(plannedSteps); measurements = measurements == null ? Map.of() : Map.copyOf(measurements); failureReason = failureReason; if (connectionStatus == DeviceConnectionStatus.ONLINE && failureReason != null) throw new IllegalArgumentException("online observation cannot contain a failure reason"); } /** 将已有实时快照提升为统一观测模型,兼容旧适配器实现。 */ public static DeviceObservation from(RealtimeSnapshot snapshot) { Objects.requireNonNull(snapshot, "snapshot"); DeviceConnectionStatus status = snapshot.connectionStatus() == null ? DeviceConnectionStatus.ONLINE : snapshot.connectionStatus(); if (status == DeviceConnectionStatus.OFFLINE) return offline(snapshot.channel(), snapshot.failureReason() == null ? "OFFLINE" : snapshot.failureReason()); Map measurements = new LinkedHashMap<>(snapshot.measurements()); addScalar(measurements, "current", snapshot.currentA(), "A"); addScalar(measurements, "voltage", snapshot.voltageV(), "V"); addScalar(measurements, "capacity", snapshot.capacityAh(), "Ah"); addScalar(measurements, "energy", snapshot.energyWh(), "Wh"); return new DeviceObservation(DeviceIdentity.from(snapshot.channel()), snapshot.channel(), status, snapshot.workStatus(), snapshot.plannedSteps(), snapshot.deviceTime(), measurements, parseFailureReason(snapshot.failureReason())); } private static void addScalar(Map measurements, String code, java.math.BigDecimal value, String unit) { if (value != null) measurements.putIfAbsent(code, new Measurement(code, value, null, unit, "DEVICE")); } public boolean online() { return connectionStatus == DeviceConnectionStatus.ONLINE; } /** 兼容调用方以通道工作状态命名的读取方式。 */ public DeviceWorkStatus workStatus() { return channelWorkStatus; } public static DeviceObservation offline(ChannelIdentity channel, String failureReason) { Objects.requireNonNull(channel, "channel"); ObservationFailureReason reason = parseFailureReason(failureReason); return new DeviceObservation(DeviceIdentity.from(channel), channel, DeviceConnectionStatus.OFFLINE, DeviceWorkStatus.UNOBSERVABLE, List.of(), null, Map.of(), reason == null ? ObservationFailureReason.OFFLINE : reason); } public static DeviceObservation offline(ChannelIdentity channel, ObservationFailureReason failureReason) { Objects.requireNonNull(channel, "channel"); return new DeviceObservation(DeviceIdentity.from(channel), channel, DeviceConnectionStatus.OFFLINE, DeviceWorkStatus.UNOBSERVABLE, List.of(), null, Map.of(), failureReason == null ? ObservationFailureReason.OFFLINE : failureReason); } private static ObservationFailureReason parseFailureReason(String value) { if (value == null || value.isBlank()) return null; try { return ObservationFailureReason.valueOf(value.trim().toUpperCase(java.util.Locale.ROOT)); } catch (IllegalArgumentException ignored) { return ObservationFailureReason.INVALID_RESPONSE; } } }