ウィジェットプラグイン

ウィジェットプラグイン

ここではダッシュボードに追加できるウィジェットのプラグイン開発の概要について説明します。

ウィジェットプラグインの開発を手掛ける前にアプリケーションとプラグインの基本コンセプトおよび "Hello world!"スタイルデモプラグインを説明したintroductionの一読をお勧めします。

これらの他、ドキュメント内で照会されているプラグインは全てcumulocity-ui-plugin-examplesレポジトリで参照できます。

アイコンマップ プラグイン

アイコンマッププラグインを使用するとダッシュボード上にデバイスをアイコンとしてマップ上に表示されるウィジェットが新しく利用できるようになります。ウィジェットは以下のように表示されます:

アイコンマップウィジェット

こちらを表示するためには以下の手順を踏んでください:

  • プラグインを作成
  • アプリケーションマニフェストのインポート一覧にプラグインを定義.
  • ウィジェットメニューにアイテムを追加
  • デバイスの画像を取得
  • ウィジェットビューを作成

このプラグインを追加するアプリケーションをすでに作成されていると想定しますが、もし無い場合はレポジトリ内で提供されているアプリケーションをご使用ください。ここで紹介されている例題は"plugins/iconmap"フォルダ内に保存されています。

プラグインの作成

アプリケーションフォルダ内で以下のコマンドを走らせてください:

$ c8y create:plugin iconmap

/plugins/iconmap内のプラグインマニフェストを編集し、以下を追記してください:

{
  "name": "Icon Map",
  "description": "Shows devices on a map using an icon for the device type.",
}

次に以下の内容を含んだ"iconmap.module.js"ファイルをプラグインのルートフォルダ内に作成します:

(function () {
  'use strict';

  angular.module('myapp.iconmap', []);
}());

新規プラグインをインポート一覧へ追加するためにアプリケーションマニフェストを更新してください。

{
    ...
    "imports": [
        ...
        "myapplication/iconmap"
    ]
}

ウェジェットリスト上にアイテムを追加する方法

次に、ウィジェットメニューにアイテムを追加する設定ファイルを追加する必要があります。ここではCumulocity JavaScript APIで提供されている"c8yComponentsProvider"サービスを使用できます。このサービスを設定に挿入し以下の関数を呼んでください:

(function () {
  'use strict';

  angular
    .module('myapp.iconmap')
    .config(configure);

  configure.$inject = [
    'c8yComponentsProvider',
    'gettext'
  ];

  function configure(
    c8yComponentsProvider,
    gettext
  ) {
    c8yComponentsProvider.add({ // adds a menu item to the widget menu list with ...
      name: 'iconmap', // ... the identifier *"iconmap"* which has to be unique among the widgets in the application
      nameDisplay: gettext('Icon Map'), // ... the displayed name *"Icon Map"*
      description: gettext('Displays a map with icons for devices instead of markers'), // ... a description
      templateUrl: ':::PLUGIN_PATH:::/views/iconmap.main.html', // ... displaying *"iconmap.main.html"* when added to the dashboard
      options: { noDeviceTarget: true }
    });
  }
}());

デバイス用の画像を取得する方法

まずは、マップ上で表示されたデバイス全ての持つ配列「マーカー」を定義する必要があります。ここでは、デバイスのハードウェアモデルにより画像を割り当てます。画像を取得するには、"c8yBinary"サービスを使用してインベントリ内の全てのバイナリオブジェクトを取得する必要があります。そして、取得した全てのバイナリオブジェクトをフィルタリングし、特定のハードウェアモデルを表す画像を取得します。その後、各デバイスは"c8y_Position"フラグメントに応じてハードウェアモデルの画像があればその画像が、また、該当する画像が存在しない場合はいつものマーカーがマップに表示されます。

(function () {
  'use strict';

  angular
    .module('myapp.iconmap')
    .controller('iconmapController', iconmapController);

  iconmapController.$inject = [
    '$scope',
    '$q',
    'c8yInventory',
    'c8yBinary'
  ];

  function iconmapController(
    $scope,
    $q,
    c8yInventory,
    c8yBinary
  ) {
    $scope.markers = [];

    var getDevicesAndBinaries = {
      devices: getDevicesWithLocation(),
      binaries: c8yBinary.list({})
    };
    $q.all(getDevicesAndBinaries).then(placeTypes);

    function getDevicesWithLocation() {
      var filters = {fragmentType: 'c8y_Position' };
      return c8yInventory.list(filters);
    }

    function placeTypes(devicesAndBinaries) {
      var devicesOfType = createTypeMap(devicesAndBinaries.devices);
      var iconOfType = createIconMap(devicesAndBinaries.binaries);
      angular.forEach(devicesOfType, _.curry(placeType)(iconOfType));
    }

    function placeType(iconOfType, devices, type) {
      var icon = iconOfType[type];
      if (icon) {
        var placeDevices = _.curry(place)(devices);
        c8yBinary.downloadAsDataUri(icon).then(placeDevices);
      } else {
        place(devices);
      }
    }

    function createTypeMap(devices) {
      var typeMap = {};
      angular.forEach(devices, _.curry(addDeviceToTypeMap)(typeMap));
      return typeMap;
    }

    function addDeviceToTypeMap(typeMap, device) {
      var hw = 'default';
      if (device.c8y_Hardware && device.c8y_Hardware.model) {
        hw = device.c8y_Hardware.model;
      }

      if (!typeMap[hw]) {
        typeMap[hw] = [];
      }

      typeMap[hw].push(device);
    }

    function createIconMap(binaries) {
      var iconMap = {};
      angular.forEach(binaries, _.curry(addIconToIconMap)(iconMap));
      return iconMap;
    }

    function addIconToIconMap(iconMap, icon) {
      if (c8yBinary.isImage(icon)) {
        var name = icon.name;
        name = name.substring(0, name.lastIndexOf('.'));
        iconMap[name] = icon;
      }
    }

    function place(devices, uri) {
      angular.forEach(devices, _.curry(placeDevice)(_, uri));
    }

    function placeDevice(device, uri) {
      var pos = device.c8y_Position;
      var marker = {
        lat: pos.lat,
        lng: pos.lng,
        message: '<a href="#/device/' + device.id + '">' + device.name + '</a>'
      };

      if (uri) {
        marker.icon = { iconUrl: uri };
      }

      $scope.markers.push(marker);
    }
  }
}());

モジュール、設定、コントローラーを追加した今、"myapp.iconmap"をモジュールとして特定し、各javaスクリプトファイルをプラグインマニフェスト内に追加する必要があります:

{
    "name": "Icon Map",
    "description": "Shows devices on a map using an icon for the device type.",
  "ngModules": [
    "myapp.iconmap"
  ],
  "js": [
    "iconmap.module.js",
    "iconmap.config.js",
    "iconmap.controller.js"
  ]
}

ウィジェットのビューを作成

設定上すでにウィジェットのビューを含んでいる.htmlファイルを特定しました。この例題のウィジェットは簡単なマップ表示します。ビューにマップを追加する場合はプラグインフォルダ内に"views"フォルダを作成し、以下の記載を含む"iconmap.main.html"を作成してください:

<div ng-controller="iconmapController">
    <leaflet markers="markers" ></leaflet>
</div>

「リーフレット」タグウィジェットにインタラクティブマップを追加しています。デバイスをマップ上に表示する場合はコントローラーで定義した配列を「リーフレット」タグの「マーカー」属性に割り当てます。

プラグインを検証

After 自身のテナントにプラグインを展開した後、"Icon Map"ウィジェットを作成することができます。注意)デバイスの画像を表示したい場合自身のテナント内にある ファイルレポジトリ内にデバイスタイプ名の画像をアップロードする必要があります。

天気プラグイン

ここで紹介するプラグインはデバイスが置かれている場所の現在の天気情報をダッシュボード上で表示するウィジェットとなります。このウィジェットは以下の図のように表示されます:

天気ウィジェット

以下の手順を踏んでください:

  • Drak Sky APIを使用するプラグインを作成
  • APIキー入力用プラグインを作成
  • ナビゲーターメニューにアイテムを追加
  • ユーザーがAPIキーを保存できるビューを作成
  • ウィジェット用プラグインを作成
  • アプリケーションマニフェストのインポート一覧にプラグインを定義する。
  • ウィジェットリストにアイテムを追加
  • デバイスの天気情報を取得
  • ウィジェットのビューを作成

新規プラグインを追加するアプリケーションをすでに作成していると想定しますが、もしアプリケーションが無い場合はレポジトリ内で提供されている上記に記載されたアプリケーションをご使用ください。ここで紹介された例題は "plugins/weather"、"plugins/weatherAdmin"、"plugins/weatherService"の各フォルダ内に格納されています。

Drak Sky APIを使用したプラグインを作成

ここではレポジトリから "weatherService"をダウンロードし、自身のアプリケーション内に保存することをお勧めします。このプラグインはAPIキーを保存・ロードする機能および天気情報を取得する機能を提供します。

アプリケーションマニフェスト内にプラグインが含んでいることを確認してください:

{
    ...
    "imports": [
        ...
        "myapplication/weatherService"
    ]
}

APIキーの入力プラグインを作成

自身のアプリケーションフォルダ内で以下のコマンドを走らせてください:

$ c8y create:plugin weatherAdmin

/plugins/weatherAdmin内にあるプラグインマニフェストを編集し、以下の情報を追記してください:

{
  "name": "Weather settings",
  "description": "Configure the API key for weather forecasts",
  "icon": "cloud",
  "category": "Administrator",
  "imports": [
    "myapplication/weatherService"
  ]
}

APIキーをロード、およびユーザーが入力したAPIキーを保存する機能の備わっている"weatherService"プラグインをインポートします。

インポート一覧に新規プラグインを追加するためにアプリケーションマニフェストを更新します。

{
    ...
    "imports": [
        ...
        "myapplication/weatherAdmin"
    ]
}

ナビゲーションメニューにアイテムを追加する方法

次に、ナビゲーションメニューにアイテムを追加する設定ファイルを作成する必要があります。ここではCumulocity JavaScript API内で提供している"c8yNavigatorProvider"と"c8yViewsProvider"のサービスを使用します。サービスを設定内に挿入し以下の関数を呼んでください:

(function () {
  'use strict';

  angular
  .module('myapp.weatherAdmin', [ 'myapp.weatherService' ])
  .config(configure);

  configure.$inject = [
    'c8yNavigatorProvider',
    'c8yViewsProvider',
    'gettext'
  ];

  function configure(c8yNavigatorProvider, c8yViewsProvider, gettext) {
    c8yNavigatorProvider.addNavigation({ // adds a menu item to the navigator with ...
      parent: gettext('Settings'), // ... the category *"Settings"*
      name: gettext('Weather'), // ... the name *"Weather"*
      path: 'weather', // ... */weather* as path
      icon: 'cloud' // ... the cloud icon (icons are provided by the great Font Awesome library and you can use any of their [icon names](http://fontawesome.io/icons/) without the *fa-* prefix here
    });

    c8yViewsProvider.when('/weather', { // when the path "/weather" is accessed ...
      templateUrl: ':::PLUGIN_PATH:::/views/weatherAdmin.html' //  ... display our html file "weatherAdmin.html" inside the "views" folder of our plugin (the plugin's folder is represented using the magic string ```:::PLUGIN_PATH:::```, which is replaced by the actual path during the build process)
    });
  }
}());

コントローラーに、APIキーをロードする機能とユーザーが入力したAPIキーを保存する機能を導入する必要があります。APIキーをロードする機能は"weatherService"機能で提供されている"load"を使用します。

(function () {
  'use strict';

  angular
    .module('myapp.weatherAdmin')
    .controller('weatherAdminController', weatherAdminController);

  weatherAdminController.$inject = [
    '$scope',
    'c8yTitle',
    'weatherService',
    'gettext'
  ];

  function weatherAdminController($scope, c8yTitle, weatherService, gettext) {
    $scope.updateKey = updateKey;
    weatherService.load().then(function setOpt(key) {
      $scope.key = key;
    });

    c8yTitle.changeTitle({
      title: gettext('Weather provider settings')
    });

    function updateKey() {
      weatherService.save($scope.key);
    }
  }
}());

プラグイン内に設定とコントローラーを追加した今、"myapp.weatherAdmin"をモジュールとして特定し、各javaスクリプトをプラグインマニフェストに追加する必要があります:

{
  "name": "Weather settings",
  "description": "Configure the API key for weather forecasts",
  "icon": "cloud",
  "category": "Administrator",
  "imports": [
    "myapplication/weatherService"
  ],
  "ngModules": [
    "myapp.weatherAdmin"
  ],
  "js": [
    "weatheradmin.config.js"
    "weatheradmin.controller.js"
  ]
}

APIキーをユーザーが保存できるビューの作成方法

設定上すでにナビゲーションアイテムのビューの入った.htmlファイルを特定しました。ここでは簡単なテキストと入力フィールド、そして保存ボタンが表示させます。このビューをプラグインに追加するにはプラグインフォルダ内に"views"フォルダを作成し、以下の内容を記した"weatherAdmin.main.html"ファイルを作成します:

<div ng-controller="weatherAdminController">
  <div class="col-lg-6 panel panel-clean">
    <p translate>Weather functionality is based on the <a href="https://darksky.net" target="_blank">Dark Sky</a> service. Usage of Dark Sky requires an API key that can be obtained by registering at <a href="https://darksky.net/dev/" target="_blank">https://darksky.net/dev/</a>. Paste the API key below.</p>
    <form class="form-horizontal" name="weatherAdminForm" novalidate>
      <div class="form-group">
        <label for="key" class="control-label" translate>API Key</label>
        <div ng-class="{'has-error': invalid('license')}">
          <input type="text" class="form-control" required name="key" id="key" ng-model="key" c8y-autocomplete="off">
        </div>
      </div>
      <div class="form-group ">
        <button type="submit" class="btn btn-primary" ng-click="updateKey()"
                ng-disabled="weatherAdminForm.$invalid||weatherAdminForm.$pristine" translate>
          Save
        </button>
      </div>
    </form>
  </div>
</div>

ウィジェット用プラグインの作成

アプリケーションフォルダの中で以下のコマンドを走らせてください:

$ c8y create:plugin weather

/plugins/weatherにあるプラグインマニフェスト を編集し、以下の情報を追記してください:

{
  "name": "Weather",
  "description": "Shows the current weather at the location of a device.",
  "category": "Widgets",
  "icon": "cloud",
  "imports": [
    "myapplication/weatherService"
  ]
}

特定の場所の天気情報が得られる"weatherService"プラグインをインポートします。

このプラグインをインポートリストに追加するためにアプリケーションマニフェストを更新してください。

{
    ...
    "imports": [
        ...
        "myapplication/weather"
    ]
}

ウェジェットのメニューにアイテムを追加する方法

次に、メニューにアイテムを追加する設定ファイルを作成する必要があります。ここではCumulocity JavaScript APIで提供されている"c8yComponentsProvider"サービスが使用可能です。設定内にサービスを挿入し、以下の関数を呼んでください:

(function () {
  'use strict';

  angular
    .module('myapp.weather', [ 'myapp.weatherService' ])
    .config(configure);

  configure.$inject = [
    'c8yComponentsProvider',
    'gettext'
  ];

  function configure(c8yComponentsProvider, gettext) {
    c8yComponentsProvider.add({ // adds a menu item to the widget menu list with ...
      name: 'weather', // ... the identifier *"weather"* which has to be unique among the widgets in the application
      nameDisplay: gettext('Weather'), // ... the displayed name *"weather"*
      description: gettext('Shows the current weather at the location of a device'), // ... a description
      templateUrl: ':::PLUGIN_PATH:::/views/weather.main.html' // ... displaying *"weather.main.html"* when added to the dashboard
    });
  }
}());

デバイスの天気を取得

コントローラーでは、ウィジェットダイアログで選択されたデバイスの位置によって天気情報を取得します。もしデバイスが変更されるとウィジェットも更新されます。

(function () {
  'use strict';

  angular
  .module('myapp.weather')
  .controller('weatherController', weatherController);

  weatherController.$inject = [
    '$scope',
    '$q',
    'weatherService',
    'gettext',
    'c8yInventory'
  ];

  function weatherController($scope, $q, weatherService, gettext, c8yInventory) {
    $scope.$watch('child.config.device', function reInit(newVal, oldVal) {
      if (newVal && !angular.equals(newVal, oldVal)) {
        init();
      }
    }, true);
    init();

    function init() {
      getDevice().then(tryGetWeather).then(showWeather, printError);
    }

    function getDevice() {
      var deviceId = $scope.child.config.device.id;
      $scope.status = gettext('Retrieving device ...');
      return c8yInventory.detail(deviceId);
    }

    function tryGetWeather(res) {
      $scope.device = res.data;

      if (locationAvailable($scope.device)) {
        $scope.status = gettext('Retrieving weather ...');
        return getWeather($scope.device.c8y_Position);
      }

      $scope.status = gettext('Device has not reported a location, cannot retrieve weather.');
      return $q.reject();
    }

    function locationAvailable(device) {
      return device && device.c8y_Position && device.c8y_Position.lat && device.c8y_Position.lng;
    }

    function getWeather(coordinate) {
      return weatherService.weather.getCurrent(coordinate.lat, coordinate.lng);
    }

    function showWeather(weather) {
      $scope.weather = weather;
      $scope.windDirection = {
        'display': 'inline-block',
        '-ms-transform': rotate(weather),
        '-webkit-transform': rotate(weather),
        'transform': rotate(weather)
      };
      $scope.status = 'ready';
    }

    function printError() {
      $scope.status = gettext('Error retrieving weather information.');
    }

    function rotate(weather) {
      var direction = (weather.currently.windBearing + 180) % 360;
      return 'rotate(' + direction + 'deg)';
    }
  }
}());

プラグインに設定とコントローラーを追加した今、"myapp.weather"をモジュールとして特定し、各javaスクリプトファイルをプラグインマニフェスト内に追加する必要があります。

{
  "name": "Weather",
  "description": "Shows the current weather at the location of a device.",
  "category": "Widgets",
  "icon": "cloud",
  "imports": [
    "myapplication/weatherService"
  ],
  "ngModules": [
    "myapplication.weather"
  ],
  "js": [
    "weather.config.js",
    "weather.controller.js"
  ]
}

ウィジェット用のビューを作成

さきほど設定上では、ビューが保存された.htmlファイルを特定しました。ここで、デバイスが位置する温度、気圧、湿度、風力の各情報を記した簡単な表をウィジェット内に表示したいとします。ビューに表を追加したい場合プラグインフォルダ内に"views"フォルダを作成し、以下を追記した"weather.main.html"ファイルを作成します。

<div ng-controller="weatherController" style="padding: 10px">
  <div ng-show="status != 'ready'" class="alert alert-info">{{ status }}</div>
  <div ng-show="status == 'ready'">
    <table class="table">
      <tbody>
        <tr>
          <td>{{ 'Weather' | translate }}</td>
          <td>
            <dark-sky-icon icon="{{ weather.currently.icon }}" uib-tooltip="{{weather.currently.summary | translate }}" tooltip-append-to-body="true"></dark-sky-icon>
          </td>
        </tr>
        <tr>
          <td>{{ 'Temperature' | translate }}</td>
          <td>{{weather.currently.temperature}} C</td>
        </tr>
        <tr>
          <td>{{ 'Pressure' | translate }}</td>
          <td>{{weather.currently.pressure}} hPa</td>
        </tr>
        <tr>
          <td>{{ 'Humidity' | translate }}</td>
          <td>{{weather.currently.humidity * 100}} %</td>
        </tr>
        <tr>
          <td>{{ 'Wind' | translate }}</td>
          <td>{{weather.currently.windSpeed}} {{ 'm/s' | translate }}
            <span class="direction" ng-style="windDirection" uib-tooltip="{{weather.currently.windBearing}} {{ 'deg' | translate }}">↑</span>
          </td>
        </tr>
      </tbody>
    </table>
    <a href="https://darksky.net/poweredby/" target="_blank">Powered by Dark Sky</a>
  </div>
</div>

プラグインを検証

プラグインを自身のテナント上で展開 した後、天気ウィジェットを作成することが可能となっているはずです。注記)天気情報を表示するには最初にAPIキーを入力する必要があります。