diff --git a/packages/external-provider-contract/providers/gelios/CAPABILITY_CATALOG.md b/packages/external-provider-contract/providers/gelios/CAPABILITY_CATALOG.md index fa79fbd..b907c14 100644 --- a/packages/external-provider-contract/providers/gelios/CAPABILITY_CATALOG.md +++ b/packages/external-provider-contract/providers/gelios/CAPABILITY_CATALOG.md @@ -1,9 +1,15 @@ # Gelios capability catalog -`capability-catalog-v1.mjs` is the complete classified inventory for the +`capability-catalog-v1.mjs` is the immutable baseline classified inventory for the current `UnitModelOutput` surface and the adjacent object APIs that NODE.DC may consume. Its authority is the official Gelios OpenAPI, checked on 2026-07-22. +`capability-catalog-v2.mjs` preserves that inventory and records the first cold +profile implementation: stable unit type, hardware classification and trip +settings in `fleet.units.profile.current.v1`. Sensor definitions, fuel, +storage and maintenance remain explicitly planned rather than being implied by +the new product. + The catalog is deliberately separate from an active L2 package. `implemented` means a capability already has a governed collection and Data Product path; `catalogued` means NODE.DC understands the surface but has not activated it; diff --git a/packages/external-provider-contract/providers/gelios/capability-catalog-v2.mjs b/packages/external-provider-contract/providers/gelios/capability-catalog-v2.mjs new file mode 100644 index 0000000..e64a852 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/capability-catalog-v2.mjs @@ -0,0 +1,51 @@ +import { geliosCapabilityCatalogV1 } from "./capability-catalog-v1.mjs"; + +export const geliosCapabilityCatalogV2 = deepFreeze(updateFamilies({ + ...structuredClone(geliosCapabilityCatalogV1), + version: "2.0.0", + authority: { + ...geliosCapabilityCatalogV1.authority, + checkedAt: "2026-07-22T20:32:27Z", + }, +})); + +function updateFamilies(value) { + value.dataFamilies = value.dataFamilies.map((family) => { + if (family.id === "gelios.unit.identity") { + return implemented(family); + } + if (family.id === "gelios.unit.equipment") { + return implemented({ + ...family, + sourcePaths: [ + "hwManufacturer.id", + "hwManufacturer.name", + "hwType.id", + "hwType.name", + "hwType.port", + "hwType.type", + ], + }); + } + if (family.id === "gelios.unit.trip_settings") { + return implemented(family); + } + return family; + }); + return value; +} + +function implemented(family) { + return { + ...family, + status: "implemented", + dataProductIds: ["fleet.units.profile.current.v1"], + }; +} + +function deepFreeze(input) { + if (!input || typeof input !== "object" || Object.isFrozen(input)) return input; + Object.freeze(input); + for (const child of Object.values(input)) deepFreeze(child); + return input; +} diff --git a/packages/external-provider-contract/providers/gelios/index.mjs b/packages/external-provider-contract/providers/gelios/index.mjs index 6f1a7b2..342dc86 100644 --- a/packages/external-provider-contract/providers/gelios/index.mjs +++ b/packages/external-provider-contract/providers/gelios/index.mjs @@ -1,2 +1,4 @@ export { geliosCapabilityCatalogV1 } from "./capability-catalog-v1.mjs"; -export * from "./v7/index.mjs"; +export { geliosCapabilityCatalogV2 } from "./capability-catalog-v2.mjs"; +export { geliosProviderPackageV7 } from "./v7/index.mjs"; +export * from "./v8/index.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v8/README.md b/packages/external-provider-contract/providers/gelios/v8/README.md new file mode 100644 index 0000000..97caf31 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v8/README.md @@ -0,0 +1,11 @@ +# Gelios provider package v8 + +Version 8 preserves every v7 capability and product, and adds the immutable +`fleet.units.profile.current.v1@1.0.0` cold-profile contract. + +The profile uses the same credential-visible `/api/v1/units` authority as the +accepted position flow, but has its own six-hour collection profile and Data +Product lifecycle. It publishes only provider-neutral equipment classification +and trip-detection settings keyed by the existing `gelios-unit-*` source id. +IMEI, phones, decrypt keys, raw message parameters, provider access metadata, +conversion tables and unclassified dynamic values remain excluded. diff --git a/packages/external-provider-contract/providers/gelios/v8/index.mjs b/packages/external-provider-contract/providers/gelios/v8/index.mjs new file mode 100644 index 0000000..b2ad6e2 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v8/index.mjs @@ -0,0 +1,8 @@ +export { + GELIOS_PROVIDER_PACKAGE_ID, + GELIOS_PROVIDER_PACKAGE_VERSION, + GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + GELIOS_UNIT_PROFILE_DATA_PRODUCT_VERSION, + GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION, + geliosProviderPackageV8, +} from "./package.mjs"; diff --git a/packages/external-provider-contract/providers/gelios/v8/package.mjs b/packages/external-provider-contract/providers/gelios/v8/package.mjs new file mode 100644 index 0000000..483ea35 --- /dev/null +++ b/packages/external-provider-contract/providers/gelios/v8/package.mjs @@ -0,0 +1,278 @@ +import { geliosProviderPackageV7 } from "../v7/package.mjs"; + +export const GELIOS_PROVIDER_PACKAGE_ID = "gelios.provider.v8"; +export const GELIOS_PROVIDER_PACKAGE_VERSION = "8.0.0"; +export const GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID = "fleet.units.profile.current.v1"; +export const GELIOS_UNIT_PROFILE_DATA_PRODUCT_VERSION = "1.0.0"; +export const GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION = "ontology.map.moving_object.v3"; + +const FIELD_POLICY_ID = "gelios.units.profile.fields.v1"; +const COLLECTION_PROFILE_ID = "gelios.units.profile.cold.v1"; +const MAPPING_ID = "gelios.units.to.fleet.units.profile.current.v1"; +const TEMPLATE_ID = "gelios.units.profile.l2.v1"; + +const profileFields = Object.freeze([ + "corrected_engine_hours_factor", + "corrected_mileage_factor", + "display_name", + "filter_by_satellite_count_enabled", + "filter_by_satellite_count_value", + "filter_emissions", + "hardware_manufacturer_name", + "hardware_port", + "hardware_type_class", + "hardware_type_name", + "limit_acceleration", + "lost_connection_enabled", + "lost_connection_time_value", + "maximum_permissible_speed", + "maximum_valid_height", + "maximum_valid_speed", + "mileage_by_ignition", + "minimum_movement_speed", + "minimum_movement_time", + "minimum_parking_time", + "minimum_stop_time", + "minimum_trip_distance", + "minimum_valid_height", + "speed_parameter", + "trip_detection_type", + "unit_type_class", + "unit_type_name", + "use_odometer", +]); + +const fieldContracts = Object.freeze({ + corrected_engine_hours_factor: optional("number"), + corrected_mileage_factor: optional("number"), + display_name: { type: "string", required: true }, + filter_by_satellite_count_enabled: optional("boolean"), + filter_by_satellite_count_value: nonNegative(), + filter_emissions: nonNegative(), + hardware_manufacturer_name: optional("string"), + hardware_port: nonNegative(), + hardware_type_class: optional("string"), + hardware_type_name: optional("string"), + limit_acceleration: nonNegative(), + lost_connection_enabled: optional("boolean"), + lost_connection_time_value: nonNegative(), + maximum_permissible_speed: nonNegative(), + maximum_valid_height: optional("number"), + maximum_valid_speed: nonNegative(), + mileage_by_ignition: optional("boolean"), + minimum_movement_speed: nonNegative(), + minimum_movement_time: nonNegative(), + minimum_parking_time: nonNegative(), + minimum_stop_time: nonNegative(), + minimum_trip_distance: nonNegative(), + minimum_valid_height: optional("number"), + speed_parameter: optional("string"), + trip_detection_type: optional("string"), + unit_type_class: optional("string"), + unit_type_name: optional("string"), + use_odometer: optional("boolean"), +}); + +const value = structuredClone(geliosProviderPackageV7); +value.id = GELIOS_PROVIDER_PACKAGE_ID; +value.version = GELIOS_PROVIDER_PACKAGE_VERSION; +value.manifest = { + ...value.manifest, + id: "gelios.provider.manifest.v8", + version: GELIOS_PROVIDER_PACKAGE_VERSION, + fieldPolicyIds: [...value.manifest.fieldPolicyIds, FIELD_POLICY_ID], + collectionProfileIds: [...value.manifest.collectionProfileIds, COLLECTION_PROFILE_ID], + dataProductIds: [...value.manifest.dataProductIds, GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID], + mappingContractIds: [...value.manifest.mappingContractIds, MAPPING_ID], + l2TemplateIds: [...value.manifest.l2TemplateIds, TEMPLATE_ID], +}; + +value.fieldPolicies.push({ + id: FIELD_POLICY_ID, + version: GELIOS_UNIT_PROFILE_DATA_PRODUCT_VERSION, + dataProductId: GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + targetFields: [...profileFields], + unknownSourceFields: "drop", + dynamicSourceFields: "drop_until_classified", + restrictedSourcePaths: [ + "availableToUsers", + "commands", + "creator", + "currentUserAccess", + "customFields", + "driver", + "extraInfo.autocompleteParams", + "extraInfo.numberPlate", + "extraInfo.vin", + "hwDecryptKey", + "imei", + "lastMsg.address", + "lastMsg.params", + "lastSensorsVal", + "phone", + "phone2", + "preSetCommandGroup", + "sensors", + "stationaryLat", + "stationaryLon", + ], +}); + +value.collectionProfiles.push({ + id: COLLECTION_PROFILE_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + mode: "realtime", + schedule: { intervalMs: 6 * 60 * 60 * 1000 }, + capabilityIds: ["gelios.units.current.read"], + dataProductId: GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + mappingContractId: MAPPING_ID, + fieldPolicyId: FIELD_POLICY_ID, + l2TemplateId: TEMPLATE_ID, + entityScope: { + mode: "all_visible_to_credential", + refresh: "each_collection_run", + businessEntityFilter: "forbidden", + }, + batching: { maxFacts: 5000 }, + cardinality: { + maxCurrentEntities: 5000, + onExceed: "require_partitioned_data_product", + }, + retry: { maxAttempts: 4, backoff: "exponential_with_jitter" }, +}); + +value.dataProducts.push({ + id: GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + version: GELIOS_UNIT_PROFILE_DATA_PRODUCT_VERSION, + ontologyRevision: GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION, + deliveryMode: "snapshot+patch", + semanticTypes: ["map.moving_object"], + fields: [...profileFields], + fieldContracts, + history: { mode: "none", retentionDays: 1 }, +}); + +value.mappingContracts.push({ + schemaVersion: "nodedc.semantic-mapping/v1", + id: MAPPING_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + sourceCapabilityId: "gelios.units.current.read", + fieldPolicyId: FIELD_POLICY_ID, + target: { + dataProductId: GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + version: GELIOS_UNIT_PROFILE_DATA_PRODUCT_VERSION, + ontologyRevision: GELIOS_UNIT_PROFILE_ONTOLOGY_REVISION, + semanticType: "map.moving_object", + }, + derivations: {}, + fact: { + sourceId: { + strategy: "first_non_empty", + paths: ["id", "unit_id", "unitId"], + coerce: "string", + prefix: "gelios-unit-", + }, + semanticType: { constant: "map.moving_object" }, + observedAt: { + strategy: "first_non_empty", + paths: ["updatedAt"], + coerce: "unix_or_iso_timestamp", + fallback: "collection_received_at", + }, + attributes: { + corrected_engine_hours_factor: numberField(["tripParams.correctedEngineHoursFactor"]), + corrected_mileage_factor: numberField(["tripParams.correctedMileageFactor"]), + display_name: stringField(["name", "unit_name", "title", "label"], false), + filter_by_satellite_count_enabled: booleanField(["tripParams.filterBySatellitesCountIsOn"]), + filter_by_satellite_count_value: nonNegativeField(["tripParams.filterBySatellitesCountValue"]), + filter_emissions: nonNegativeField(["tripParams.filterEmissions"]), + hardware_manufacturer_name: stringField(["hwManufacturer.name"]), + hardware_port: nonNegativeField(["hwType.port"]), + hardware_type_class: stringField(["hwType.type"]), + hardware_type_name: stringField(["hwType.name"]), + limit_acceleration: nonNegativeField(["tripParams.limitAcceleration"]), + lost_connection_enabled: booleanField(["tripParams.lostConnectionIsOn"]), + lost_connection_time_value: nonNegativeField(["tripParams.lostConnectionTimeValue"]), + maximum_permissible_speed: nonNegativeField(["tripParams.maximumPermissibleSpeed"]), + maximum_valid_height: numberField(["tripParams.maximumValidHeight"]), + maximum_valid_speed: nonNegativeField(["tripParams.maximumValidSpeed"]), + mileage_by_ignition: booleanField(["tripParams.mileageByIgnition"]), + minimum_movement_speed: nonNegativeField(["tripParams.minimumMovementSpeed"]), + minimum_movement_time: nonNegativeField(["tripParams.minimumMovementTime"]), + minimum_parking_time: nonNegativeField(["tripParams.minimumParkingTime"]), + minimum_stop_time: nonNegativeField(["tripParams.minimumTimeStop"]), + minimum_trip_distance: nonNegativeField(["tripParams.minimumTripDistance"]), + minimum_valid_height: numberField(["tripParams.minimumValidHeight"]), + speed_parameter: stringField(["tripParams.speedParam"]), + trip_detection_type: stringField(["tripParams.tripDetectionType.name"]), + unit_type_class: stringField(["unitType.type"]), + unit_type_name: stringField(["unitType.name"]), + use_odometer: booleanField(["tripParams.useOdometer"]), + }, + }, +}); + +const positionsTemplate = value.l2Templates.find((template) => template.id === "gelios.positions.current.l2.v7"); +if (!positionsTemplate) throw new Error("gelios_v8_positions_template_missing"); +value.l2Templates.push({ + ...structuredClone(positionsTemplate), + id: TEMPLATE_ID, + version: GELIOS_PROVIDER_PACKAGE_VERSION, + steps: [ + { id: "collection.trigger", kind: "collection_trigger", collectionProfileDriven: true }, + { id: "provider.fetch-units", kind: "provider_request", capabilityId: "gelios.units.current.read" }, + { id: "provider.extract-units", kind: "extract_items", capabilityId: "gelios.units.current.read" }, + { id: "ontology.map", kind: "semantic_mapping", mappingContractId: MAPPING_ID }, + { + id: "data-product.publish", + kind: "data_product_publish", + dataProductId: GELIOS_UNIT_PROFILE_DATA_PRODUCT_ID, + nodeType: "n8n-nodes-ndc.ndcDataProductPublish", + }, + ], +}); + +export const geliosProviderPackageV8 = deepFreeze(value); + +function optional(type) { + return { type, required: false }; +} + +function nonNegative() { + return { type: "number", required: false, minimum: 0 }; +} + +function stringField(paths, omitIfMissing = true) { + return { + strategy: "first_non_empty", + paths, + coerce: "string", + ...(omitIfMissing ? { omitIfMissing: true } : {}), + }; +} + +function numberField(paths) { + return { strategy: "first_non_empty", paths, coerce: "number", omitIfMissing: true }; +} + +function nonNegativeField(paths) { + return { + strategy: "first_non_empty", + paths, + coerce: "number", + minimum: 0, + omitIfMissing: true, + omitIfInvalid: true, + }; +} + +function booleanField(paths) { + return { strategy: "first_non_empty", paths, coerce: "boolean", omitIfMissing: true }; +} + +function deepFreeze(input) { + if (!input || typeof input !== "object" || Object.isFrozen(input)) return input; + Object.freeze(input); + for (const child of Object.values(input)) deepFreeze(child); + return input; +} diff --git a/packages/external-provider-contract/test/provider-capability-catalog.test.mjs b/packages/external-provider-contract/test/provider-capability-catalog.test.mjs index 9538e5e..8601c88 100644 --- a/packages/external-provider-contract/test/provider-capability-catalog.test.mjs +++ b/packages/external-provider-contract/test/provider-capability-catalog.test.mjs @@ -1,8 +1,13 @@ import assert from "node:assert/strict"; import { validateProviderCapabilityCatalog } from "../src/index.mjs"; -import { geliosCapabilityCatalogV1, geliosProviderPackageV7 } from "../providers/gelios/index.mjs"; +import { + geliosCapabilityCatalogV1, + geliosCapabilityCatalogV2, + geliosProviderPackageV8, +} from "../providers/gelios/index.mjs"; assert.deepEqual(validateProviderCapabilityCatalog(geliosCapabilityCatalogV1), { ok: true, errors: [] }); +assert.deepEqual(validateProviderCapabilityCatalog(geliosCapabilityCatalogV2), { ok: true, errors: [] }); const expectedUnitRoots = [ "activatedAt", "availableToUsers", "blockTime", "commands", "counters", "createdAt", @@ -20,10 +25,10 @@ const expectedIncludeFlags = [ ]; assert.deepEqual(geliosCapabilityCatalogV1.queryFlags.map(({ id }) => id), expectedIncludeFlags); -const unitCapability = geliosProviderPackageV7.capabilities.find(({ id }) => id === "gelios.units.current.read"); +const unitCapability = geliosProviderPackageV8.capabilities.find(({ id }) => id === "gelios.units.current.read"); assert.ok(unitCapability); for (const enabledFlag of Object.keys(unitCapability.request.query)) { - assert.ok(expectedIncludeFlags.includes(enabledFlag), `v7 query flag is missing from the capability catalog: ${enabledFlag}`); + assert.ok(expectedIncludeFlags.includes(enabledFlag), `provider query flag is missing from the capability catalog: ${enabledFlag}`); } assert.deepEqual( @@ -34,6 +39,21 @@ assert.equal( geliosCapabilityCatalogV1.capabilities.filter(({ classification }) => classification === "control").every(({ status }) => status === "disabled"), true, ); +assert.deepEqual( + ["gelios.unit.identity", "gelios.unit.equipment", "gelios.unit.trip_settings"].map((id) => { + const family = geliosCapabilityCatalogV2.dataFamilies.find((item) => item.id === id); + return { id, status: family.status, dataProductIds: family.dataProductIds }; + }), + [ + { id: "gelios.unit.identity", status: "implemented", dataProductIds: ["fleet.units.profile.current.v1"] }, + { id: "gelios.unit.equipment", status: "implemented", dataProductIds: ["fleet.units.profile.current.v1"] }, + { id: "gelios.unit.trip_settings", status: "implemented", dataProductIds: ["fleet.units.profile.current.v1"] }, + ], +); +assert.equal( + geliosCapabilityCatalogV2.dataFamilies.find(({ id }) => id === "gelios.unit.sensor_definitions").status, + "planned", +); const restrictedPublish = structuredClone(geliosCapabilityCatalogV1); restrictedPublish.dataFamilies.find(({ id }) => id === "gelios.unit.device_identifiers").disposition = "publish"; diff --git a/packages/external-provider-contract/test/provider-package.test.mjs b/packages/external-provider-contract/test/provider-package.test.mjs index deadbe6..44ce484 100644 --- a/packages/external-provider-contract/test/provider-package.test.mjs +++ b/packages/external-provider-contract/test/provider-package.test.mjs @@ -20,6 +20,7 @@ import { geliosProviderPackageV4 } from "../providers/gelios/v4/index.mjs"; import { geliosProviderPackageV5 } from "../providers/gelios/v5/index.mjs"; import { geliosProviderPackageV6 } from "../providers/gelios/v6/index.mjs"; import { geliosProviderPackageV7 } from "../providers/gelios/v7/index.mjs"; +import { geliosProviderPackageV8 } from "../providers/gelios/v8/index.mjs"; import { normalizeDataProductDefinition } from "../../../services/external-data-plane/src/data-product-policy.mjs"; const expectedFields = [ @@ -45,6 +46,24 @@ assert.deepEqual(validateProviderPackage(geliosProviderPackageV4), { ok: true, e assert.deepEqual(validateProviderPackage(geliosProviderPackageV5), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV6), { ok: true, errors: [] }); assert.deepEqual(validateProviderPackage(geliosProviderPackageV7), { ok: true, errors: [] }); +assert.deepEqual(validateProviderPackage(geliosProviderPackageV8), { ok: true, errors: [] }); + +const profileProduct = geliosProviderPackageV8.dataProducts.find((product) => product.id === "fleet.units.profile.current.v1"); +const registeredProfileProduct = JSON.parse(await readFile(new URL( + "../../../services/external-data-plane/definitions/fleet.units.profile.current.v1.json", + import.meta.url, +), "utf8")); +assert.deepEqual(profileProduct, registeredProfileProduct); +assert.deepEqual(profileProduct.semanticTypes, ["map.moving_object"]); +assert.equal(profileProduct.history.mode, "none"); +const profileCollection = geliosProviderPackageV8.collectionProfiles.find((profile) => profile.id === "gelios.units.profile.cold.v1"); +assert.equal(profileCollection.schedule.intervalMs, 6 * 60 * 60 * 1000); +assert.deepEqual(profileCollection.capabilityIds, ["gelios.units.current.read"]); +const profileMapping = geliosProviderPackageV8.mappingContracts.find((mapping) => mapping.id === "gelios.units.to.fleet.units.profile.current.v1"); +assert.equal(profileMapping.fact.sourceId.prefix, "gelios-unit-"); +assert.equal(profileMapping.fact.attributes.hardware_type_name.paths[0], "hwType.name"); +assert.equal(profileMapping.fact.attributes.trip_detection_type.paths[0], "tripParams.tripDetectionType.name"); +assert.equal(profileMapping.fact.attributes.use_odometer.coerce, "boolean"); const telemetryProduct = geliosProviderPackageV7.dataProducts.find((product) => product.id === "fleet.positions.current.v5"); const registeredTelemetryProduct = JSON.parse(await readFile(new URL( diff --git a/services/external-data-plane/definitions/fleet.units.profile.current.v1.json b/services/external-data-plane/definitions/fleet.units.profile.current.v1.json new file mode 100644 index 0000000..59a0df9 --- /dev/null +++ b/services/external-data-plane/definitions/fleet.units.profile.current.v1.json @@ -0,0 +1,73 @@ +{ + "id": "fleet.units.profile.current.v1", + "version": "1.0.0", + "ontologyRevision": "ontology.map.moving_object.v3", + "deliveryMode": "snapshot+patch", + "semanticTypes": [ + "map.moving_object" + ], + "fields": [ + "corrected_engine_hours_factor", + "corrected_mileage_factor", + "display_name", + "filter_by_satellite_count_enabled", + "filter_by_satellite_count_value", + "filter_emissions", + "hardware_manufacturer_name", + "hardware_port", + "hardware_type_class", + "hardware_type_name", + "limit_acceleration", + "lost_connection_enabled", + "lost_connection_time_value", + "maximum_permissible_speed", + "maximum_valid_height", + "maximum_valid_speed", + "mileage_by_ignition", + "minimum_movement_speed", + "minimum_movement_time", + "minimum_parking_time", + "minimum_stop_time", + "minimum_trip_distance", + "minimum_valid_height", + "speed_parameter", + "trip_detection_type", + "unit_type_class", + "unit_type_name", + "use_odometer" + ], + "fieldContracts": { + "corrected_engine_hours_factor": { "type": "number", "required": false }, + "corrected_mileage_factor": { "type": "number", "required": false }, + "display_name": { "type": "string", "required": true }, + "filter_by_satellite_count_enabled": { "type": "boolean", "required": false }, + "filter_by_satellite_count_value": { "type": "number", "required": false, "minimum": 0 }, + "filter_emissions": { "type": "number", "required": false, "minimum": 0 }, + "hardware_manufacturer_name": { "type": "string", "required": false }, + "hardware_port": { "type": "number", "required": false, "minimum": 0 }, + "hardware_type_class": { "type": "string", "required": false }, + "hardware_type_name": { "type": "string", "required": false }, + "limit_acceleration": { "type": "number", "required": false, "minimum": 0 }, + "lost_connection_enabled": { "type": "boolean", "required": false }, + "lost_connection_time_value": { "type": "number", "required": false, "minimum": 0 }, + "maximum_permissible_speed": { "type": "number", "required": false, "minimum": 0 }, + "maximum_valid_height": { "type": "number", "required": false }, + "maximum_valid_speed": { "type": "number", "required": false, "minimum": 0 }, + "mileage_by_ignition": { "type": "boolean", "required": false }, + "minimum_movement_speed": { "type": "number", "required": false, "minimum": 0 }, + "minimum_movement_time": { "type": "number", "required": false, "minimum": 0 }, + "minimum_parking_time": { "type": "number", "required": false, "minimum": 0 }, + "minimum_stop_time": { "type": "number", "required": false, "minimum": 0 }, + "minimum_trip_distance": { "type": "number", "required": false, "minimum": 0 }, + "minimum_valid_height": { "type": "number", "required": false }, + "speed_parameter": { "type": "string", "required": false }, + "trip_detection_type": { "type": "string", "required": false }, + "unit_type_class": { "type": "string", "required": false }, + "unit_type_name": { "type": "string", "required": false }, + "use_odometer": { "type": "boolean", "required": false } + }, + "history": { + "mode": "none", + "retentionDays": 1 + } +} diff --git a/services/external-data-plane/test/definitions.test.mjs b/services/external-data-plane/test/definitions.test.mjs index 2696977..b56edd5 100644 --- a/services/external-data-plane/test/definitions.test.mjs +++ b/services/external-data-plane/test/definitions.test.mjs @@ -12,6 +12,7 @@ assert.deepEqual(bundled.map((definition) => definition.id), [ "fleet.positions.current.v3", "fleet.positions.current.v4", "fleet.positions.current.v5", + "fleet.units.profile.current.v1", "map.zones.current.v1", "map.zones.current.v2", ]); @@ -52,19 +53,25 @@ assert.equal(bundled[4].version, "5.0.0"); assert.equal(bundled[4].ontologyRevision, "ontology.map.moving_object.v3"); assert.equal(bundled[4].fieldContracts.sensor_readings.type, "telemetry_readings"); assert.equal(bundled[5].version, "1.0.0"); -assert.equal(bundled[5].ontologyRevision, "ontology.map.zone.v1"); -assert.deepEqual(bundled[5].semanticTypes, ["map.zone"]); -assert.deepEqual(bundled[5].fieldContracts.geometry, { type: "geometry", required: true }); -assert.equal(bundled[5].fields.includes("dataset_authority"), false); -assert.equal(bundled[6].version, "2.0.0"); +assert.equal(bundled[5].ontologyRevision, "ontology.map.moving_object.v3"); +assert.deepEqual(bundled[5].semanticTypes, ["map.moving_object"]); +assert.equal(bundled[5].history.mode, "none"); +assert.equal(bundled[5].fieldContracts.hardware_port.type, "number"); +assert.equal(bundled[6].version, "1.0.0"); assert.equal(bundled[6].ontologyRevision, "ontology.map.zone.v1"); assert.deepEqual(bundled[6].semanticTypes, ["map.zone"]); assert.deepEqual(bundled[6].fieldContracts.geometry, { type: "geometry", required: true }); -assert.deepEqual(bundled[6].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]); +assert.equal(bundled[6].fields.includes("dataset_authority"), false); +assert.equal(bundled[7].version, "2.0.0"); +assert.equal(bundled[7].ontologyRevision, "ontology.map.zone.v1"); +assert.deepEqual(bundled[7].semanticTypes, ["map.zone"]); +assert.deepEqual(bundled[7].fieldContracts.geometry, { type: "geometry", required: true }); +assert.deepEqual(bundled[7].fieldContracts.dataset_authority.enum, ["moscow-department-of-transport"]); assert.equal(Object.keys(bundled[3].fieldContracts).length, bundled[3].fields.length); assert.equal(Object.keys(bundled[4].fieldContracts).length, bundled[4].fields.length); assert.equal(Object.keys(bundled[5].fieldContracts).length, bundled[5].fields.length); assert.equal(Object.keys(bundled[6].fieldContracts).length, bundled[6].fields.length); +assert.equal(Object.keys(bundled[7].fieldContracts).length, bundled[7].fields.length); const directory = await mkdtemp(join(tmpdir(), "nodedc-edp-definitions-")); try {