From ee23617b8a5ae1d466f5ccacc6d4f37d0938ec6f Mon Sep 17 00:00:00 2001 From: Florian Pethig Date: Mon, 27 Jul 2026 16:15:40 +0200 Subject: [PATCH 1/2] feat(aas-lib): add Sensor Measurement Value view Co-Authored-By: Oz --- projects/aas-lib/src/lib/types.ts | 1 + .../sensor-measurement-value-validator.ts | 271 ++++ .../sensor-measurement-value-view.html | 29 + .../sensor-measurement-value-view.scss | 0 .../sensor-measurement-value-view.ts | 30 + .../sensor-measurement-value.html | 81 + .../sensor-measurement-value.scss | 0 .../sensor-measurement-value.spec.ts | 173 ++ .../sensor-measurement-value.ts | 102 ++ .../src/lib/views/view-route-config.ts | 10 + .../aas-lib/src/lib/views/views-constants.ts | 1 + projects/aas-lib/src/public-api.ts | 3 + .../assets/sensor-measurement-value-1-0.json | 1409 +++++++++++++++++ .../aas-portal/src/assets/i18n/de-de.json | 21 +- .../aas-portal/src/assets/i18n/en-us.json | 19 +- 15 files changed, 2147 insertions(+), 3 deletions(-) create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.html create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.scss create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.ts create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.html create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.scss create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts create mode 100644 projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.ts create mode 100644 projects/aas-lib/src/test/assets/sensor-measurement-value-1-0.json diff --git a/projects/aas-lib/src/lib/types.ts b/projects/aas-lib/src/lib/types.ts index 004e6e8c6..f853f1d0e 100644 --- a/projects/aas-lib/src/lib/types.ts +++ b/projects/aas-lib/src/lib/types.ts @@ -126,6 +126,7 @@ export type ViewRouteName = | 'OperationalData' | 'Laser' | 'ServiceRequestNotification' + | 'SensorMeasurementValue' | 'TechnicalData'; export type ViewRouteMap = Partial>; diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts new file mode 100644 index 000000000..9dc0064f3 --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts @@ -0,0 +1,271 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2026 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { aas, getChildren, getSemanticId, isProperty, isRange, isSubmodelElementCollection } from 'aas-core'; + +import { SENSOR_MEASUREMENT_VALUE_1_0 } from '../views-constants'; + +export { SENSOR_MEASUREMENT_VALUE_1_0 }; + +type ElementType = 'Property' | 'Range' | 'SubmodelElementCollection'; + +type ElementDefinition = { + path: string; + modelType: ElementType; + semanticId: string; + valueType?: aas.DataTypeDefXsd; + required: boolean; +}; + +const definitions: ElementDefinition[] = [ + { + path: 'MeasuredValue', + modelType: 'SubmodelElementCollection', + semanticId: 'https://admin-shell.io/idta/measurementvalue/measuredvalue/1/0', + required: true, + }, + { + path: 'MeasuredValue.Value', + modelType: 'Property', + valueType: 'xs:float', + semanticId: 'https://admin-shell.io/idta/measurementvalue/value/1/0', + required: true, + }, + { + path: 'MeasuredValue.Unit', + modelType: 'Property', + valueType: 'xs:string', + semanticId: '0112/2///62720#UBA000', + required: true, + }, + { + path: 'MeasuredValue.Kind', + modelType: 'Property', + valueType: 'xs:string', + semanticId: '0112/2///62683#ACI144', + required: true, + }, + { + path: 'MeasuredValuePreDefined', + modelType: 'SubmodelElementCollection', + semanticId: 'https://admin-shell.io/idta/measurementvalue/measuredvaluepredefined/1/0', + required: false, + }, + { + path: 'MeasuredValuePreDefined.Distance', + modelType: 'Property', + valueType: 'xs:float', + semanticId: '0112/2///61987#ABN718#001', + required: true, + }, + { + path: 'MeasurementTimestamp', + modelType: 'Property', + valueType: 'xs:dateTime', + semanticId: '0112/2///61360_7#CBA007', + required: true, + }, + { + path: 'Concept', + modelType: 'SubmodelElementCollection', + semanticId: 'https://admin-shell.io/idta/measurementvalue/concept/1/0', + required: false, + }, + { + path: 'Concept.Origin', + modelType: 'Property', + valueType: 'xs:string', + semanticId: 'https://admin-shell.io/idta/measurementvalue/origin/1/0', + required: false, + }, + { + path: 'Concept.Version', + modelType: 'Property', + valueType: 'xs:string', + semanticId: 'https://admin-shell.io/idta/measurementvalue/version/1/0', + required: false, + }, + { + path: 'Concept.Identifier', + modelType: 'Property', + valueType: 'xs:string', + semanticId: 'https://admin-shell.io/idta/measurementvalue/identifier/1/0', + required: false, + }, + { + path: 'MeasurementQualifier', + modelType: 'SubmodelElementCollection', + semanticId: 'https://admin-shell.io/idta/measurementvalue/measurementqualifier/1/0', + required: true, + }, + { + path: 'MeasurementQualifier.Quality', + modelType: 'Property', + valueType: 'xs:string', + semanticId: '0112/2///61360_7#CBA006', + required: true, + }, + { + path: 'MeasurementQualifier.Range', + modelType: 'Range', + valueType: 'xs:float', + semanticId: 'https://admin-shell.io/idta/meaurementvalue/range/1/0', + required: false, + }, + { + path: 'MeasurementQualifier.Scale', + modelType: 'Property', + valueType: 'xs:integer', + semanticId: 'https://admin-shell.io/idta/measurementvalue/scale/1/0', + required: false, + }, + { + path: 'MeasurementQualifier.Tag', + modelType: 'Property', + valueType: 'xs:string', + semanticId: '0112/2///61987#ABB271', + required: false, + }, +]; + +export interface MeasurementValueData { + value: string | undefined; + valueProperty: aas.Property; + unit: string | undefined; + kind: string | undefined; + timestamp: string | undefined; + predefinedValue?: string; + quality: string | undefined; + range?: string; + scale?: string; + tag?: string; + origin?: string; + version?: string; + identifier?: string; + extensions: aas.Referable[]; +} + +export type MeasurementValueValidation = + | { valid: true; data: MeasurementValueData } + | { valid: false; errors: string[] }; + +/** Validates and normalizes an IDTA 02029-1 Measurement Value submodel. */ +export function validateSensorMeasurementValue(submodel: aas.Submodel | undefined): MeasurementValueValidation { + const errors: string[] = []; + if (!submodel) { + return { valid: false, errors: ['Submodel is missing.'] }; + } + + if (getSemanticId(submodel) !== SENSOR_MEASUREMENT_VALUE_1_0) { + errors.push('Submodel semantic ID does not match IDTA 02029-1 v1.0.'); + } + + const elements = new Map(); + for (const definition of definitions) { + const element = findSingle(submodel, definition.path, errors); + if (!element) { + if (definition.required && hasOptionalParent(definition.path, elements)) { + errors.push(`${definition.path} is required when its collection is present.`); + } else if (definition.required && !hasOptionalAncestor(definition.path)) { + errors.push(`${definition.path} is required.`); + } + continue; + } + + elements.set(definition.path, element); + if (element.modelType !== definition.modelType) { + errors.push(`${definition.path} must be a ${definition.modelType}.`); + continue; + } + if (getSemanticId(element) !== definition.semanticId) { + errors.push(`${definition.path} has an unexpected semantic ID.`); + } + if (definition.valueType && (element as aas.Property | aas.Range).valueType !== definition.valueType) { + errors.push(`${definition.path} must use ${definition.valueType}.`); + } + } + + if (errors.length > 0) { + return { valid: false, errors }; + } + + const valueProperty = elements.get('MeasuredValue.Value') as aas.Property; + const range = elements.get('MeasurementQualifier.Range'); + return { + valid: true, + data: { + value: valueProperty.value, + valueProperty, + unit: propertyValue(elements, 'MeasuredValue.Unit'), + kind: propertyValue(elements, 'MeasuredValue.Kind'), + timestamp: propertyValue(elements, 'MeasurementTimestamp'), + predefinedValue: propertyValue(elements, 'MeasuredValuePreDefined.Distance'), + quality: propertyValue(elements, 'MeasurementQualifier.Quality'), + range: isRange(range) + ? [range.min, range.max].filter(value => value !== undefined).join(' – ') || undefined + : undefined, + scale: propertyValue(elements, 'MeasurementQualifier.Scale'), + tag: propertyValue(elements, 'MeasurementQualifier.Tag'), + origin: propertyValue(elements, 'Concept.Origin'), + version: propertyValue(elements, 'Concept.Version'), + identifier: propertyValue(elements, 'Concept.Identifier'), + extensions: getExtensions(submodel), + }, + }; +} + +function findSingle(parent: aas.Referable, path: string, errors: string[]): aas.Referable | undefined { + let current = parent; + for (const idShort of path.split('.')) { + const matches = getChildren(current).filter(child => child.idShort === idShort); + if (matches.length > 1) { + errors.push(`${path} occurs more than once.`); + return undefined; + } + if (matches.length === 0) { + return undefined; + } + current = matches[0]; + } + return current; +} + +function hasOptionalAncestor(path: string): boolean { + const parent = path.split('.').slice(0, -1).join('.'); + return definitions.some(definition => definition.path === parent && !definition.required); +} + +function hasOptionalParent(path: string, elements: Map): boolean { + const parent = path.split('.').slice(0, -1).join('.'); + return Boolean(parent && elements.has(parent)); +} + +function propertyValue(elements: Map, path: string): string | undefined { + const element = elements.get(path); + return isProperty(element) ? element.value : undefined; +} + +function getExtensions(submodel: aas.Submodel): aas.Referable[] { + const knownPaths = new Set(definitions.map(definition => definition.path)); + const extensions: aas.Referable[] = []; + collect(submodel, ''); + return extensions; + + function collect(parent: aas.Referable, prefix: string): void { + for (const child of getChildren(parent)) { + const path = prefix ? `${prefix}.${child.idShort}` : child.idShort; + if (!knownPaths.has(path)) { + extensions.push(child); + continue; + } + if (isSubmodelElementCollection(child)) { + collect(child, path); + } + } + } +} diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.html b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.html new file mode 100644 index 000000000..8275dc6a2 --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.html @@ -0,0 +1,29 @@ +
+
+
+
+
+ + +
+
+ +
+ + @if (count() >= 2) { + + } +
diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.scss b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.scss new file mode 100644 index 000000000..e69de29bb diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.ts new file mode 100644 index 000000000..d5f1e1cc9 --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-view.ts @@ -0,0 +1,30 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2026 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { TranslateDirective } from '@ngx-translate/core'; +import { NgbPaginationModule } from '@ng-bootstrap/ng-bootstrap'; + +import { ThumbnailQRCode } from '../thumbnail-qrcode/thumbnail-qrcode'; +import { LeafView } from '../leaf-view'; +import { VIEW_ROUTE_NAME } from '../view-route-name'; +import { SensorMeasurementValue } from './sensor-measurement-value'; + +@Component({ + selector: 'fhg-sensor-measurement-value-view', + providers: [{ provide: VIEW_ROUTE_NAME, useValue: 'SensorMeasurementValue' }], + imports: [RouterModule, TranslateDirective, NgbPaginationModule, ThumbnailQRCode, SensorMeasurementValue], + templateUrl: './sensor-measurement-value-view.html', + styleUrl: './sensor-measurement-value-view.scss', +}) +export class SensorMeasurementValueView extends LeafView { + public constructor() { + super(); + } +} diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.html b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.html new file mode 100644 index 000000000..e25710e44 --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.html @@ -0,0 +1,81 @@ +
+ @if (data(); as measurement) { +
+
+
+ {{ measurement.value ?? '—' }} + @if (measurement.unit) { + {{ measurement.unit }} + } +
+ @if (measurement.kind) { +
{{ measurement.kind }}
+ } + @if (isLive()) { + + } +
+ +
+ @if (measurement.timestamp) { +
+
{{ measurement.timestamp }}
+ } + @if (measurement.predefinedValue) { +
+
{{ measurement.predefinedValue }}
+ } + @if (measurement.quality) { +
+
{{ measurement.quality }}
+ } + @if (measurement.range) { +
+
{{ measurement.range }}
+ } + @if (measurement.scale) { +
+
{{ measurement.scale }}
+ } + @if (measurement.tag) { +
+
{{ measurement.tag }}
+ } + @if (measurement.origin) { +
+
{{ measurement.origin }}
+ } + @if (measurement.version) { +
+
{{ measurement.version }}
+ } + @if (measurement.identifier) { +
+
{{ measurement.identifier }}
+ } +
+ + @if (measurement.extensions.length) { +
+ +
    + @for (extension of measurement.extensions; track extension.idShort) { +
  • {{ extension.idShort }}
  • + } +
+
+ } + } @else { + + } +
diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.scss b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.scss new file mode 100644 index 000000000..e69de29bb diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts new file mode 100644 index 000000000..b3d0f32ba --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts @@ -0,0 +1,173 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2026 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideZonelessChangeDetection } from '@angular/core'; +import { provideTranslateService, TranslateLoader } from '@ngx-translate/core'; +import { Subject } from 'rxjs'; +import { aas, AASDocument, WebSocketData } from 'aas-core'; + +import { WebSocketService } from '../../services/web-socket.service'; +import { FakeLoader } from '../../../test/mocks'; +import { SensorMeasurementValue } from './sensor-measurement-value'; +import { validateSensorMeasurementValue } from './sensor-measurement-value-validator'; + +import measurementValue from '../../../test/assets/sensor-measurement-value-1-0.json'; + +describe('SensorMeasurementValue', () => { + let component: SensorMeasurementValue; + let fixture: ComponentFixture; + let messages: Subject; + let webSocket: Pick; + let document: AASDocument; + let environment: aas.Environment; + let submodel: aas.Submodel; + + beforeEach(async () => { + messages = new Subject(); + webSocket = { getMessages: () => messages, sendMessage: vi.fn() }; + environment = measurementValue as aas.Environment; + submodel = environment.submodels[0]; + document = { + address: '', + crc32: 0, + endpoint: 'Test', + id: 'https://example.com/aas/sensor', + idShort: 'SensorAAS', + readonly: false, + timestamp: 0, + content: environment, + }; + + await TestBed.configureTestingModule({ + imports: [SensorMeasurementValue], + providers: [ + { provide: WebSocketService, useValue: webSocket }, + provideTranslateService({ loader: { provide: TranslateLoader, useClass: FakeLoader } }), + provideZonelessChangeDetection(), + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SensorMeasurementValue); + component = fixture.componentInstance; + fixture.componentRef.setInput('document', document); + fixture.componentRef.setInput('submodel', submodel); + fixture.detectChanges(); + }); + + it('validates and renders the required measurement, optional metadata, and vendor extension', () => { + expect(component.validation().valid).toBe(true); + expect(component.data()).toMatchObject({ + value: '23.5', + unit: '°C', + kind: 'Temperature', + timestamp: '2025-12-11T10:15:00Z', + range: '0 – 100', + quality: 'good', + identifier: '0173-1#02-BAF016#001', + }); + expect(component.data()?.extensions.map(item => item.idShort)).toEqual(['VendorExtension']); + }); + + it('rejects missing required fields, wrong types, duplicate fields, and semantic IDs', () => { + const cases: Array<(submodel: aas.Submodel) => void> = [ + submodel => submodel.submodelElements?.splice(0, 1), + submodel => { + const timestamp = submodel.submodelElements?.find( + item => item.idShort === 'MeasurementTimestamp', + ) as aas.Property; + timestamp.valueType = 'xs:string'; + }, + submodel => submodel.submodelElements?.push(structuredClone(submodel.submodelElements[0])), + submodel => { + const qualifier = submodel.submodelElements?.find(item => item.idShort === 'MeasurementQualifier')!; + qualifier.semanticId!.keys[0].value = 'https://example.com/wrong'; + }, + ]; + for (const mutate of cases) { + const invalidSubmodel = structuredClone(submodel) as aas.Submodel; + mutate(invalidSubmodel); + expect(validateSensorMeasurementValue(invalidSubmodel).valid).toBe(false); + } + }); + + it('allows omitted optional structures', () => { + const optionalSubmodel = structuredClone(submodel) as aas.Submodel; + optionalSubmodel.submodelElements = optionalSubmodel.submodelElements?.filter( + item => !['MeasuredValuePreDefined', 'Concept'].includes(item.idShort), + ); + const qualifier = optionalSubmodel.submodelElements?.find( + item => item.idShort === 'MeasurementQualifier', + ) as aas.SubmodelElementCollection; + qualifier.value = qualifier.value?.filter(item => !['Range', 'Scale', 'Tag'].includes(item.idShort)); + expect(validateSensorMeasurementValue(optionalSubmodel).valid).toBe(true); + }); + + it('rejects an optional collection when its required child is missing', () => { + const invalidSubmodel = structuredClone(submodel) as aas.Submodel; + const predefined = invalidSubmodel.submodelElements?.find( + item => item.idShort === 'MeasuredValuePreDefined', + ) as aas.SubmodelElementCollection; + predefined.value = []; + expect(validateSensorMeasurementValue(invalidSubmodel).valid).toBe(false); + }); + + it('requests and applies live updates for MeasuredValue.Value', () => { + expect(webSocket.sendMessage).toHaveBeenCalledWith({ + type: 'LiveRequest', + data: { + endpoint: document.endpoint, + id: document.id, + nodes: [{ nodeId: 'ns=2;s=Temperature', valueType: 'xs:float' }], + }, + }); + + messages.next({ + type: 'LiveNode[]', + data: [{ nodeId: 'ns=2;s=Temperature', value: 24.2, valueType: 'xs:float', timeStamp: 0 }], + }); + expect(component.data()?.value).toBe('24.2'); + expect(component.data()?.timestamp).not.toBe('2025-12-11T10:15:00Z'); + }); + + it('does not subscribe when the primary value has no node ID', () => { + const noLiveSubmodel = structuredClone(submodel) as aas.Submodel; + const measuredValue = noLiveSubmodel.submodelElements?.find( + item => item.idShort === 'MeasuredValue', + ) as aas.SubmodelElementCollection | undefined; + measuredValue?.value?.forEach(item => { + if (item.idShort === 'Value') { + delete (item as aas.Property).nodeId; + } + }); + + fixture.componentRef.setInput('submodel', noLiveSubmodel); + fixture.detectChanges(); + expect(component.isLive()).toBe(false); + expect(messages.observed).toBe(false); + }); + + it('does not render a partial measurement for an invalid template', () => { + const invalidSubmodel = structuredClone(submodel) as aas.Submodel; + invalidSubmodel.submodelElements = invalidSubmodel.submodelElements?.filter( + item => item.idShort !== 'MeasuredValue', + ); + fixture.componentRef.setInput('submodel', invalidSubmodel); + fixture.detectChanges(); + expect(component.data()).toBeUndefined(); + expect(component.errors()).not.toHaveLength(0); + expect(fixture.nativeElement.textContent).toContain('SensorMeasurementValue.INVALID_TEMPLATE'); + }); + + it('unsubscribes from live updates when destroyed', () => { + expect(messages.observed).toBe(true); + fixture.destroy(); + expect(messages.observed).toBe(false); + }); +}); diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.ts new file mode 100644 index 000000000..3e1f2be5a --- /dev/null +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.ts @@ -0,0 +1,102 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2026 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Component, computed, effect, inject, input, OnDestroy, signal } from '@angular/core'; +import { TranslateDirective, TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { Subscription } from 'rxjs'; +import { aas, AASDocument, convertToString, LiveNode, LiveRequest, WebSocketData } from 'aas-core'; + +import { WebSocketService } from '../../services/web-socket.service'; +import { MeasurementValueData, validateSensorMeasurementValue } from './sensor-measurement-value-validator'; + +@Component({ + selector: 'fhg-sensor-measurement-value', + templateUrl: './sensor-measurement-value.html', + styleUrl: './sensor-measurement-value.scss', + imports: [TranslateDirective, TranslatePipe], +}) +export class SensorMeasurementValue implements OnDestroy { + private readonly webSocket = inject(WebSocketService); + private readonly translate = inject(TranslateService); + private webSocketSubscription?: Subscription; + private readonly liveValue = signal(undefined); + private readonly liveTimestamp = signal(undefined); + + public readonly document = input(); + public readonly submodel = input(); + public readonly validation = computed(() => validateSensorMeasurementValue(this.submodel())); + public readonly data = computed(() => { + const validation = this.validation(); + if (!validation.valid) { + return undefined; + } + + return { + ...validation.data, + value: this.liveValue() ?? validation.data.value, + timestamp: this.liveTimestamp() ?? validation.data.timestamp, + }; + }); + public readonly errors = computed(() => { + const validation = this.validation(); + return validation.valid ? [] : validation.errors; + }); + public readonly isLive = computed(() => Boolean(this.data()?.valueProperty.nodeId)); + + public constructor() { + effect(() => { + const document = this.document(); + const validation = this.validation(); + this.stop(); + this.liveValue.set(undefined); + this.liveTimestamp.set(undefined); + if (document && validation.valid) { + const value = validation.data.valueProperty; + const nodeId = value.nodeId; + if (!nodeId) { + return; + } + + this.webSocketSubscription = this.webSocket.getMessages().subscribe({ next: this.onMessage }); + this.webSocket.sendMessage({ + type: 'LiveRequest', + data: { + endpoint: document.endpoint, + id: document.id, + nodes: [{ nodeId, valueType: value.valueType ?? 'undefined' }], + } satisfies LiveRequest, + }); + } + }); + } + + public ngOnDestroy(): void { + this.stop(); + } + + private readonly onMessage = (message: WebSocketData): void => { + if (message.type !== 'LiveNode[]') { + return; + } + + const nodeId = this.data()?.valueProperty.nodeId; + const liveNode = (message.data as LiveNode[]).find(item => item.nodeId === nodeId); + if (liveNode) { + const locale = this.translate.currentLang() ?? 'en-us'; + this.liveValue.set(convertToString(liveNode.value, locale)); + if (liveNode.timeStamp !== undefined) { + this.liveTimestamp.set(convertToString(new Date(liveNode.timeStamp), locale)); + } + } + }; + + private stop(): void { + this.webSocketSubscription?.unsubscribe(); + this.webSocketSubscription = undefined; + } +} diff --git a/projects/aas-lib/src/lib/views/view-route-config.ts b/projects/aas-lib/src/lib/views/view-route-config.ts index 3dbb75d15..5af274cc3 100644 --- a/projects/aas-lib/src/lib/views/view-route-config.ts +++ b/projects/aas-lib/src/lib/views/view-route-config.ts @@ -18,6 +18,7 @@ import { OperationalDataView } from './operational-data/operational-data-view'; import { TechnicalDataView } from './technical-data/technical-data-view'; import { HierarchicalStructureView } from './hierarchical-structure/hierarchical-structure-view'; import { ServiceRequestNotification } from './service-request-notification/service-request-notification'; +import { SensorMeasurementValueView } from './sensor-measurement-value/sensor-measurement-value-view'; import { CARBON_FOOTPRINT_0_9, CARBON_FOOTPRINT_1_0, @@ -32,6 +33,7 @@ import { NAMEPLATE_FHG, NAMEPLATE_HSU, TECHNICAL_DATA_1_2, + SENSOR_MEASUREMENT_VALUE_1_0, } from './views-constants'; import { AssetStatus } from './asset-status/asset-status'; import { DocumentContent } from './document-content/document-content'; @@ -140,6 +142,14 @@ export const viewRoutes: ViewRoute[] = [ idShorts: ['ServiceRequestNotification'], }, }, + { + path: 'SensorMeasurementValue', + component: SensorMeasurementValueView, + data: { + type: 'Leaf', + semanticIds: [SENSOR_MEASUREMENT_VALUE_1_0], + }, + }, { path: 'TechnicalData', component: TechnicalDataView, diff --git a/projects/aas-lib/src/lib/views/views-constants.ts b/projects/aas-lib/src/lib/views/views-constants.ts index e58159e4d..82b3b919f 100644 --- a/projects/aas-lib/src/lib/views/views-constants.ts +++ b/projects/aas-lib/src/lib/views/views-constants.ts @@ -19,3 +19,4 @@ export const NAMEPLATE_3_0 = 'https://admin-shell.io/idta/nameplate/3/0/Nameplat export const NAMEPLATE_FHG = 'urn:IOSB:Fraunhofer:de:KIReallabor:CUNACup:SemId:Submodel:Nameplate'; export const NAMEPLATE_HSU = 'https://www.hsu-hh.de/aut/aas/nameplate'; export const TECHNICAL_DATA_1_2 = 'https://admin-shell.io/ZVEI/TechnicalData/Submodel/1/2'; +export const SENSOR_MEASUREMENT_VALUE_1_0 = 'https://admin-shell.io/idta/SubmodelTemplate/measurementValue/1/0'; diff --git a/projects/aas-lib/src/public-api.ts b/projects/aas-lib/src/public-api.ts index de2bcb660..71855e935 100644 --- a/projects/aas-lib/src/public-api.ts +++ b/projects/aas-lib/src/public-api.ts @@ -97,6 +97,9 @@ export * from './lib/views/nameplate/nameplate.state'; export * from './lib/views/nameplate/nameplate'; export * from './lib/views/operational-data/operational-data-view'; export * from './lib/views/service-request-notification/service-request-notification'; +export * from './lib/views/sensor-measurement-value/sensor-measurement-value-view'; +export * from './lib/views/sensor-measurement-value/sensor-measurement-value'; +export * from './lib/views/sensor-measurement-value/sensor-measurement-value-validator'; export * from './lib/views/technical-data/technical-data-view'; export * from './lib/views/technical-data/technical-data.state'; export * from './lib/views/technical-data/technical-data'; diff --git a/projects/aas-lib/src/test/assets/sensor-measurement-value-1-0.json b/projects/aas-lib/src/test/assets/sensor-measurement-value-1-0.json new file mode 100644 index 000000000..ef7e523e1 --- /dev/null +++ b/projects/aas-lib/src/test/assets/sensor-measurement-value-1-0.json @@ -0,0 +1,1409 @@ +{ + "assetAdministrationShells": [ + { + "idShort": "MeasurementValueAAS", + "id": "https://admin-shell.io/idta/aas/measurementvalue/1/0", + "assetInformation": { + "assetKind": "Type", + "globalAssetId": "https://admin-shell.io/idta/asset/measurementvalue/1/0", + "assetType": "Type", + "defaultThumbnail": { + "path": "/IM0082877_jpgBig.jpg", + "contentType": "image/png" + } + }, + "submodels": [ + { + "type": "ModelReference", + "keys": [ + { + "type": "Submodel", + "value": "https://admin-shell.io/idta/SubmodelTemplate/measurementValue/1/0" + } + ] + } + ], + "modelType": "AssetAdministrationShell" + } + ], + "submodels": [ + { + "idShort": "MeasurementValue", + "displayName": [ + { + "language": "en", + "text": "Measurement Value" + } + ], + "description": [ + { + "language": "en", + "text": "The Submodel Template for Measurement Value" + } + ], + "id": "https://example.com/submodels/measurement-value", + "kind": "Template", + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/SubmodelTemplate/measurementValue/1/0" + } + ] + }, + "submodelElements": [ + { + "idShort": "MeasuredValue", + "displayName": [ + { + "language": "en", + "text": "Measured Value" + } + ], + "description": [ + { + "language": "en", + "text": "This is the measured value of the sensor output" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/measuredvalue/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "value": [ + { + "category": "VARIABLE", + "idShort": "Value", + "displayName": [ + { + "language": "en", + "text": "Value" + } + ], + "description": [ + { + "language": "en", + "text": "The value of the of measured sensor output" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/value/1/0" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:float", + "value": "23.5", + "modelType": "Property", + "nodeId": "ns=2;s=Temperature" + }, + { + "category": "PARAMETER", + "idShort": "Unit", + "displayName": [ + { + "language": "en", + "text": "Unit" + } + ], + "description": [ + { + "language": "en", + "text": "The unit of the measured value" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///62720#UBA000" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:string", + "value": "\u00b0C", + "modelType": "Property" + }, + { + "category": "PARAMETER", + "idShort": "Kind", + "displayName": [ + { + "language": "en", + "text": "Kind" + } + ], + "description": [ + { + "language": "en", + "text": "Kind of measured value" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///62683#ACI144" + } + ] + }, + "supplementalSemanticIds": [ + { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///62683#ACI144" + } + ] + } + ], + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:string", + "value": "Temperature", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "MeasuredValuePreDefined", + "displayName": [ + { + "language": "en", + "text": "Measured Value Pre Defined" + } + ], + "description": [ + { + "language": "en", + "text": "The predefined measured value of the sensor" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/measuredvaluepredefined/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "value": [ + { + "idShort": "Distance", + "displayName": [ + { + "language": "en", + "text": "Distance" + } + ], + "description": [ + { + "language": "en", + "text": "The measured length between two points." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61987#ABN718#001" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:float", + "value": "5", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "category": "VARIABLE", + "idShort": "MeasurementTimestamp", + "displayName": [ + { + "language": "en", + "text": "Measurement Timestamp" + } + ], + "description": [ + { + "language": "en", + "text": "Date and time when the measurement was taken." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61360_7#CBA007" + } + ] + }, + "supplementalSemanticIds": [ + { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61360_7#CBA008" + } + ] + } + ], + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:dateTime", + "value": "2025-12-11T10:15:00Z", + "modelType": "Property" + }, + { + "idShort": "Concept", + "displayName": [ + { + "language": "en", + "text": "Concept" + } + ], + "description": [ + { + "language": "en", + "text": "This collection defines which semantic definition is used for measuredvalue" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/concept/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "value": [ + { + "idShort": "Origin", + "displayName": [ + { + "language": "en", + "text": "Origin" + } + ], + "description": [ + { + "language": "en", + "text": "The origin of the semantic description" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/origin/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:string", + "value": "IEC CDD", + "modelType": "Property" + }, + { + "idShort": "Version", + "displayName": [ + { + "language": "en", + "text": "Version" + } + ], + "description": [ + { + "language": "en", + "text": "The version of the semantic defintion used" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/version/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:string", + "value": "1.0", + "modelType": "Property" + }, + { + "idShort": "Identifier", + "displayName": [ + { + "language": "en", + "text": "Identifier" + } + ], + "description": [ + { + "language": "en", + "text": "The identifier of the semantic definition used" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/identifier/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:string", + "value": "0173-1#02-BAF016#001", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "MeasurementQualifier", + "displayName": [ + { + "language": "en", + "text": "Measurement Qualifier" + } + ], + "description": [ + { + "language": "en", + "text": "The Qualifier of the measurement" + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/measurementqualifier/1/0" + } + ] + }, + "qualifiers": [ + { + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "value": [ + { + "category": "VARIABLE", + "idShort": "Quality", + "displayName": [ + { + "language": "en", + "text": "Quality" + } + ], + "description": [ + { + "language": "en", + "text": "Quality of the measured value (e.g., Good, Bad, Uncertain and others like out of range)." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61360_7#CBA006" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/AllowedValue/1/0" + } + ] + }, + "kind": "ValueQualifier", + "type": "SMT/AllowedValue", + "valueType": "xs:string", + "value": "good | bad | uncertain | others", + "valueId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61360_7#CTA052" + } + ] + } + }, + { + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "One" + } + ], + "valueType": "xs:string", + "value": "good", + "modelType": "Property" + }, + { + "idShort": "Range", + "displayName": [ + { + "language": "en", + "text": "Range" + } + ], + "description": [ + { + "language": "en", + "text": "Allowed minimum and maximum measurement limits." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/meaurementvalue/range/1/0" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "kind": "TemplateQualifier", + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:float", + "min": "0", + "max": "100", + "modelType": "Range" + }, + { + "category": "PARAMETER", + "idShort": "Scale", + "displayName": [ + { + "language": "en", + "text": "Scale" + } + ], + "description": [ + { + "language": "en", + "text": "Factor used to convert or adjust the measured value." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/measurementvalue/scale/1/0" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/SubmodelTemplates/Cardinality/1/0" + } + ] + }, + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:integer", + "value": "1", + "modelType": "Property" + }, + { + "category": "PARAMETER", + "idShort": "Tag", + "displayName": [ + { + "language": "en", + "text": "Tag" + } + ], + "description": [ + { + "language": "en", + "text": "Identifier linking the measurement to its source or device." + } + ], + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "0112/2///61987#ABB271" + } + ] + }, + "qualifiers": [ + { + "semanticId": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/idta/sensor/measurementTag/1/0" + } + ] + }, + "type": "SMT/Cardinality", + "valueType": "xs:string", + "value": "ZeroToOne" + } + ], + "valueType": "xs:string", + "value": "process", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + }, + { + "idShort": "VendorExtension", + "modelType": "Property", + "valueType": "xs:string", + "value": "retained" + } + ], + "modelType": "Submodel" + } + ], + "conceptDescriptions": [ + { + "idShort": "MeasurementValue", + "description": [ + { + "language": "en", + "text": "The Submodel Template for Measurement Value" + } + ], + "id": "https://admin-shell.io/idta/SubmodelTemplate/measurementValue/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "MeasurementValue" + } + ], + "definition": [ + { + "language": "en", + "text": "The Submodel Template for Measurement Value" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "MeasuredValue", + "description": [ + { + "language": "en", + "text": "This is the measured value of the sensor output" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/measuredvalue/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "MeasuredValue" + } + ], + "definition": [ + { + "language": "en", + "text": "This is the measured value of the sensor output" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Value", + "description": [ + { + "language": "en", + "text": "The value of the of measured sensor output" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/value/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Value" + } + ], + "definition": [ + { + "language": "en", + "text": "The value of the of measured sensor output" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Unit", + "description": [ + { + "language": "en", + "text": "The unit of the measured value" + } + ], + "id": "0112/2///62720#UBA000", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Unit" + } + ], + "definition": [ + { + "language": "en", + "text": "The unit of the measured value" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Kind", + "description": [ + { + "language": "en", + "text": "Kind of measured value" + } + ], + "id": "0112/2///62683#ACI144", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Kind" + } + ], + "definition": [ + { + "language": "en", + "text": "Kind of measured value" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "MeasuredValuePreDefined", + "description": [ + { + "language": "en", + "text": "The predefined measured value of the sensor" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/measuredvaluepredefined/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "MeasuredValuePreDefined" + } + ], + "definition": [ + { + "language": "en", + "text": "The predefined measured value of the sensor" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Distance", + "description": [ + { + "language": "en", + "text": "The measured length between two points." + } + ], + "id": "0112/2///61987#ABN718#001", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Distance" + } + ], + "definition": [ + { + "language": "en", + "text": "The measured length between two points." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "MeasurementTimestamp", + "description": [ + { + "language": "en", + "text": "Date and time when the measurement was taken." + } + ], + "id": "0112/2///61360_7#CBA007", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "MeasurementTimestamp" + } + ], + "definition": [ + { + "language": "en", + "text": "Date and time when the measurement was taken." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Concept", + "description": [ + { + "language": "en", + "text": "This collection defines which semantic definition is used for measuredvalue" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/concept/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Concept" + } + ], + "definition": [ + { + "language": "en", + "text": "This collection defines which semantic definition is used for measuredvalue" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Origin", + "description": [ + { + "language": "en", + "text": "The origin of the semantic description" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/origin/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Origin" + } + ], + "definition": [ + { + "language": "en", + "text": "The origin of the semantic description" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Version", + "description": [ + { + "language": "en", + "text": "The version of the semantic defintion used" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/version/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Version" + } + ], + "definition": [ + { + "language": "en", + "text": "The version of the semantic defintion used" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Identifier", + "description": [ + { + "language": "en", + "text": "The identifier of the semantic definition used" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/identifier/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Identifier" + } + ], + "definition": [ + { + "language": "en", + "text": "The identifier of the semantic definition used" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "MeasurementQualifier", + "description": [ + { + "language": "en", + "text": "The Qualifier of the measurement" + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/measurementqualifier/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "MeasurementQualifier" + } + ], + "definition": [ + { + "language": "en", + "text": "The Qualifier of the measurement" + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Quality", + "description": [ + { + "language": "en", + "text": "Quality of the measured value (e.g., Good, Bad, Uncertain and others like out of range)." + } + ], + "id": "0112/2///61360_7#CBA006", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Quality" + } + ], + "definition": [ + { + "language": "en", + "text": "Quality of the measured value (e.g., Good, Bad, Uncertain and others like out of range)." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Range", + "description": [ + { + "language": "en", + "text": "Allowed minimum and maximum measurement limits." + } + ], + "id": "https://admin-shell.io/idta/meaurementvalue/range/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Range" + } + ], + "definition": [ + { + "language": "en", + "text": "Allowed minimum and maximum measurement limits." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Scale", + "description": [ + { + "language": "en", + "text": "Factor used to convert or adjust the measured value." + } + ], + "id": "https://admin-shell.io/idta/measurementvalue/scale/1/0", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Scale" + } + ], + "definition": [ + { + "language": "en", + "text": "Factor used to convert or adjust the measured value." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + }, + { + "idShort": "Tag", + "description": [ + { + "language": "en", + "text": "Identifier linking the measurement to its source or device." + } + ], + "id": "0112/2///61987#ABB271", + "embeddedDataSpecifications": [ + { + "dataSpecification": { + "type": "ExternalReference", + "keys": [ + { + "type": "GlobalReference", + "value": "https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0" + } + ] + }, + "dataSpecificationContent": { + "preferredName": [ + { + "language": "en", + "text": "Tag" + } + ], + "definition": [ + { + "language": "en", + "text": "Identifier linking the measurement to its source or device." + } + ], + "modelType": "DataSpecificationIec61360" + } + } + ], + "modelType": "ConceptDescription" + } + ] +} diff --git a/projects/aas-portal/src/assets/i18n/de-de.json b/projects/aas-portal/src/assets/i18n/de-de.json index 6e948a1bd..85d32a118 100644 --- a/projects/aas-portal/src/assets/i18n/de-de.json +++ b/projects/aas-portal/src/assets/i18n/de-de.json @@ -9,7 +9,7 @@ "TITLE": "AAS-Baum", "EMPTY": "Kein AAS-Document verfügbar." }, - "AASTable": { + "AASTable": { "COLUMN_NAME": "Name", "COLUMN_ID": "ID", "COLUMN_ENDPOINT": "Endpunkt", @@ -322,6 +322,22 @@ "ServiceRequestNotification": { "TITLE": "Serviceanfrage" }, + "SensorMeasurementValue": { + "TITLE": "Sensor-Messwert", + "VALUE": "Wert", + "TIMESTAMP": "Messzeitpunkt", + "PREDEFINED_VALUE": "Vordefinierter Wert", + "QUALITY": "Qualität", + "RANGE": "Bereich", + "SCALE": "Skalierung", + "TAG": "Kennzeichnung", + "ORIGIN": "Ursprung", + "VERSION": "Version", + "IDENTIFIER": "Bezeichner", + "LIVE": "Live-Wert", + "EXTENSIONS": "Zusätzliche Elemente", + "INVALID_TEMPLATE": "Das Messwert-Teilmodell entspricht nicht IDTA 02029-1 v1.0." + }, "Settings": { "ADD_ENDPOINT": "Endpunkt hinzufügen", "EXTRAS": "Endpunkt ???", @@ -346,6 +362,7 @@ "OperationalData": "Operative Daten", "PLACEHOLDER_FILTER": "Filter", "ServiceRequestNotification": "Serviceanfrage", + "SensorMeasurementValue": "Sensor-Messwert", "TechnicalData": "Technische Daten", "VIEWS": "Sichten" }, @@ -386,4 +403,4 @@ "VALUE": "Wert", "WebDAV": "WebDAV" } -} \ No newline at end of file +} diff --git a/projects/aas-portal/src/assets/i18n/en-us.json b/projects/aas-portal/src/assets/i18n/en-us.json index 1ad8bc724..195ee8189 100644 --- a/projects/aas-portal/src/assets/i18n/en-us.json +++ b/projects/aas-portal/src/assets/i18n/en-us.json @@ -322,6 +322,22 @@ "ServiceRequestNotification": { "TITLE": "Service Request Notification" }, + "SensorMeasurementValue": { + "TITLE": "Sensor Measurement Value", + "VALUE": "Value", + "TIMESTAMP": "Measurement timestamp", + "PREDEFINED_VALUE": "Predefined value", + "QUALITY": "Quality", + "RANGE": "Range", + "SCALE": "Scale", + "TAG": "Tag", + "ORIGIN": "Origin", + "VERSION": "Version", + "IDENTIFIER": "Identifier", + "LIVE": "Live value", + "EXTENSIONS": "Additional elements", + "INVALID_TEMPLATE": "The Measurement Value submodel does not conform to IDTA 02029-1 v1.0." + }, "Settings": { "ADD_ENDPOINT": "Add Endpoint", "EXTRAS": "Extras...", @@ -346,6 +362,7 @@ "OperationalData": "Operational data", "PLACEHOLDER_FILTER": "Filter", "ServiceRequestNotification": "Service request notification", + "SensorMeasurementValue": "Sensor measurement value", "TechnicalData": "Technical data", "VIEWS": "Views" }, @@ -386,4 +403,4 @@ "VALUE": "Value", "WebDAV": "WebDAV" } -} \ No newline at end of file +} From e5d750a02ac801e348af60764c519ba8d791227c Mon Sep 17 00:00:00 2001 From: Florian Pethig Date: Mon, 27 Jul 2026 20:15:47 +0200 Subject: [PATCH 2/2] fix(aas-lib): satisfy Sensor Measurement Value lint Co-Authored-By: Oz --- .../sensor-measurement-value-validator.ts | 3 +- .../sensor-measurement-value.spec.ts | 29 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts index 9dc0064f3..aba06b53e 100644 --- a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value-validator.ts @@ -151,8 +151,7 @@ export interface MeasurementValueData { } export type MeasurementValueValidation = - | { valid: true; data: MeasurementValueData } - | { valid: false; errors: string[] }; + { valid: true; data: MeasurementValueData } | { valid: false; errors: string[] }; /** Validates and normalizes an IDTA 02029-1 Measurement Value submodel. */ export function validateSensorMeasurementValue(submodel: aas.Submodel | undefined): MeasurementValueValidation { diff --git a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts index b3d0f32ba..97936c08e 100644 --- a/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts +++ b/projects/aas-lib/src/lib/views/sensor-measurement-value/sensor-measurement-value.spec.ts @@ -29,9 +29,9 @@ describe('SensorMeasurementValue', () => { let environment: aas.Environment; let submodel: aas.Submodel; - beforeEach(async () => { + beforeEach(async (): Promise => { messages = new Subject(); - webSocket = { getMessages: () => messages, sendMessage: vi.fn() }; + webSocket = { getMessages: (): Subject => messages, sendMessage: vi.fn() }; environment = measurementValue as aas.Environment; submodel = environment.submodels[0]; document = { @@ -77,17 +77,25 @@ describe('SensorMeasurementValue', () => { it('rejects missing required fields, wrong types, duplicate fields, and semantic IDs', () => { const cases: Array<(submodel: aas.Submodel) => void> = [ - submodel => submodel.submodelElements?.splice(0, 1), - submodel => { + (submodel: aas.Submodel): void => { + submodel.submodelElements?.splice(0, 1); + }, + (submodel: aas.Submodel): void => { const timestamp = submodel.submodelElements?.find( item => item.idShort === 'MeasurementTimestamp', ) as aas.Property; timestamp.valueType = 'xs:string'; }, - submodel => submodel.submodelElements?.push(structuredClone(submodel.submodelElements[0])), - submodel => { - const qualifier = submodel.submodelElements?.find(item => item.idShort === 'MeasurementQualifier')!; - qualifier.semanticId!.keys[0].value = 'https://example.com/wrong'; + (submodel: aas.Submodel): void => { + submodel.submodelElements?.push(structuredClone(submodel.submodelElements[0])); + }, + (submodel: aas.Submodel): void => { + const qualifier = submodel.submodelElements?.find(item => item.idShort === 'MeasurementQualifier') as + aas.SubmodelElementCollection | undefined; + const semanticKey = qualifier?.semanticId?.keys[0]; + if (semanticKey) { + semanticKey.value = 'https://example.com/wrong'; + } }, ]; for (const mutate of cases) { @@ -138,9 +146,8 @@ describe('SensorMeasurementValue', () => { it('does not subscribe when the primary value has no node ID', () => { const noLiveSubmodel = structuredClone(submodel) as aas.Submodel; - const measuredValue = noLiveSubmodel.submodelElements?.find( - item => item.idShort === 'MeasuredValue', - ) as aas.SubmodelElementCollection | undefined; + const measuredValue = noLiveSubmodel.submodelElements?.find(item => item.idShort === 'MeasuredValue') as + aas.SubmodelElementCollection | undefined; measuredValue?.value?.forEach(item => { if (item.idShort === 'Value') { delete (item as aas.Property).nodeId;