@c8y/ngx-components
Version:
Angular modules for Cumulocity IoT applications
644 lines (619 loc) • 25.8 kB
JavaScript
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