1.1.3.11. カスタムウィジェット・microserviceコード例を確認する

本項では、本システムの実装において必要なカスタムウィジェット・microserviceのコード例を記載しています。

なお、本書中の設定値の「< >」の表記については、ご利用の環境により各自入力いただく箇所となります("<"から">"までを設定値に置き換えてください)。

microservice用コードを確認する

本実装のためのmicroservice作成用コードをまとめています。

注釈

  • 本項では、標準提供されていない「microservice」機能を利用しています。
    「microservice」のご利用にあたっては、提供環境や費用などの諸条件について個別対応とさせていただいております。
    「microservice」機能に関しては iot-app@ntt.com までご相談ください。
    案件相談、提供環境や費用などの諸条件に関しては iot-info@ntt.com までご相談ください。
  • 本項では開発用ディレクトリを「C:\tmp\microservice」としています。お使いの環境に合わせて読み替えてください。

「app.ts」

C:\tmp\microservice\app.ts

"use strict";

require("dotenv").config();

import * as express from 'express';
import { routes } from './routes';
import { convertMeasurementDataToCSV } from './createCSV';
import * as cron from 'node-cron'

const app: express.Express = express();

routes(app);
convertMeasurementDataToCSV();

//cron機能(本マイクロサービスを毎晩0時5分に定期実行する)
cron.schedule('0 05 0 * * *', () => {

  convertMeasurementDataToCSV();

});

app.use(express.json());
app.listen(80);
console.log('node-microservice started on port 80');

「routes.ts」

C:\tmp\microservice\routes.ts

"use strict";

import * as express from 'express';
import {formatingPredictionData} from './createPredictiondata'

//Azure上Berryサーバーの接続用アドレス、学習モデルのIDを指定
const baseUrl = "http://<BerryサーバーのパブリックIPアドレス>/api/v2/predict/<モデルID>";

export async function routes(app: express.Express) {

  app.route("/inference").get(async (req, res) => {

    const predictionString = formatingPredictionData();
    const predictionData = JSON.parse(predictionString);

    const requestOptions = {
      method: "POST",
      headers: {
         "Content-Type": "application/json",
         "Accept": "application/json",
      },
      body: JSON.stringify(predictionData)
    };

    await fetch(baseUrl, requestOptions)
      .then(response => response.json())
      .then(data => {
        res.send(data);
      })
      .catch(error => {
        console.error('エラーが発生:', error);
        res.send(error);
      });

  })

};

「createPredictiondata.ts」

C:\tmp\microservice\createPredictiondata.ts

import * as fs from 'fs';
import * as iconv from "iconv-lite";

function createFileName() {

    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() -1);

    //日付を2桁表記にフォーマット
    const month = ("0" + (yesterday.getMonth() + 1)).slice(-2);
    const Day = ("0" + yesterday.getDate()).slice(-2);

    const csvFileName = 'measurement_' + yesterday.getFullYear() + month + Day + '.csv';

    return csvFileName;

}

export function formatingPredictionData() {

    var datetime = "";
    var people = "";
    var week = "";
    var temperature = "";
    var weather = "";

    var data_split;

    //ファイル読み込み
    const csvFile = createFileName();
    const data = fs.readFileSync('./' + csvFile, 'utf-8');
    const encoded = iconv.encode(data, 'SJIS')

    var splitted = String(encoded).split("\n");

    //csvファイルのデータを列ごとで分割
    for (var i = 0; i < splitted.length; i++) {

        if (splitted[i].length == 0) {
            break;
        } else if (i == 0) {
            continue;
        }

        data_split = splitted[i].split(",");

        for (var k = 0; k < data_split.length; k++) {

            if (i == 1) {
                if (k == 0) {
                    datetime += '"' + data_split[k] + '"';
                } else if (k == 1) {
                    people += data_split[k];
                } else if (k == 2) {
                    week += '"' + data_split[k] + '"';
                } else if (k == 3) {
                    temperature += data_split[k];
                } else if (k == 4) {
                    weather += '"' + data_split[k] + '"';
                }

            } else {
                if (k == 0) {
                    datetime += ',"' + data_split[k] + '"';
                } else if (k == 1) {
                    people += "," + data_split[k];
                } else if (k == 2) {
                    week += ',"' + data_split[k] + '"';
                } else if (k == 3) {
                    temperature += "," + data_split[k];
                } else if (k == 4) {
                    weather += ',"' + data_split[k] + '"';
                }
            }
        }

    }

    const prediction_data = '{"data":{"datetime":[' + datetime + '],"people":[' + people + '],"week":[' + week + '],"temperature":[' + temperature + '],"weather":[' + weather + ']}}';
    console.log(prediction_data);
    return prediction_data;

}

「createCSV.ts」

C:\tmp\microservice\createCSV.ts

"use strict";

import fetch from 'node-fetch';
import * as readline from 'readline';
import * as fs from 'fs';
import { sendWasabi } from './wasabi';

let measureDate = '';
let rowArray = new Array();
let csvStr = '';

//Things Cloud内部での通信用URL
let baseUrl = "http://cumulocity:8111/measurement/measurements";
const pageSize = "pageSize=10";

//Things Cloud内部での通信用リクエストオプション
const requestOptions = {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "Accept": "text/csv",
    //Things Cloudの認証情報
    "Authorization": "Basic <Things Cloudの認証情報>"
  }
};

//Things CloudメジャーメントAPIを日付指定して実行するためのURLを生成
function createUrl() {

  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() -1);

  //日付を2桁表記にフォーマット
  const month = ("0" + (yesterday.getMonth() + 1)).slice(-2);
  const Day = ("0" + yesterday.getDate()).slice(-2);

  //計測データの取得範囲を設定
  const dateFrom = yesterday.getFullYear() + "-" + month + "-" + Day + 'T00:00:00.000%2B09:00';
  const dateTo = yesterday.getFullYear() + "-" + month + "-" + Day + 'T23:59:00.000%2B09:00';


  //Things CloudメジャーメントAPIを日付指定して実行するためのURLを生成
  const reqUrl = baseUrl + "?" + pageSize + "&dateTo=" + dateTo + "&dateFrom=" + dateFrom;

  return reqUrl;

}

//計測データ格納用のcsvファイルを作成
function createFile() {

  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() -1);

  //日付を2桁表記にフォーマット
  const month = ("0" + (yesterday.getMonth() + 1)).slice(-2);
  const Day = ("0" + yesterday.getDate()).slice(-2);

  //csvファイルを作成
  const filePath = './';
  const csvFileName = filePath + 'measurement_' + yesterday.getFullYear() + month + Day + '.csv';
  fs.writeFile(csvFileName, 'datetime.dt,people.p,week.we,temperature.t,weather.wt', 'utf-8', err => {
    if (err) console.log(err.message);
  });

  return csvFileName;

}

// 計測データを取得しcsvファイル化
export async function convertMeasurementDataToCSV() {

  console.log('node-microservice started');

  const reqUrl = createUrl();
  console.log(reqUrl);
  const csvFileName = String(createFile());

  //Things Cloudにリクエストを送り計測データを取得
  const res = await fetch(reqUrl, requestOptions);
  const body = await res.body;

  let reader = readline.createInterface({ input: body });
  let i = 1;
  for await (const line of reader) {

    //読み込んだ行をカンマ区切りの配列に格納する
    let itemData = line.split(',');
    if (i > 1) {
      if (measureDate != itemData[0]) {
        if (i > 2) {

          //csv出力関数を呼び出す
          writeCSV(rowArray, csvFileName);
        }
        rowArray = [];
      }

      //パラメーター値を配列にセット
      switch (itemData[3]) {
        case "datetime.dt":
          rowArray[0] = itemData[4];
          break;
        case "people.p":
          rowArray[1] = itemData[4];
          break;
        case "week.we":
          rowArray[2] = itemData[4];
          break;
        case "temperature.t":
          rowArray[3] = itemData[4];
          break;
        case "weather.wt":
          rowArray[4] = itemData[4];
          break;
        default:
          break;
      }
    }
    measureDate = itemData[0];
    i++;
  }

  console.log(rowArray)

  //csv出力関数を呼び出す
  writeCSV(rowArray, csvFileName);
  rowArray = [];

  const wasabi = sendWasabi();

}

//計測データをcsvファイルに書き込み
function writeCSV(dataArray: string[], csvFileName) {

  //csv用のパラメーター形式へ変換
  csvStr = "\n";
  for (let j = 0; j < 5; j++) {
    if (dataArray[j] == null) {
      dataArray[j] = "";
    }
    if (j == 0) {
      csvStr += dataArray[j];
    } else {
      csvStr += ",";
      csvStr += dataArray[j];
    }
  }

  //パラメーターをcsvファイルに出力
  try {
    fs.appendFileSync(csvFileName, csvStr, 'utf-8');
  }
  catch (e) {
    console.log(e.message);
  }
  csvStr = "";

}

「wasabi.ts」

C:\tmp\microservice\wasabi.ts

"use strict";

import * as AWS from 'aws-sdk';
import * as S3 from 'aws-sdk/clients/s3';
import * as fs from 'fs';
import { FetchClient, IFetchOptions } from "@c8y/client";
import * as iconv from "iconv-lite";

const baseUrl = process.env.C8Y_BASEURL;

//CSVファイルをアップロードするバケットの名前を記載
const bucketName = "<CSVファイルをアップロードするバケットの名前>"

//Wasabi送信用のファイル名を定義
function createFileName() {

    const yesterday = new Date();
    yesterday.setDate(yesterday.getDate() -1);

    //日付を2桁表記にフォーマット
    const month = ("0" + (yesterday.getMonth() + 1)).slice(-2);
    const Day = ("0" + yesterday.getDate()).slice(-2);

    const csvFileName = 'measurement_' + yesterday.getFullYear() + month + Day + '.csv';

    return csvFileName;

}

export function sendWasabi(): any {

    const filePath = "./";
    const csvFileName = createFileName();
    const objectKey = csvFileName;

    console.log('wasabi send');

    (async () => {
        try {

            //Wasabiのクレデンシャル情報
            const accessKey = "<Wasabiのアクセスキー>";
            const secretKey = "<Wasabiのシークレットキー>";

            const wasabiCredential = new AWS.Credentials(accessKey, secretKey);
            const config = new S3({
                endpoint: "s3.ap-northeast-1-ntt.wasabisys.com",
                s3ForcePathStyle: true,
                signatureVersion: 'v4'
            });

            const client = new S3({
                credentials: wasabiCredential,
                endpoint: config.endpoint,
                s3ForcePathStyle: true,
                signatureVersion: 'v4',
            });

            //createCSVで作成されたcsvファイルを読み込み、Shift-JISにエンコード
            const csvFile = String(fs.readFileSync(filePath + csvFileName));
            const encoded = iconv.encode(csvFile, 'SJIS');

            const putRequest = {
                Bucket: bucketName,
                Key: objectKey,
                Body: encoded
            };

            //Wasabiへ作成したcsvファイルを送信
            client.putObject(putRequest, (err) => {

                if (err) {
                    console.log(err);
                    return;
                }

                console.log('Successfully uploaded data to ${bucketName}/${objectKey}');
            });

        } catch (err) {
            console.log(err);
        }

    })();

}

「package.json」

C:\tmp\microservice\package.json

{
  "name": "nodemicro",
  "version": "1.0.0",
  "description": "Things Cloud microservice application",
  "main": "app.js",
  "dependencies": {
    "@aws-sdk/client-s3": "^3.347.1",
    "@c8y/client": "^1017.0.382",
    "@types/node": "^13.9.1",
    "@types/source-map-support": "^0.5.1",
    "aws-sdk": "^2.1392.0",
    "dotenv": "^8.1.0",
    "express": "^4.18.2",
    "node-cron": "^3.0.3",
    "source-map-support": "^0.5.16"
  },
  "scripts": {
    "start": "node app.js",
    "build": "tsc -p .",
    "build:watch": "tsc -p . -w",
    "microservice2": "rm image.tar && rm csvapi.zip && tsc -p . && docker rmi -f csvapi && docker build -t csvapi . && docker save -o image.tar csvapi && jar cvf apitest.zip cumulocity.json image.tar",
    "microservice": "tsc -p . && docker rmi -f csvapi && docker build -t csvapi . && docker save -o image.tar csvapi && jar cvf apitest.zip cumulocity.json image.tar",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Your name",
  "license": "MIT",
  "devDependencies": {
    "@types/express": "^4.17.21"
  }
}

「tsconfig.json」

C:\tmp\microservice\tsconfig.json

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude":["node_modules"]
}

「Dockerfile」

C:\tmp\microservice\Dockerfile

FROM node:alpine

WORKDIR /usr/app

COPY ./package.json ./
RUN npm install
COPY ./*.js ./

CMD ["npm", "start"]

「cumulocity.json」

C:\tmp\microservice\cumulocity.json

{
    "apiVersion": "v1",
    "version": "1.0.0-SNAPSHOT",
    "contextPath": "prediction-ms",
    "provider": {
        "name": "NTT Communications"
    },
    "isolation": "MULTI_TENANT",
    "requiredRoles": [
    ],
    "roles": [
    ]
}

カスタムウィジェット用コードを確認する

本実装のためのカスタムウィジェット作成用コードをまとめています。

注釈

  • 本項では開発用ディレクトリを「C:\tmp\berry-widget」としています。お使いの環境に合わせて読み替えてください。

「package.json」

C:\tmp\berry-widget\package.json

{
  "name": "cockpit",
  "version": "1.0.0",
  "description": "View Prediction Result Widget",
  "main": "index.js",
  "scripts": {
    "start": "c8ycli server -u https://<Things Cloudのテナント名>.je1.thingscloud.ntt.com",
    "build": "c8ycli build",
    "deploy": "c8ycli deploy",
    "test": "karma start karma.conf.js"
    },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@angular/animations": "8.2.13",
    "@angular/cdk": "8.2.3",
    "@angular/common": "8.2.13",
    "@angular/compiler": "8.2.13",
    "@angular/core": "^8.2.13",
    "@angular/forms": "8.2.13",
    "@angular/platform-browser": "8.2.13",
    "@angular/platform-browser-dynamic": "8.2.13",
    "@angular/router": "8.2.13",
    "@angular/upgrade": "8.2.13",
    "@aws-sdk/client-s3": "^3.347.1",
    "@c8y/ng1-modules": "1006.6.32",
    "@c8y/ngx-components": "1006.6.32",
    "@c8y/style": "1006.6.32",
    "@observablehq/plot": "^0.3.2",
    "@types/dom-mediacapture-record": "^1.0.7",
    "angular": "1.6.9",
    "aws-sdk": "^2.1392.0",
    "core-js": "^3.4.0",
    "cumulocity-ui-build": "http://resources.cumulocity.com/webapps/ui/9.20.16.tar.gz",
    "hls": "0.0.1",
    "hls.js": "^0.14.16",
    "jquery": "^3.5.1",
    "rxjs": "~6.4.0",
    "url-search-params-polyfill": "6.0.0",
    "zone.js": "~0.9.1"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.803.23",
    "@angular/compiler-cli": "8.2.13",
    "@angular/language-service": "8.2.13",
    "@c8y/cli": "1006.6.32",
    "@types/jasmine": "^3.5.7",
    "bootstrap": "^4.5.0",
    "css-loader": "^3.4.2",
    "html-loader": "^1.1.0",
    "jasmine": "^3.6.1",
    "jasmine-core": "^3.6.0",
    "karma": "^5.1.0",
    "karma-chrome-launcher": "^3.1.0",
    "karma-jasmine": "^3.3.1",
    "karma-jasmine-html-reporter": "^1.5.4",
    "karma-safari-launcher": "^1.0.0",
    "karma-sourcemap-loader": "^0.3.7",
    "karma-webpack": "^4.0.2",
    "local-ssl-proxy": "^2.0.5",
    "mockdate": "^3.0.5",
    "to-string-loader": "^1.1.6",
    "ts-loader": "^8.0.1",
    "typescript": "3.5.3",
    "webpack-dev-server": "^3.11.2"
  },
  "c8y": {
    "application": {
      "name": "cockpit",
      "contextPath": "cockpit",
      "key": "cockpit-application-key",
      "brandingCssVars": {
        "brand-primary": "#1357ad",
        "brand-complementary": "#a4c8cd",
        "brand-dark": "#0e407f",
        "brand-light": "#94aac5",
        "gray-text": "#444",
        "link-color": "#1357ad",
        "link-hover-color": "#0b3468",
        "body-background-color": "#f8f8f8",
        "brand-logo-img": "url(/apps/public/ui-assets-ntt/logo.svg?nocache=8324918234)",
        "brand-logo-img-height": "25%",
        "navigator-platform-logo": "url(/apps/public/ui-assets-ntt/tenant-logo.svg?nocache=8324918234)",
        "navigator-platform-logo-height": "23%",
        "navigator-font-family": "'Frutiger Neue', 'Helvetica Neue', Helvetica, Arial, sans-serif",
        "navigator-app-name-size": "16px",
        "navigator-app-icon-size": "0",
        "navigator-bg-color": "#1357ad",
        "navigator-header-bg": "white",
        "navigator-title-color": "#1357ad",
        "navigator-text-color": "rgba(255, 255, 255, 0.7)",
        "navigator-separator-color": "rgba(255, 255, 255, 0.05)",
        "navigator-active-color": "#1357ad",
        "navigator-active-bg": "#fdd34c",
        "header-color": "white",
        "header-text-color": "#1357ad",
        "header-hover-color": "var(--brand-primary, #1357ad)",
        "header-border-color": "rgba(117, 117, 117, 0.05)",
        "font-family-base": "'Frutiger Neue', 'Helvetica Neue', Helvetica, Arial, sans-serif",
        "headings-font-family": "'Frutiger Neue', 'Helvetica Neue', Helvetica, Arial, sans-serif"
      },
      "extraCssUrls": [
        "/apps/public/ui-assets-ntt/style.css"
      ],
      "faviconUrl": "/apps/public/ui-assets-ntt/favicon.ico?nocache=08563753891373449",
      "globalTitle": "NTT",
      "docsBaseUrl": "https://developer.ntt.com/iot/docs",
      "tabsHorizontal": true,
      "upgrade": true,
      "rightDrawer": false,
      "sensorPhone": false,
      "contentSecurityPolicy": "base-uri 'self'; default-src 'self' 'unsafe-inline' http: https: ws: wss:; connect-src 'self' *.billwerk.com http: https: ws: wss: blob:;  script-src 'self' open.mapquestapi.com *.twitter.com *.twimg.com swc.safie.link 'unsafe-inline' 'unsafe-eval' data:; style-src * 'unsafe-inline' blob:; img-src * data: blob:; font-src * data:; frame-src *; media-src * data: blob:;"
    },
    "cli": {}
  }
}

「app.module.ts」

C:\tmp\berry-widget\app.module.ts

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule as NgRouterModule } from '@angular/router';
import { UpgradeModule as NgUpgradeModule } from '@angular/upgrade/static';
import { HttpClientModule } from '@angular/common/http';

import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
import { CollapseModule } from 'ngx-bootstrap/collapse';
import { BsDatepickerModule } from 'ngx-bootstrap/datepicker';
import { TimepickerModule } from 'ngx-bootstrap/timepicker';

import { CoreModule, RouterModule } from '@c8y/ngx-components';
import {
  DashboardUpgradeModule,
  UpgradeModule,
  HybridAppModule,
  UPGRADE_ROUTES
} from '@c8y/ngx-components/upgrade';
import { AssetsNavigatorModule } from '@c8y/ngx-components/assets-navigator';
import { CockpitDashboardModule } from '@c8y/ngx-components/context-dashboard';
import { ReportsModule } from '@c8y/ngx-components/reports';
import { SensorPhoneModule } from '@c8y/ngx-components/sensor-phone';
import { BinaryFileDownloadModule } from '@c8y/ngx-components/binary-file-download';

import { BerryModule } from './custom-widgets/berry.module';

@NgModule({
  imports: [
    // Upgrade module must be the first
    UpgradeModule,
    BrowserAnimationsModule,

    BsDropdownModule.forRoot(),
    CollapseModule.forRoot(),
    BsDatepickerModule.forRoot(),
    TimepickerModule.forRoot(),

    RouterModule.forRoot(),
    NgRouterModule.forRoot([...UPGRADE_ROUTES], { enableTracing: false, useHash: true }),
    CoreModule.forRoot(),
    AssetsNavigatorModule,
    ReportsModule,
    NgUpgradeModule,
    DashboardUpgradeModule,
    CockpitDashboardModule,
    SensorPhoneModule,
    BinaryFileDownloadModule,

    HttpClientModule,
    BerryModule,
  ],
  declarations: [
  ],
  entryComponents: [
  ],
  providers: [
  ],
  schemas: [
    CUSTOM_ELEMENTS_SCHEMA
  ]
})
export class AppModule extends HybridAppModule {
  constructor(protected upgrade: NgUpgradeModule) {
    super();
  }
}

「index.ts」

C:\tmp\berry-widget\index.ts

import './polyfills';
import './ng1';

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';

declare const __MODE__: string;
if (__MODE__ === 'production') {
  enableProdMode();
}

export function bootstrap() {
  return platformBrowserDynamic()
    .bootstrapModule(AppModule).catch((err) => console.log(err));
}

「karma.conf.js」

C:\tmp\berry-widget\karma.conf.js

// Karma configuration
// Generated on Mon Jul 27 2020 10:38:49 GMT+0900 (日本標準時)

module.exports = function(config) {
  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: './custom-widgets',


    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: ['jasmine'],


    plugins: [  // 追記箇所
      require("karma-jasmine"),
      require("karma-chrome-launcher"),
      require("karma-safari-launcher"),
      require("karma-jasmine-html-reporter"),
      require("karma-webpack"),
      require("karma-sourcemap-loader")
    ],

    client:{
      clearContext: false
    },

    // list of files / patterns to load in the browser
    files: [
      "../node_modules/zone.js/dist/zone.js",
      "./test.ts"
    ],


    // list of files / patterns to exclude
    exclude: [
    ],


    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      "./test.ts": ["webpack", "sourcemap"]
    },

    webpack: { // 追記箇所: webpack の設定
      resolve: {
        extensions: [".ts", ".js"]
      },
      module: {
        rules: [
          { test: /\.ts$/, use: [{ loader: "ts-loader" }] },
          { test: /\.html$/, use: [{ loader: "html-loader" }] }, // 外部htmlファイルの読み込み用
          { test: /\.css$/, loaders: ["to-string-loader", "style-loader", "css-loader"] } // 外部cssファイルの読み込み用
        ]
      },
      mode: "development",
      devtool: 'inline-source-map'
    },



    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress', 'kjhtml'],


    // web server port
    port: 9876,


    // enable / disable colors in the output (reporters and logs)
    colors: true,


    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,


    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: true,


    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['Chrome'],


    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: false,

    // Concurrency level
    // how many browser should be started simultaneous
    concurrency: Infinity,

    // timeout
    browserNoActivityTimeout: 60000
  })
}

「ng1.ts」

C:\tmp\berry-widget\ng1.ts

import '@c8y/ng1-modules/core';
import '@c8y/ng1-modules/imageWidget/cumulocity.json';
import '@c8y/ng1-modules/assetPropertyWidget/cumulocity.json';
import '@c8y/ng1-modules/devicePropertyWidget/cumulocity.json';
import '@c8y/ng1-modules/alarms/cumulocity.json';
import '@c8y/ng1-modules/assetTable/cumulocity.json';
import '@c8y/ng1-modules/eventsBinary/cumulocity.json';
import '@c8y/ng1-modules/devicemanagement-alarmList/cumulocity.json';
import '@c8y/ng1-modules/deviceSelector/cumulocity.json';
import '@c8y/ng1-modules/kpi/cumulocity.json';
import '@c8y/ng1-modules/kpiAdmin/cumulocity.json';
import '@c8y/ng1-modules/devicemanagement-location/cumulocity.json';
import '@c8y/ng1-modules/dashboard2/cumulocity.json';
import '@c8y/ng1-modules/dashboardUI/cumulocity.json';
import '@c8y/ng1-modules/groupsHierarchy/cumulocity.json';
import '@c8y/ng1-modules/measurements/cumulocity.json';
import '@c8y/ng1-modules/map/cumulocity.json';
import '@c8y/ng1-modules/alarmAssets/cumulocity.json';
import '@c8y/ng1-modules/deviceControlMessage/cumulocity.json';
import '@c8y/ng1-modules/deviceControlRelay/cumulocity.json';
import '@c8y/ng1-modules/deviceControlRelayArray/cumulocity.json';
import '@c8y/ng1-modules/cockpit-cockpitConfig/cumulocity.json';
import '@c8y/ng1-modules/cockpit-assetCount/cumulocity.json';
import '@c8y/ng1-modules/cockpit-alarmRecent/cumulocity.json';
import '@c8y/ng1-modules/cockpit-reports/cumulocity.json';
import '@c8y/ng1-modules/cockpit-smartRulesUI/cumulocity.json';
import '@c8y/ng1-modules/cockpit-dataPointExplorerUI/cumulocity.json';
import '@c8y/ng1-modules/alarmsEventsExplorer/cumulocity.json';
import '@c8y/ng1-modules/deviceDatabase4/cumulocity.json';
import '@c8y/ng1-modules/modbusWidget4/cumulocity.json';
import '@c8y/ng1-modules/scada/cumulocity.json';
import '@c8y/ng1-modules/imageWidget/cumulocity.json';
import '@c8y/ng1-modules/htmlWidget/cumulocity.json';
import '@c8y/ng1-modules/applicationLinks/cumulocity.json';
import '@c8y/ng1-modules/quickLinks/cumulocity.json';
import '@c8y/ng1-modules/helpAndSupport/cumulocity.json';
import '@c8y/ng1-modules/eventList/cumulocity.json';
import '@c8y/ng1-modules/export/cumulocity.json';
import '@c8y/ng1-modules/dataPointTable/cumulocity.json';
import '@c8y/ng1-modules/switchDisplay/cumulocity.json';
import '@c8y/ng1-modules/trafficLightWidget/cumulocity.json';
import '@c8y/ng1-modules/infoGauge/cumulocity.json';

「polyfills.ts」

C:\tmp\berry-widget\polyfills.ts

/**
 * This file includes polyfills needed by Angular and is loaded before the app.
 * You can add your own extra polyfills to this file.
 *
 * This file is divided into 2 sections:
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
 *      file.
 *
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
 * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
 * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
 *
 * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
 */

/***************************************************************************************************
 * BROWSER POLYFILLS
 */

/** IE9, IE10, IE11, Evergreen browsers require the following polyfills. */
import "@angular-devkit/build-angular/src/angular-cli-files/models/es5-jit-polyfills.js";
import "@angular-devkit/build-angular/src/angular-cli-files/models/es5-polyfills.js";
import "@angular-devkit/build-angular/src/angular-cli-files/models/jit-polyfills.js";

/**
 * By default, zone.js will patch all possible macroTask and DomEvents
 * user can disable parts of macroTask/DomEvents patch by setting following flags
 */

(window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
// (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
(window as any).__zone_symbol__BLACK_LISTED_EVENTS = [
  "scroll",
  "mousemove",
  "message",
  "mouseover",
  "mouseout",
  "mouseenter",
  "mouseleave",
]; // disable patch specified eventNames

/***************************************************************************************************
 * Zone JS is required by default for Angular itself.
 */
import "zone.js/dist/zone"; // Included with Angular CLI.

/***************************************************************************************************
 * APPLICATION IMPORTS
 */
import "url-search-params-polyfill";
import { addPolyfills } from "@c8y/ngx-components/polyfills";
addPolyfills();

「tsconfig.json」

C:\tmp\berry-widget\tsconfig.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "module": "commonjs",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "dom",
      "es2016"
    ],
    "skipLibCheck": true
  }
}

「custom-widgets\preview-berry-view.ts」

C:\tmp\berry-widget\custom-widgets\preview-berry-view.ts

export const previewImage = 'data:image/png;base64,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'

「custom-widgets\berry.module.ts」

C:\tmp\berry-widget\custom-widgets\berry.module.ts

import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core";
import { CommonModule } from "@angular/common";
import { FormsModule } from "@angular/forms";
import { HOOK_COMPONENTS } from "@c8y/ngx-components";

import { CollapseModule } from "ngx-bootstrap/collapse";
import { AccordionModule } from "ngx-bootstrap";
import { BsDatepickerModule, BsDatepickerConfig } from "ngx-bootstrap";

import { BerryView } from "./berry-view-widget/berry-view.component";
import { BerryViewConfig } from "./berry-view-widget/berry-view-config.component";

import * as previewBerryView from "./preview-berry-view";

@NgModule({
  imports: [CommonModule, FormsModule, CollapseModule.forRoot(), AccordionModule.forRoot(), BsDatepickerModule.forRoot()],
  declarations: [
    BerryView,
    BerryViewConfig,
  ],
  entryComponents: [
    BerryView,
    BerryViewConfig,
  ],
  providers: [
    {
      provide: HOOK_COMPONENTS,
      multi: true,
      useValue: {
        id: "berry-view.widget",
        label: "推論結果表示ウィジェット",
        previewImage: previewBerryView.previewImage,
        description: "推論結果を表示するウィジェットです",
        component: BerryView,
        configComponent: BerryViewConfig,
      },
    },

    BsDatepickerConfig,
  ],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class BerryModule {}

「custom-widgets\berry-view-widget\berry-view-config.component.html」

C:\tmp\berry-widget\custom-widgets\berry-view-widget\berry-view-config.component.html

<div class="form-group">
  <c8y-form-group>
    <label translate>Berry View Config</label>
  </c8y-form-group>
</div>

「custom-widgets\berry-view-widget\berry-view-config.component.ts」

C:\tmp\berry-widget\custom-widgets\berry-view-widget\berry-view-config.component.ts

import { Component, Input } from "@angular/core";

@Component({
  selector: "berry-view-config",
  templateUrl: "./berry-view-config.component.html",
})
export class BerryViewConfig {
  @Input() config: any = {};
}

「custom-widgets\berry-view-widget\berry-view.component.css」

C:\tmp\berry-widget\custom-widgets\berry-view-widget\berry-view.component.css

.video-container {
  text-align: center;
}

.sticky-container {
  position: sticky;
  top: 0px;
  background-color: #ffffff;
  z-index: 100;
}

.video {
  width: 95%;
  margin: auto;
}
i.fa-play-circle-o {
  font-size: 30px;
}
.play-btn {
  color: #3f4a50;
  cursor: pointer;
}
.play-btn:hover {
  color: #1776bf;
}
.timeline-list>div.active .timeline-item-content .list-item-actions:before, .timeline-list>li.active .timeline-item-content .list-item-actions:before {
  display: none !important;
}

「custom-widgets\berry-view-widget\berry-view.component.html」

C:\tmp\berry-widget\custom-widgets\berry-view-widget\berry-view.component.html

<label for="time">推論時刻:</label>
<input type="time" id="time" name="推論時刻" value="" [(ngModel)]="time" />
<button class="btn btn-primary btn-lg btn-block" (click)="getPredictionResult()">推論</button>
<p>推論結果 : {{ predictionResult }}</p>

「custom-widgets\berry-view-widget\berry-view.component.ts」

C:\tmp\berry-widget\custom-widgets\berry-view-widget\berry-view.component.ts

import { preserveWhitespacesDefault } from "@angular/compiler";
import { Component, Input } from "@angular/core";
import { FetchClient, IFetchOptions } from "@c8y/client";

@Component({
  selector: "berry-view",
  templateUrl: "./berry-view.component.html",
  styleUrls: ["./berry-view.component.css"],
})

export class BerryView {
  @Input() config;


  res = "";
  time = "";
  predictionResult = "";


  //非同期通信先のURLを指定する
  URL = '/service/prediction-ms/inference';
  //API実行のオプションを設定
  fetchOptions: IFetchOptions = {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    }
  };

  constructor(
    private fetchClient: FetchClient
  ) { }

  async getPredictionResult() {

    //microserviceのAPIを実行
    await this.fetchClient.fetch(this.URL, this.fetchOptions)
    .then(response => response.json())
      .then(data => {
        console.log(data);
        const predictionData = JSON.stringify(data)
        this.res = predictionData;
        this.extractingPredictionResult(predictionData);
      })
      .catch(error => {
        console.error('エラーが発生:', error);
      });

  }

  //responseから指定された時間の推論結果を抽出
  extractingPredictionResult(predictionData) {

    var timeFormat = this.time + ":00";
    var predictionDataParse= JSON.parse(predictionData);
    var predictionIndex = predictionDataParse.result.index;
    var predictionResult = predictionDataParse.result.prediction;

    for (var i = 0; i < predictionIndex.length; i++) {

      var predictionIndexSplit = predictionIndex[i].split("T");

      if (predictionIndexSplit[1] == timeFormat) {
        this.predictionResult = predictionResult[i];
        break;
      }

    }

  }
}