Skip to content

Commit

Permalink
feat: Add Core SiteWise Asset Module
Browse files Browse the repository at this point in the history
Add Core SiteWise Asset Module that supports:
* AssetSummary
* AssetModel
* AssetPropertyValue
  • Loading branch information
gareth-amazon committed Nov 19, 2021
1 parent eb1e7f4 commit 1287af8
Show file tree
Hide file tree
Showing 16 changed files with 560 additions and 5 deletions.
17 changes: 16 additions & 1 deletion packages/components/src/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
* It contains typing information for all components that exist in this project.
*/
import { HTMLStencilElement, JSXBase } from "@stencil/core/internal";
import { DataStreamQuery, Request } from "@iot-app-kit/core";
import { AssetSummaryQuery, DataStreamQuery, Request } from "@iot-app-kit/core";
import { DataStream, MinimalViewPortConfig } from "@synchro-charts/core";
export namespace Components {
interface IotAssetDetails {
"query": AssetSummaryQuery;
}
interface IotBarChart {
"isEditing": boolean | undefined;
"query": DataStreamQuery;
Expand Down Expand Up @@ -60,6 +63,12 @@ export namespace Components {
}
}
declare global {
interface HTMLIotAssetDetailsElement extends Components.IotAssetDetails, HTMLStencilElement {
}
var HTMLIotAssetDetailsElement: {
prototype: HTMLIotAssetDetailsElement;
new (): HTMLIotAssetDetailsElement;
};
interface HTMLIotBarChartElement extends Components.IotBarChart, HTMLStencilElement {
}
var HTMLIotBarChartElement: {
Expand Down Expand Up @@ -121,6 +130,7 @@ declare global {
new (): HTMLTestingGroundElement;
};
interface HTMLElementTagNameMap {
"iot-asset-details": HTMLIotAssetDetailsElement;
"iot-bar-chart": HTMLIotBarChartElement;
"iot-connector": HTMLIotConnectorElement;
"iot-kpi": HTMLIotKpiElement;
Expand All @@ -134,6 +144,9 @@ declare global {
}
}
declare namespace LocalJSX {
interface IotAssetDetails {
"query"?: AssetSummaryQuery;
}
interface IotBarChart {
"isEditing"?: boolean | undefined;
"query"?: DataStreamQuery;
Expand Down Expand Up @@ -185,6 +198,7 @@ declare namespace LocalJSX {
interface TestingGround {
}
interface IntrinsicElements {
"iot-asset-details": IotAssetDetails;
"iot-bar-chart": IotBarChart;
"iot-connector": IotConnector;
"iot-kpi": IotKpi;
Expand All @@ -201,6 +215,7 @@ export { LocalJSX as JSX };
declare module "@stencil/core" {
export namespace JSX {
interface IntrinsicElements {
"iot-asset-details": LocalJSX.IotAssetDetails & JSXBase.HTMLAttributes<HTMLIotAssetDetailsElement>;
"iot-bar-chart": LocalJSX.IotBarChart & JSXBase.HTMLAttributes<HTMLIotBarChartElement>;
"iot-connector": LocalJSX.IotConnector & JSXBase.HTMLAttributes<HTMLIotConnectorElement>;
"iot-kpi": LocalJSX.IotKpi & JSXBase.HTMLAttributes<HTMLIotKpiElement>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Component, h, Prop, State, Watch } from '@stencil/core';
import {
getSiteWiseAssetModule,
SiteWiseAssetSession,
AssetSummaryQuery,
AssetModelQuery,
AssetPropertyValueQuery,
} from '@iot-app-kit/core';
import { AssetPropertyValue, AssetSummary, DescribeAssetModelResponse } from '@aws-sdk/client-iotsitewise';
import { AssetModelProperty } from '@aws-sdk/client-iotsitewise/dist-types/models/models_0';

@Component({
tag: 'iot-asset-details',
shadow: false,
})
export class IotAssetDetails {
@Prop() query: AssetSummaryQuery;
@State() assetSummary: AssetSummary | null = null;
@State() assetModel: DescribeAssetModelResponse | null = null;
@State() assetPropertyValues: Map<string, string> = new Map();

@State() assetSession: SiteWiseAssetSession;

componentWillLoad() {
this.assetSession = getSiteWiseAssetModule().startSession();
this.assetSession.addRequest(this.query, (summary: AssetSummary) => {
this.assetSummary = summary;
const assetId = this.assetSummary?.id as string;
const modelQuery: AssetModelQuery = { assetModelId: this.assetSummary.assetModelId as string };
this.assetSession.addRequest(modelQuery, (assetModel: DescribeAssetModelResponse) => {
this.assetModel = assetModel;
assetModel.assetModelProperties?.forEach((prop) => {
let propQuery: AssetPropertyValueQuery = { assetId: assetId, propertyId: prop.id as string };

This comment has been minimized.

Copy link
@fpauer

fpauer Nov 20, 2021

Contributor

propQuery can be a const.

this.assetSession.addRequest(propQuery, (propValue: AssetPropertyValue) => {
const copy = new Map(this.assetPropertyValues);
copy.set(prop.id as string, this.convertToString(propValue));
this.assetPropertyValues = copy;
});
});
});
});
}

convertToString(propValue: AssetPropertyValue): string {
if (propValue == undefined) {
return '';
}
const value = propValue.value;
return (
value?.stringValue ||
value?.booleanValue?.toString() ||
value?.doubleValue?.toString(10) ||
value?.integerValue?.toString(10) ||
''
);
}

componentDidUnmount() {
this.assetSession.close();
}

/**
* Sync subscription to change in queried data
*/
@Watch('query')
onUpdateProp(newProp: unknown, oldProp: unknown) {
/* TODO:
if (!isEqual(newProp, oldProp) && this.update != null) {
this.update({
query: this.query,
requestInfo: this.requestInfo,
});
}
*/
}

render() {
return (
<div>
<h2>{this.assetSummary?.name}</h2>
<p>{this.assetSummary?.arn}</p>
<p>
<b>Model: </b>
{this.assetModel?.assetModelName}
</p>
<ul>
{this.assetModel?.assetModelProperties?.map((property) => (
<li>
{property.name}: {this.assetPropertyValues.get(property?.id as string)}
</li>
))}
</ul>
</div>
);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const STRING_ASSET_ID = '9a9ca8e2-779d-443f-93a9-c287fd8f9c66';
const STRING_PROPERTY_ID = '9530e220-b353-4331-b4b3-cf0949c8684d';
const STRING_ASSET_ID = '888dbcd1-cdfe-44ba-a99b-0ad3ca19a019';
const STRING_PROPERTY_ID = '9bd13790-377b-429f-87b0-43382b1709fd';

const DEMO_TURBINE_ASSET_1 = '4df123fc-dc29-470e-8fd2-9242a2d3fa17';

Expand All @@ -13,6 +13,10 @@ export const STRING_QUERY = {
assets: [{ assetId: STRING_ASSET_ID, propertyIds: [STRING_PROPERTY_ID] }],
};

export const ASSET_DETAILS_QUERY = {
assetId: STRING_ASSET_ID,
};

export const NUMBER_QUERY = {
source: 'site-wise',
assets: [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Component, h } from '@stencil/core';
import { initialize } from '@iot-app-kit/core';
import { NUMBER_QUERY } from './siteWiseQueries';
import { NUMBER_QUERY, ASSET_DETAILS_QUERY } from './siteWiseQueries';
import { getEnvCredentials } from './getEnvCredentials';

const VIEWPORT = { duration: 3 * 1000 * 60 };
Expand All @@ -23,6 +23,7 @@ export class TestingGround {
<div style={{ width: '400px', height: '500px' }}>
<iot-line-chart query={NUMBER_QUERY} viewport={VIEWPORT} />
</div>
<iot-asset-details query={ASSET_DETAILS_QUERY} />
</div>
);
}
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"parse-duration": "^1.0.0",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0",
"rxjs": "^7.4.0",
"typescript": "4.4.4",
"uuid": "^3.3.2"
},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/asset-modules/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './sitewise/types';
export * from './sitewise/siteWiseAssetModule';
export * from './sitewise/session';
126 changes: 126 additions & 0 deletions packages/core/src/asset-modules/sitewise/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { SiteWiseAssetCache } from './cache';
import {
AssetPropertyValue,
AssetState,
AssetSummary,
DescribeAssetModelResponse,
DescribeAssetResponse,
Quality,
} from '@aws-sdk/client-iotsitewise';

const ASSET_ID = 'assetABC123';
const ASSET_MODEL_ID = 'assetModelABC123';
const ASSET_PROPERTY_ID = 'assetPropertyIdAbc123';
const creationDate: Date = new Date(2000, 0, 0);
const lastUpdatedDate: Date = new Date(2021, 0, 0);
const sampleAssetSummary: AssetSummary = {
id: ASSET_ID,
assetModelId: ASSET_MODEL_ID,
name: 'assetName',
arn: 'arn:assetArn',
creationDate: creationDate,
lastUpdateDate: lastUpdatedDate,
hierarchies: [],
status: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
};
const sampleAssetDescription: DescribeAssetResponse = {
assetId: ASSET_ID,
assetModelId: ASSET_MODEL_ID,
assetName: 'assetName',
assetArn: 'arn:assetArn',
assetCreationDate: creationDate,
assetLastUpdateDate: lastUpdatedDate,
assetHierarchies: [],
assetStatus: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
assetCompositeModels: [],
assetProperties: [],
};
const sampleAssetModel: DescribeAssetModelResponse = {
assetModelId: ASSET_MODEL_ID,
assetModelName: 'Asset Model Name',
assetModelDescription: 'a happy little asset model',
assetModelArn: 'arn:assetModelArn',
assetModelCreationDate: creationDate,
assetModelLastUpdateDate: lastUpdatedDate,
assetModelProperties: [],
assetModelCompositeModels: [],
assetModelHierarchies: [],
assetModelStatus: {
error: {
code: undefined,
details: undefined,
message: undefined,
},
state: AssetState.ACTIVE,
},
};
const samplePropertyValue: AssetPropertyValue = {
value: { stringValue: undefined, booleanValue: undefined, doubleValue: undefined, integerValue: 1234 },
quality: Quality.GOOD,
timestamp: {
timeInSeconds: 100,
offsetInNanos: 100,
},
};

describe('cacheAssetSummary', () => {
const cache: SiteWiseAssetCache = new SiteWiseAssetCache();
it('returns empty when the asset summary is not in the cache', () => {
expect(cache.getAssetSummary(ASSET_ID)).toBeUndefined();
});

it('returns the cached asset summary when one is stored', () => {
cache.storeAssetSummary(sampleAssetSummary);
expect(cache.getAssetSummary(ASSET_ID)).toEqual(sampleAssetSummary);
});
});

describe('cacheDescribeAsset', () => {
const cache: SiteWiseAssetCache = new SiteWiseAssetCache();
it('returns empty when the asset summary is not in the cache', () => {
expect(cache.getAssetSummary(ASSET_ID)).toBeUndefined();
});

it('returns the cached asset summary when an Asset Description is stored', () => {
cache.storeAssetSummary(sampleAssetDescription);
expect(cache.getAssetSummary(ASSET_ID)).toEqual(sampleAssetSummary);
});
});

describe('cacheAssetModel', () => {
const cache: SiteWiseAssetCache = new SiteWiseAssetCache();
it('returns empty when the asset model is not in the cache', () => {
expect(cache.getAssetModel(ASSET_MODEL_ID)).toBeUndefined();
});

it('returns the cached asset model when an Asset Model is stored', () => {
cache.storeAssetModel(sampleAssetModel);
expect(cache.getAssetModel(ASSET_MODEL_ID)).toEqual(sampleAssetModel);
});
});

describe('cacheAssetPropertyValue', () => {
const cache: SiteWiseAssetCache = new SiteWiseAssetCache();
it('returns empty when the asset property value is not in the cache', () => {
expect(cache.getPropertyValue(ASSET_ID, ASSET_PROPERTY_ID)).toBeUndefined();
});

it('returns the cached asset property value when an AssetPropertyValue is stored', () => {
cache.storePropertyValue(ASSET_ID, ASSET_PROPERTY_ID, samplePropertyValue);
expect(cache.getPropertyValue(ASSET_ID, ASSET_PROPERTY_ID)).toEqual(samplePropertyValue);
});
});
64 changes: 64 additions & 0 deletions packages/core/src/asset-modules/sitewise/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
AssetPropertyValue,
AssetSummary,
DescribeAssetResponse,
DescribeAssetModelResponse,
} from '@aws-sdk/client-iotsitewise';

export class SiteWiseAssetCache {
private assetCache: Record<string, AssetSummary> = {};
private assetModelCache: Record<string, DescribeAssetModelResponse> = {};
private propertyValueCache: Record<string, AssetPropertyValue> = {};

private convertToAssetSummary(assetDescription: DescribeAssetResponse): AssetSummary {
return {
id: assetDescription.assetId,
arn: assetDescription.assetArn,
name: assetDescription.assetName,
assetModelId: assetDescription.assetModelId,
creationDate: assetDescription.assetCreationDate,
lastUpdateDate: assetDescription.assetLastUpdateDate,
status: assetDescription.assetStatus,
hierarchies: assetDescription.assetHierarchies,
};
}

private readonly isDescribeAssetResponse = (
assetAny: AssetSummary | DescribeAssetResponse
): assetAny is DescribeAssetResponse => (assetAny as DescribeAssetResponse).assetId != undefined;

private assetPropertyValueKey(assetId: string, propertyId: string): string {
return assetId + ':' + propertyId;
}

public getAssetSummary(assetId: string): AssetSummary | undefined {
return this.assetCache[assetId];
}

public storeAssetSummary(assetAny: AssetSummary | DescribeAssetResponse): void {
let assetSummary: AssetSummary = this.isDescribeAssetResponse(assetAny)
? this.convertToAssetSummary(assetAny)
: assetAny;
if (assetSummary.id != undefined) {
this.assetCache[assetSummary.id] = assetSummary;
}
}

public getAssetModel(assetModelId: string): DescribeAssetModelResponse | undefined {
return this.assetModelCache[assetModelId];
}

public storeAssetModel(assetModel: DescribeAssetModelResponse) {
if (assetModel.assetModelId != undefined) {
this.assetModelCache[assetModel.assetModelId] = assetModel;
}
}

public getPropertyValue(assetId: string, propertyId: string): AssetPropertyValue | undefined {
return this.propertyValueCache[this.assetPropertyValueKey(assetId, propertyId)];
}

public storePropertyValue(assetId: string, propertyId: string, assetPropertyValue: AssetPropertyValue) {
this.propertyValueCache[this.assetPropertyValueKey(assetId, propertyId)] = assetPropertyValue;
}
}

0 comments on commit 1287af8

Please sign in to comment.