yeg
2026-09-05 bd50f1b756c1da2dbf92f6541407ad40f455e24a
battery-device-bts/src/main/java/com/battery/device/bts/BtsXmlCodec.java
@@ -24,6 +24,7 @@
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import com.battery.device.model.ChannelIdentity;
import com.battery.device.model.DeviceConnectionStatus;
import com.battery.device.model.DetectedChannel;
import com.battery.device.model.DetectedDeviceMetadata;
import com.battery.device.model.DeviceWorkStatus;
@@ -31,6 +32,7 @@
import com.battery.device.model.HistoryPage;
import com.battery.device.model.HistorySample;
import com.battery.device.model.Measurement;
import com.battery.device.model.PlannedStep;
import com.battery.device.model.RealtimeSnapshot;
public final class BtsXmlCodec
@@ -38,7 +40,8 @@
    private static final DateTimeFormatter BTS_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH.mm.ss");
    private static final Set<String> SNAPSHOT_ATTRIBUTES = Set.of(
            "dev", "cycle_id", "step_id", "step_type", "workstatus", "barcode", "current", "voltage",
            "capacity", "energy", "totaltime", "relativetime", "auxtemp", "auxvol", "open_or_close", "log_code");
            "capacity", "energy", "totaltime", "relativetime", "auxtemp", "auxvol", "open_or_close", "log_code",
            "device_time", "deviceTime", "time", "atime", "testtime");
    private static final Set<String> HISTORY_ATTRIBUTES = Set.of(
            "seqid", "stepid", "cycleid", "steptype", "testtime", "atime", "volt", "curr", "cap", "eng");
@@ -136,12 +139,14 @@
                        "Invalid BTS dev identity: " + element.getAttribute("dev"));
            ChannelIdentity identity = new ChannelIdentity(adapterCode, integer(device[0], "devtype"), integer(device[1], "devid"),
                    integer(device[2], "subdevid"), integer(device[3], "chlid"));
            Instant deviceTime = optionalDeviceTime(element);
            snapshots.put(identity, new RealtimeSnapshot(identity,
                    DeviceWorkStatus.fromBts(element.getAttribute("workstatus")), optionalInteger(element, "cycle_id"),
                    optionalInteger(element, "step_id"), optionalAttribute(element, "step_type"), optionalAttribute(element, "barcode"),
                    optionalDecimal(element, "current"), optionalDecimal(element, "voltage"), optionalDecimal(element, "capacity"),
                    optionalDecimal(element, "energy"), optionalLong(element, "totaltime"), optionalLong(element, "relativetime"),
                    optionalAttribute(element, "log_code"), receivedAt, dynamicMeasurements(element, SNAPSHOT_ATTRIBUTES)));
                    optionalAttribute(element, "log_code"), receivedAt, dynamicMeasurements(element, SNAPSHOT_ATTRIBUTES),
                    DeviceConnectionStatus.ONLINE, deviceTime, parsePlannedSteps(element), null));
        }
        return Map.copyOf(snapshots);
    }
@@ -258,7 +263,14 @@
        try
        {
            // BTS 时间不带时区,按适配器配置的现场时区解释后统一转换为 UTC 时间点。
            return LocalDateTime.parse(value, BTS_TIME).atZone(deviceZone).toInstant();
            try
            {
                return Instant.parse(value);
            }
            catch (DateTimeParseException ignored)
            {
                return LocalDateTime.parse(value, BTS_TIME).atZone(deviceZone).toInstant();
            }
        }
        catch (DateTimeParseException ex)
        {
@@ -267,6 +279,65 @@
        }
    }
    private Instant optionalDeviceTime(Element element)
    {
        for (String name : List.of("device_time", "deviceTime", "time", "atime", "testtime"))
        {
            String value = optionalAttribute(element, name);
            if (value != null) return parseDeviceTime(value);
        }
        return null;
    }
    private List<PlannedStep> parsePlannedSteps(Element channel)
    {
        List<PlannedStep> steps = new ArrayList<>();
        NodeList nodes = channel.getElementsByTagName("plannedStep");
        if (nodes.getLength() == 0) nodes = channel.getElementsByTagName("planStep");
        if (nodes.getLength() == 0) nodes = channel.getElementsByTagName("step");
        for (int i = 0; i < nodes.getLength(); i++)
        {
            Element step = (Element) nodes.item(i);
            String number = firstOptionalAttribute(step, "step_no", "stepNo", "step_id", "stepid", "id");
            String type = firstOptionalAttribute(step, "step_type", "stepType", "steptype", "type");
            if (number == null || type == null) continue;
            steps.add(new PlannedStep(integer(number, "planned step number"), type,
                    firstOptionalAttribute(step, "name", "step_name", "stepName"),
                    optionalDecimalFromNames(step, "target_voltage", "targetVoltage", "voltage"),
                    optionalDecimalFromNames(step, "target_current", "targetCurrent", "current"),
                    optionalLongFromNames(step, "duration_seconds", "durationSeconds", "duration")));
        }
        return List.copyOf(steps);
    }
    private static String firstOptionalAttribute(Element element, String... names)
    {
        for (String name : names)
        {
            String value = optionalAttribute(element, name);
            if (value != null) return value;
        }
        return null;
    }
    private static BigDecimal optionalDecimalFromNames(Element element, String... names)
    {
        String value = firstOptionalAttribute(element, names);
        return value == null ? null : decimal(value, names[0]);
    }
    private static Long optionalLongFromNames(Element element, String... names)
    {
        String value = firstOptionalAttribute(element, names);
        if (value == null) return null;
        try { return Long.parseLong(value); }
        catch (NumberFormatException ex)
        {
            throw new BtsProtocolException(BtsProtocolException.Reason.INVALID_VALUE,
                    "Invalid planned step duration: " + value, ex);
        }
    }
    private void requireOwned(ChannelIdentity channel)
    {
        if (!adapterCode.equals(channel.adapterCode())) throw new IllegalArgumentException("Channel belongs to another adapter");