| | |
| | | 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; |
| | |
| | | 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 |
| | |
| | | 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"); |
| | | |
| | |
| | | "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); |
| | | } |
| | |
| | | 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) |
| | | { |
| | |
| | | } |
| | | } |
| | | |
| | | 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"); |