UNPKG

@c8y/ngx-components

Version:

Angular modules for Cumulocity IoT applications

644 lines (619 loc) • 25.8 kB
import { hookComputedProperty } from '@c8y/ngx-components/asset-properties'; import { inject, Injector } from '@angular/core'; import { MeasurementService, InventoryService, AlarmService, EventService, OperationService } from '@c8y/client'; import { MeasurementRealtimeService, DatePipe, ManagedObjectRealtimeService, AlarmRealtimeService, EventRealtimeService, OperationRealtimeService } from '@c8y/ngx-components'; import { from, map, filter, startWith, take, switchMap, distinctUntilChanged, pairwise, share, NEVER, scan, tap, of, catchError, combineLatest, merge } from 'rxjs'; import { gettext } from '@c8y/ngx-components/gettext'; class LastMeasurementStrategy { constructor(config, injector) { this.config = config; this.measurementService = injector.get(MeasurementService); this.measurementRealtime = injector.get(MeasurementRealtimeService); this.datePipe = injector.get(DatePipe); this.datapoint = config.dp.filter(dp => dp.__active)[0]; } fetchCurrentValue() { const measurementFilter = { valueFragmentSeries: this.datapoint.series, valueFragmentType: this.datapoint.fragment, pageSize: 1, revert: true, dateFrom: '1970-01-01', source: this.datapoint.__target.id }; return from(this.measurementService.list(measurementFilter)).pipe(map(({ data }) => data[0]), filter(measurement => !!measurement), map(measurement => this.formatMeasurement(measurement))); } createRealtimeStream(initialValue) { return this.measurementRealtime .onCreateOfSpecificMeasurement$(this.datapoint.fragment, this.datapoint.series, this.datapoint.__target) .pipe(map(measurement => this.formatMeasurement(measurement)), startWith(initialValue)); } formatMeasurement(measurement) { const resultType = this.config?.resultType; const fragment = this.datapoint.fragment; const series = this.datapoint.series; if (resultType === 1) { return measurement[fragment][series].value; } else if (resultType === 2) { return measurement[fragment][series].value + ' ' + measurement[fragment][series].unit; } else if (resultType === 3) { const date = this.datePipe.transform(new Date(measurement.time), 'short'); return `${date} | ${measurement[fragment][series].value} ${measurement[fragment][series].unit}`; } else if (resultType === 4) { return measurement[fragment][series]; } return ''; } } /** * Generic handler for realtime values following the three modes pattern * Closed for modification but open for extension through strategies */ class RealtimeValueHandler { constructor(strategy, config) { this.strategy = strategy; this.config = { refetchOnResume: true, preserveValueOnPause: true }; if (config) { this.config = { ...this.config, ...config }; } } /** * Creates an Observable based on the metadata mode */ getValue(metadata = { mode: 'realtime' }) { if (metadata.mode === 'singleValue') { return this.strategy.fetchCurrentValue().pipe(take(1)); } if (metadata.mode === 'realtime' && !metadata.realtimeControl$) { return this.handleUncontrolledRealtime(); } if (metadata.mode === 'realtime' && metadata.realtimeControl$) { return this.handleControlledRealtime(metadata.realtimeControl$); } } handleUncontrolledRealtime() { return this.strategy .fetchCurrentValue() .pipe(switchMap(initialValue => this.strategy.createRealtimeStream(initialValue))); } handleControlledRealtime(control$) { const controlWithPrevious$ = control$.pipe(distinctUntilChanged(), startWith(null), pairwise(), share()); return controlWithPrevious$.pipe(switchMap(([previous, current]) => { if (!current) { // Realtime is disabled if (previous === null) { // Initial emission while disabled return this.strategy.fetchCurrentValue().pipe(take(1)); } else if (this.config.preserveValueOnPause) { // Was previously enabled - preserve last value return NEVER; } else { // Don't preserve value - fetch current return this.strategy.fetchCurrentValue().pipe(take(1)); } } else { // Realtime is enabled if (this.config.refetchOnResume || previous === null) { // Re-fetch current value and start streaming return this.strategy .fetchCurrentValue() .pipe(switchMap(currentValue => this.strategy.createRealtimeStream(currentValue))); } else { // Continue with realtime stream without re-fetching return this.strategy.createRealtimeStream(null); } } })); } } /** * Creates an Observable that tracks the latest measurement value for a specific datapoint. * Combines initial server fetch with real-time measurement updates. * * @param config - Measurement configuration (datapoint, result type, etc.) * @param metadata - Configuration controlling the behavior of the function * @returns Observable<string> - Stream of measurement string values */ function lastMeasurementValue(config, metadata = { mode: 'realtime' }) { const injector = inject(Injector); const strategy = new LastMeasurementStrategy(config, injector); const handler = new RealtimeValueHandler(strategy); return handler.getValue(metadata); } const lastMeasurement = { name: 'lastMeasurement', contextType: ['device', 'asset', 'group'], prop: { c8y_JsonSchema: { properties: { lastMeasurement: { label: 'Last measurement', type: 'string' } } }, name: 'lastMeasurement', label: gettext('Last measurement'), type: 'string', config: { dp: [], resultType: 1 }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-last-measurement-config.component-BXfM7hTQ.mjs').then(m => m.ComputedPropertyLastMeasurementConfigComponent), value: ({ config }) => { return lastMeasurementValue(config); } }; class ChildCountStrategy { constructor(asset, childType, injector) { this.asset = asset; this.childType = childType; this.inventoryService = injector.get(InventoryService); this.moRealtimeService = injector.get(ManagedObjectRealtimeService); } fetchCurrentValue() { return from(this.inventoryService.detail(this.asset.id, { withChildren: true })).pipe(map(resp => resp?.data?.[this.childType]?.references?.length || 0)); } createRealtimeStream(initialValue) { return this.moRealtimeService.onAll$(this.asset.id).pipe(map(resp => resp?.data?.[this.childType]?.references?.length || 0), startWith(initialValue)); } } /** * Shared function that tracks the count of child items (devices or assets) for a specific managed object. * Supports real-time updates when child items are added or removed. * * @param asset - The managed object to track child items for * @param childType - Type of child items ('childDevices' or 'childAssets') * @param metadata - Configuration controlling the behavior of the function * @returns Observable<number> - Stream of child items count values */ function childCountValue(asset, metadata = { mode: 'realtime' }, childType) { const injector = inject(Injector); const strategy = new ChildCountStrategy(asset, childType, injector); const handler = new RealtimeValueHandler(strategy); return handler.getValue(metadata); } const childAssetsCount = { name: 'childAssetsCount', contextType: ['group', 'asset'], prop: { c8y_JsonSchema: { properties: { childAssetsCount: { label: 'Number of child assets', type: 'number' } } }, name: 'childAssetsCount', label: gettext('Number of child assets'), type: 'number', computed: true, isEditable: false, isStandardProperty: true }, value: ({ context, metadata }) => childAssetsCountValue(context, metadata) }; /** * Creates an Observable that tracks the count of child assets for a specific asset. * Supports real-time updates when child assets are added or removed. * * @param asset - The managed object (asset) to track child assets for * @param metadata - Configuration controlling the behavior of the function * @returns Observable<number> - Stream of child assets count values */ function childAssetsCountValue(asset, metadata = { mode: 'realtime' }) { return childCountValue(asset, metadata, 'childAssets'); } /** * Base class for count-based strategies * Provides common accumulation logic */ class CountStrategyBase { createRealtimeStream(initialValue) { return this.getRealtimeIncrement$().pipe(scan((count, increment) => count + increment, initialValue), startWith(initialValue)); } } class AlarmCountStrategy extends CountStrategyBase { constructor(config, asset, dateFrom, injector) { super(); this.config = config; this.asset = asset; this.dateFrom = dateFrom; this.alarmService = injector.get(AlarmService); this.alarmRealtimeService = injector.get(AlarmRealtimeService); this.startTime = new Date(); } fetchCurrentValue() { const severities = Object.keys(this.config.severities || {}).filter(key => this.config.severities[key]); const filters = { source: this.asset.id, dateFrom: this.dateFrom.toISOString(), type: this.config.type, pageSize: 1, withTotalElements: true, ...(severities.length && { severity: severities.join(',') }) }; return from(this.alarmService.list(filters)).pipe(map(resp => resp?.paging?.totalElements || 0), tap(() => (this.startTime = new Date())) // Update start time after fetch ); } getRealtimeIncrement$() { return this.alarmRealtimeService.onAll$(this.asset.id).pipe(map(({ data }) => data), filter(alarm => alarm.type === this.config.type && this.config.severities[alarm.severity] && new Date(alarm.creationTime) > this.startTime), tap(() => (this.startTime = new Date())), map(() => 1) // Each matching alarm increments by 1 ); } } /** * Creates an Observable that tracks alarm count for a specific asset. * When real-time is paused and resumed, it re-fetches the current count from server * to account for alarms that occurred during the pause. * * @param config - Alarm filtering configuration (type, severities, etc.) * @param asset - The managed object (device/asset) to track alarms for * @param dateFrom - Start date for counting alarms * @param metadata - Configuration controlling the behavior of the function * @returns Observable<number> - Stream of alarm count values */ function alarmCountValue(config, asset, dateFrom, metadata = { mode: 'realtime' }) { const injector = inject(Injector); const strategy = new AlarmCountStrategy(config, asset, dateFrom, injector); const handler = new RealtimeValueHandler(strategy); return handler.getValue(metadata); } const alarmCount3Months = { name: 'alarmCount3Months', contextType: ['device', 'group', 'asset'], prop: { c8y_JsonSchema: { properties: { alarmCount3Months: { label: 'Alarm count 3 months', type: 'number' } } }, name: 'alarmCount3Months', label: gettext('Alarm count 3 months'), type: 'number', config: { type: '' }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-alarm-count-config.component-Bl18pHcM.mjs').then(m => m.ComputedPropertyAlarmCountConfigComponent), value: ({ config, context, metadata }) => alarmCount3MonthsValue(config, context, metadata) }; function alarmCount3MonthsValue(config, asset, metadata) { const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); return alarmCountValue(config, asset, threeMonthsAgo, metadata); } const alarmCountToday = { name: 'alarmCountToday', contextType: ['device', 'group', 'asset'], prop: { c8y_JsonSchema: { properties: { alarmCountToday: { label: 'Alarm count today', type: 'number' } } }, name: 'alarmCountToday', label: gettext('Alarm count today'), type: 'number', config: { type: '' }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-alarm-count-config.component-Bl18pHcM.mjs').then(m => m.ComputedPropertyAlarmCountConfigComponent), value: ({ config, context, metadata }) => alarmCountTodayValue(config, context, metadata) }; function alarmCountTodayValue(config, asset, metadata) { const oneDayAgo = new Date(); oneDayAgo.setDate(oneDayAgo.getDate() - 1); return alarmCountValue(config, asset, oneDayAgo, metadata); } class EventCountStrategy extends CountStrategyBase { constructor(config, asset, dateFrom, injector) { super(); this.config = config; this.asset = asset; this.dateFrom = dateFrom; this.eventService = injector.get(EventService); this.eventRealtimeService = injector.get(EventRealtimeService); } fetchCurrentValue() { const filters = { source: this.asset.id, dateFrom: this.dateFrom.toISOString(), type: this.config.type, pageSize: 1, withTotalElements: true }; return from(this.eventService.list(filters)).pipe(map(resp => resp?.paging?.totalElements || 0)); } getRealtimeIncrement$() { return this.eventRealtimeService.onAll$(this.asset.id).pipe(map(({ data }) => data), filter(event => event.type === this.config.type && new Date(event.time) >= this.dateFrom), map(() => 1)); } } /** * Creates an Observable that tracks event count for a specific asset. * When real-time is paused and resumed, it re-fetches the current count from server * to account for events that occurred during the pause. * * @param config - Event filtering configuration (type, etc.) * @param asset - The managed object (device/asset) to track events for * @param dateFrom - Start date for counting events * @param metadata - Configuration controlling the behavior of the function * @returns Observable<number> - Stream of event count values */ function eventCountValue(config, asset, dateFrom, metadata = { mode: 'realtime' }) { const injector = inject(Injector); const strategy = new EventCountStrategy(config, asset, dateFrom, injector); const handler = new RealtimeValueHandler(strategy); return handler.getValue(metadata); } const eventCountToday = { name: 'eventCountToday', contextType: ['device', 'group', 'asset'], prop: { c8y_JsonSchema: { properties: { eventCountToday: { label: 'Event count today', type: 'number' } } }, name: 'eventCountToday', label: gettext('Event count today'), type: 'number', config: { type: '' }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-event-count-config.component-C-Lc5Ble.mjs').then(m => m.ComputedPropertyEventCountConfigComponent), value: ({ config, context, metadata }) => eventCountTodayValue(config, context, metadata) }; function eventCountTodayValue(config, asset, metadata) { const today = new Date(); today.setHours(0, 0, 0, 0); return eventCountValue(config, asset, today, metadata); } const eventCount3Months = { name: 'eventCount3Months', contextType: ['device', 'group', 'asset'], prop: { c8y_JsonSchema: { properties: { eventCount3Months: { label: 'Event count 3 months', type: 'number' } } }, name: 'eventCount3Months', label: gettext('Event count 3 months'), type: 'number', config: { type: '' }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-event-count-config.component-C-Lc5Ble.mjs').then(m => m.ComputedPropertyEventCountConfigComponent), value: ({ config, context, metadata }) => eventCount3MonthsValue(config, context, metadata) }; function eventCount3MonthsValue(config, asset, metadata) { const threeMonthsAgo = new Date(); threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3); return eventCountValue(config, asset, threeMonthsAgo, metadata); } const configurationSnapshot = { name: 'configurationSnapshot', contextType: ['device', 'asset'], prop: { c8y_JsonSchema: { properties: { configurationSnapshot: { label: 'Configuration snapshot', type: 'string' } } }, contextType: ['device', 'asset'], name: 'configurationSnapshot', label: gettext('Configuration snapshot'), type: 'string', config: { legacy: true }, computed: true, isEditable: false, isStandardProperty: true }, loadConfigComponent: () => import('./c8y-ngx-components-computed-asset-properties-configuration-snapshot-config.component-C5QMFdX1.mjs').then(m => m.ConfigurationSnapshotConfigComponent), value: ({ config, context }) => configurationSnapshotValue(config, context) }; function configurationSnapshotValue(config, asset) { if (config.legacy) { const configId = asset.c8y_ConfigurationDump?.id; if (!configId) { return of(null); } const injector = inject(Injector); const inventoryService = injector.get(InventoryService); return from(inventoryService.detail(configId)).pipe(switchMap(({ data }) => { return of(data?.name); })); } else { const fragment = `c8y_Configuration_${config.type}`; return of(asset[fragment]?.name); } } const childDevicesCount = { name: 'childDevicesCount', contextType: ['group', 'device', 'asset'], prop: { c8y_JsonSchema: { properties: { childDevicesCount: { label: 'Number of child devices', type: 'number' } } }, name: 'childDevicesCount', label: gettext('Number of child devices'), type: 'number', computed: true, isEditable: false, isStandardProperty: true }, value: ({ context, metadata }) => childDevicesCountValue(context, metadata) }; /** * Creates an Observable that tracks the count of child devices for a specific asset. * Supports real-time updates when child devices are added or removed. * * @param asset - The managed object (asset) to track child devices for * @param metadata - Configuration controlling the behavior of the function * @returns Observable<number> - Stream of child devices count values */ function childDevicesCountValue(asset, metadata = { mode: 'realtime' }) { return childCountValue(asset, metadata, 'childDevices'); } class LastDeviceMessageStrategy { constructor(asset, injector) { this.asset = asset; this.measurementService = injector.get(MeasurementService); this.measurementRealtime = injector.get(MeasurementRealtimeService); this.eventService = injector.get(EventService); this.eventRealtimeService = injector.get(EventRealtimeService); this.alarmService = injector.get(AlarmService); this.alarmRealtimeService = injector.get(AlarmRealtimeService); this.operationService = injector.get(OperationService); this.operationRealtimeService = injector.get(OperationRealtimeService); this.startTime = new Date(); } fetchCurrentValue() { const fetchFilter = { source: this.asset.id, pageSize: 1, revert: true }; const fetchLatestMeasurement = () => { return from(this.measurementService.list(fetchFilter)).pipe(map(resp => resp.data?.[0]?.time || null), catchError(() => of(null))); }; const fetchLatestEvent = () => { return from(this.eventService.list(fetchFilter)).pipe(map(resp => resp.data?.[0]?.time || null), catchError(() => of(null))); }; const fetchLatestAlarm = () => { return from(this.alarmService.list(fetchFilter)).pipe(map(resp => resp.data?.[0]?.time || null), catchError(() => of(null))); }; const fetchLatestOperation = () => { return from(this.operationService.list(fetchFilter)).pipe(map(resp => resp.data?.[0]?.creationTime || null), catchError(() => of(null))); }; return combineLatest([ fetchLatestMeasurement(), fetchLatestEvent(), fetchLatestAlarm(), fetchLatestOperation() ]).pipe(map(timestamps => { const validTimestamps = timestamps.filter(Boolean); if (validTimestamps.length === 0) { return null; } const latest = validTimestamps.reduce((latest, current) => { return new Date(current) > new Date(latest) ? current : latest; }); // Update start time for realtime filtering this.startTime = new Date(); return latest; })); } createRealtimeStream(initialValue) { const measurementStream$ = this.measurementRealtime.onAll$(this.asset.id).pipe(map(({ data }) => data.time), filter(time => new Date(time) > this.startTime)); const eventStream$ = this.eventRealtimeService.onAll$(this.asset.id).pipe(map(({ data }) => data.time), filter(time => new Date(time) > this.startTime)); const alarmStream$ = this.alarmRealtimeService.onAll$(this.asset.id).pipe(map(({ data }) => data.time), filter(time => time && new Date(time) > this.startTime)); const operationStream$ = this.operationRealtimeService.onAll$(this.asset.id).pipe(map(({ data }) => data.creationTime), filter(time => time && new Date(time) > this.startTime)); return merge(measurementStream$, eventStream$, alarmStream$, operationStream$).pipe(scan((latestTimestamp, newTimestamp) => { return new Date(newTimestamp) > new Date(latestTimestamp) ? newTimestamp : latestTimestamp; }, initialValue), startWith(initialValue)); } } /** * Gets the latest timestamp from events, alarms, measurements, and operations for a device. * Returns the most recent timestamp across all these sources. * * @param asset - The managed object (device/asset) to track * @param metadata - Configuration controlling the behavior of the function * @returns Observable<string> - Stream of latest timestamp values */ function getLastDeviceMessage(asset, metadata = { mode: 'realtime' }) { const injector = inject(Injector); const strategy = new LastDeviceMessageStrategy(asset, injector); const handler = new RealtimeValueHandler(strategy); return handler.getValue(metadata); } const lastDeviceMessage = { name: 'lastDeviceMessage', contextType: ['device'], prop: { c8y_JsonSchema: { properties: { lastDeviceMessage: { label: 'Last device message', type: 'string' } } }, name: 'lastDeviceMessage', label: gettext('Last device message'), printFormat: 'datetime', type: 'string', computed: true, isEditable: false, isStandardProperty: true }, value: ({ context }) => { return getLastDeviceMessage(context); } }; const computedAssetPropertiesProviders = [ AlarmRealtimeService, EventRealtimeService, MeasurementRealtimeService, OperationRealtimeService, ManagedObjectRealtimeService, hookComputedProperty([ lastMeasurement, lastDeviceMessage, childAssetsCount, childDevicesCount, alarmCount3Months, alarmCountToday, eventCountToday, eventCount3Months, configurationSnapshot ]) ]; /** * Generated bundle index. Do not edit. */ export { computedAssetPropertiesProviders }; //# sourceMappingURL=c8y-ngx-components-computed-asset-properties.mjs.map