分类 产品 下的文章

引用文字

准备工作

  • OAuth2 认证基本知识;
  • Odoo 14版本,单机部署,最好不使用 docker 环境;
  • Odoo OAuth for Keycloak 扩展;
  • Keycloak,配置好 Client 和 User;

工作原理

1. Odoo 插件安装

下载插件,https://github.com/OCA/server-auth/tree/14.0/auth_oidc,配置到 Odoo 外部扩展插件中,安装,具体步骤看插件 Readme 文件即可;

2. 配置 Odoo 支持 Keycloak 认证,按照下图例子参考配置 Odoo 即可;

oauth1.png

3. Keycloak 配置,以便支持 Odoo;

Clients 配置基本功能即可,可参考:https://yiqisoft.cn/blogs/iot_platform/243.html,尤其需要配置一个 Mapper,匹配 email 地址用:
oauth2.png
创建 Keycloak 内置用户即可;
oauth4.png

4. Odoo 用户配置

已知内置用户可以配置成 OAuth2 用户,这样内置用户可以登录 Odoo,OAuth2 用户也可以登录;
oauth3.png
内置用户可以不创建,第一次登录 OAuth2 服务器后会自动创建,此时新创建用户无法使用 Odoo 登录,需要发送认证email后创建密码;

成果演示

ThingsBoard 官方已经发布一些 OAuth2 支持的例子,https://thingsboard.io/docs/user-guide/oauth-2-support/,有 Google,Auth0,其他比如 GitHub 也比较简单。这里介绍的是 Keycloak 的配置。

1、前提条件

1.1、ThingsBoard Oauth2 支持

首先确保你的 ThingsBoard 服务器版本支持 OAuth2,比如 V3.3.*,以 sysadmin 登录即可。

1.2、Keycloak 服务器

安装

过程比较简单,可根据自身需求选择安装方式;测试的时候可以用 docker ,方便快捷。

阅读手册

找到你能读懂的文档,简单熟悉配置方法。

2、配置

2.1、Keycloak 配置

新建 realm

keycloak2.png

复制 secret

keycloak3.png

增加 user

keycloak4.png

设置 user 密码

keycloak5.png

2.2、ThingsBoard 配置

keycloak1.png

尤其注意 一系列 URI,其中 YiSERVER 换成你的 真实 realm 即可。

访问令牌URI:

http://localhost:8081/realms/YiSERVER/protocol/openid-connect/token

授权URI:

http://localhost:8081/realms/YiSERVER/protocol/openid-connect/auth

JSON Web Key URI:

http://localhost:8081/realms/YiSERVER/protocol/openid-connect/certs

用户信息URI:

http://localhost:8081/realms/YiSERVER/protocol/openid-connect/userinfo

3、验证

3.1、登录 ThingsBoard

keycloak6.png

3.2、跳转到 Keycloak

keycloak7.png

3.3、完成 OAuth2 登录集成

keycloak8.png

3.4、视频预览

此二次开发主要是基于 ThingsBoard UI 界面上的 js 脚本开发,熟悉 js 开发的朋友应该很容易理解。
从 Dashboard 导出 csv 其实有好几种情况,1是 telemetry 数据导出,2是 attributes 数据导出。

准备工作

  • Chrome 浏览器
  • javascript 基础知识
  • ThingsBoard UI 二次开发基础知识
  • Dashbaord 开发经验

新建Dashboard

Telemetry 数据导出

1.png

导出csv文件内容:

timestamp,GasCon
2022-3-21 21:29:0,800
2022-3-21 21:44:0,800
2022-3-25 19:59:0,0

自定义 action,js脚本:

let $injector = widgetContext.$scope.$injector;
let attributeService = $injector.get(widgetContext
    .servicesMap.get('attributeService'));
let deviceService = $injector.get(widgetContext.servicesMap
    .get('deviceService'));

console.log(widgetContext);
let data = widgetContext.data;

let dataKeys = [];
// get all data keys to array
widgetContext.datasources[0].dataKeys.forEach(function(
    item) {
    dataKeys.push(item.name);
});

let fileTitle = "export-from-list-" + entityName;

function timeConverter(UNIX_timestamp) {
    var a = new Date(UNIX_timestamp);
    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
    ];
    var year = a.getFullYear();
    var month = a.getMonth()+1;
    var date = a.getDate();
    var hour = a.getHours();
    var min = a.getMinutes();
    var sec = a.getSeconds();
    var time = year + '-' + month + '-' + date + ' ' +
        hour + ':' + min + ':' + sec;
    return time;
}

function exportCSV() {
    // first line: header
    var allLine = "";
    dataKeys.unshift("timestamp");
    allLine += dataKeys;
    allLine += "\n";

    var key_length = data.length; // 字段数目
    var limit = data[0]["data"].length; // 数据行数
    var line = new Array(limit + 1); // 包括首行
    for (var l = 0; l < limit; l++)
        line[l] = "";
    data.forEach(
        (item, idx) => {
            var item_data = item["data"];
            var dataKey = item["dataKey"];
            for (var j = 0; j < limit; j++) {
                if (idx == 0) {

                    line[j] += timeConverter(item_data[
                        j][0]);
                    line[j] += ",";
                }
                line[j] += item_data[j][1];
                if (idx < key_length - 1)
                    line[j] += ",";
            }
            line[j] += "\n";
        }
    );
    for (var k = 0; k < limit; k++) {
        allLine += line[k];
        allLine += "\n";
    }

    var exportedFilenmae = fileTitle + '.csv' ||
        'export.csv';

    var blob = new Blob([allLine], {
        type: 'text/csv;charset=utf-8;'
    });
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob,
            exportedFilenmae);
    } else {
        var link = document.createElement("a");
        if (link.download !==
            undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download",
                exportedFilenmae);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

exportCSV();

Attributes 数据导出

2.png

导出csv文件内容:

timestamp,name,detector_tag,area,floor,location,department,equipment_name,gas,setup_date,sensor_date,cal_date,cal_result,ip,hub_port,sub_hub_port,maintenance
2022-3-28 21:25:9,CL96-06,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD013-HF,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD033-C-NH3,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD033-D-NH3,,,,,,,,,,,,,,,
2022-3-28 21:25:9,F-CL96-SRD037-CLF3,,,,,,,,,,,,,,,

自定义 action,js 脚本:

let $injector = widgetContext.$scope.$injector;
let attributeService = $injector.get(widgetContext
    .servicesMap.get('attributeService'));
let deviceService = $injector.get(widgetContext.servicesMap
    .get('deviceService'));

let data = widgetContext.data;
console.log(data);

let dataKeys = [];
// get all data keys to array
widgetContext.datasources[0].dataKeys.forEach(function(
    item) {
    dataKeys.push(item.name);
});

let fileTitle = "export-device-list";

function timeConverter(UNIX_timestamp) {
    var a = new Date(UNIX_timestamp);
    var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
    ];
    var year = a.getFullYear();
    var month = a.getMonth()+1;
    var date = a.getDate();
    var hour = a.getHours();
    var min = a.getMinutes();
    var sec = a.getSeconds();
    var time = year + '-' + month + '-' + date + ' ' +
        hour + ':' + min + ':' + sec;
    return time;
}

function exportCSV() {
    // first line: header
    var allLine = "";
    var key_length = dataKeys
        .length; // csv字段数目, 不含timestamp
    dataKeys.unshift("timestamp");
    allLine += dataKeys;
    // allLine += "\n";

    var limit = data.length / key_length; // csv数据行数
    var line = new Array(limit + 1); // csv总行数,包括首行
    for (var l = 0; l < limit; l++)
        line[l] = "";

    //var j = 0;
    data.forEach(
        (item, idx) => {
            var item_data = item["data"];

            if (idx % key_length == 0) {
                allLine += "\n"; // 换行
                allLine += timeConverter(item_data[0][0]); // timestamp
                allLine += ",";
            }

            allLine += item_data[0][1]; // value/值

            if ((idx+1) % key_length != 0)
                allLine += ",";
        }
    );
    
    allLine += "\n";
    // for (var k = 0; k < key_length; k++) {
    //     allLine += line[k];
    //     allLine += "\n";
    // }

    var exportedFilenmae = fileTitle + '.csv' ||
        'export.csv';

    var blob = new Blob([allLine], {
        type: 'text/csv;charset=utf-8;'
    });
    if (navigator.msSaveBlob) { // IE 10+
        navigator.msSaveBlob(blob,
            exportedFilenmae);
    } else {
        var link = document.createElement("a");
        if (link.download !==
            undefined) { // feature detection
            // Browsers that support HTML5 download attribute
            var url = URL.createObjectURL(blob);
            link.setAttribute("href", url);
            link.setAttribute("download",
                exportedFilenmae);
            link.style.visibility = 'hidden';
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
        }
    }
}

exportCSV();