commit 56434da37c27246812822f95ce366defeec908e4 Author: zhongjin Date: Wed Aug 8 21:31:17 2018 +0800 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e4a6c09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +.idea +tmp +admin/i18n/flat.txt +admin/i18n/*/flat.txt +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..0f2ae68 --- /dev/null +++ b/.npmignore @@ -0,0 +1,9 @@ +gulpfile.js +tasks +tmp +test +.travis.yml +appveyor.yml +admin/i18n +iob_npm.done +package-lock.json \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffda96f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,34 @@ +os: + - linux + - osx +language: node_js +node_js: + - '4' + - '6' + - '8' + - '10' +services: + - mysql + - postgresql +before_install: + - 'if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export export CXX=g++-4.8; fi' +before_script: + - export NPMVERSION=$(echo "$($(which npm) -v)"|cut -c1) + - 'if [[ $NPMVERSION == 5 ]]; then npm install -g npm@5; fi' + - npm -v + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install mariadb && mysql.server start; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then rm -rf /usr/local/var/postgres; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then initdb /usr/local/var/postgres; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pg_ctl -D /usr/local/var/postgres start; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then sleep 5; fi' + - 'if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then createuser -s postgres; fi' + - sleep 15 + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +addons: + apt: + sources: + - ubuntu-toolchain-r-test + packages: + - g++-4.8 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..407bce1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015-2018 bluefox , Apollon77 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e067ec0 --- /dev/null +++ b/README.md @@ -0,0 +1,506 @@ +![Logo](admin/sql.png) +# yunkong2.sql + +================================== + +[![NPM version](http://img.shields.io/npm/v/yunkong2.sql.svg)](https://www.npmjs.com/package/yunkong2.sql) +[![Downloads](https://img.shields.io/npm/dm/yunkong2.sql.svg)](https://www.npmjs.com/package/yunkong2.sql) +[![Tests](https://travis-ci.org/yunkong2/yunkong2.sql.svg?branch=master)](https://travis-ci.org/yunkong2/yunkong2.sql) + +[![NPM](https://nodei.co/npm/yunkong2.sql.png?downloads=true)](https://nodei.co/npm/yunkong2.sql/) + +This adapter saves state history into SQL DB. + +Supports PostgreSQL, mysql, Microsoft SQL Server and sqlite. +You can leave port 0 if default port is desired. + +### MS-SQL: +Use ```localhost\instance``` for host and check that TCP/IP connections are enabled. +https://msdn.microsoft.com/en-us/library/bb909712(v=vs.90).aspx + +### SQLite: +is "file"-DB and cannot manage too many events. If you have a big amount of data use real DB, like PostgreSQL and co. + +SQLite DB must not be installed extra. It is just a file on disk, but to install it you require build tools on your system. For linux, just write: + +``` +sudo apt-get install build-essential +``` + +For windows: + +``` +c:\>npm install --global --production windows-build-tools +``` + +And then reinstall the adapter, e.g: + +``` +cd /opt/yunkong2 +yunkong2 stop sql +npm install yunkong2.sql --production +yunkong2 start sql +``` + +### MySQL: +You can install mysql on linux systems: + +``` +apt-get install mysql-server mysql-client + +mysql -uroot -p + +CREATE USER 'yunkong2'@'%' IDENTIFIED BY 'yunkong2'; +GRANT ALL PRIVILEGES ON * . * TO 'yunkong2'@'%'; +FLUSH PRIVILEGES; +``` + +If required edit */etc/mysql/my.cnf* to set bind to IP-Address for remote connect. + +**Warning**: yunkong2 user is "admin". If required give limited rights to yunkong2 user. + +## Structure of the DBs +Default Database name is "yunkong2", but it can be changed in configuration. +### Sources +This table is a list of adapter's instances, that wrote the entries. (state.from) + +| DB | Name in query | +|------------|----------------------| +| MS-SQL | yunkong2.dbo.sources | +| MySQL | yunkong2.sources | +| PostgreSQL | sources | +| SQLite | sources | + +Structure: + +| Field | Type | Description | +|-------|--------------------------------------------|-------------------------------------------| +| id | INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1) | unique ID | +| name | varchar(255) / TEXT | instance of adapter, that wrote the entry | + +*Note:* MS-SQL uses varchar(255), and others use TEXT + +### Datapoints +This table is a list of datapoints. (IDs) + +| DB | Name in query | +|------------|-------------------------| +| MS-SQL | yunkong2.dbo.datapoints | +| MySQL | yunkong2.datapoints | +| PostgreSQL | datapoints | +| SQLite | datapoints | + +Structure: + +| Field | Type | Description | +|-------|--------------------------------------------|-------------------------------------------------| +| id | INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1) | unique ID | +| name | varchar(255) / TEXT | ID of variable, e.g. hm-rpc.0.JEQ283747.1.STATE | +| type | INTEGER | 0 - number, 1 - string, 2 - boolean | + +*Note:* MS-SQL uses varchar(255), and others use TEXT + +### Numbers +Values for states with type "number". **ts** means "time series". + +| DB | Name in query | +|------------|-------------------------| +| MS-SQL | yunkong2.dbo.ts_number | +| MySQL | yunkong2.ts_number | +| PostgreSQL | ts_number | +| SQLite | ts_number | + +Structure: + +| Field | Type | Description | +|--------|--------------------------------------------|-------------------------------------------------| +| id | INTEGER | ID of state from "Datapoints" table | +| ts | BIGINT / INTEGER | Time in ms till epoch. Can be converted to time with "new Date(ts)" | +| val | REAL | Value | +| ack | BIT/BOOLEAN | Is acknowledged: 0 - not ack, 1 - ack | +| _from | INTEGER | ID of source from "Sources" table | +| q | INTEGER | Quality as number. You can find description [here](https://git.spacen.net/yunkong2/yunkong2/blob/master/doc/SCHEMA.md#states) | + +*Note:* MS-SQL uses BIT, and others use BOOLEAN. SQLite uses for ts INTEGER and all others BIGINT. + + +### Strings +Values for states with type "string". + +| DB | Name in query | +|------------|-------------------------| +| MS-SQL | yunkong2.dbo.ts_string | +| MySQL | yunkong2.ts_string | +| PostgreSQL | ts_string | +| SQLite | ts_string | + +Structure: + +| Field | Type | Description | +|--------|--------------------------------------------|-------------------------------------------------| +| id | INTEGER | ID of state from "Datapoints" table | +| ts | BIGINT | Time in ms till epoch. Can be converted to time with "new Date(ts)" | +| val | TEXT | Value | +| ack | BIT/BOOLEAN | Is acknowledged: 0 - not ack, 1 - ack | +| _from | INTEGER | ID of source from "Sources" table | +| q | INTEGER | Quality as number. You can find description [here](https://git.spacen.net/yunkong2/yunkong2/blob/master/doc/SCHEMA.md#states) | + +*Note:* MS-SQL uses BIT, and others use BOOLEAN. SQLite uses for ts INTEGER and all others BIGINT. + +### Booleans +Values for states with type "boolean". + +| DB | Name in query | +|------------|-------------------------| +| MS-SQL | yunkong2.dbo.ts_bool | +| MySQL | yunkong2.ts_bool | +| PostgreSQL | ts_bool | +| SQLite | ts_bool | + +Structure: + +| Field | Type | Description | +|--------|--------------------------------------------|-------------------------------------------------| +| id | INTEGER | ID of state from "Datapoints" table | +| ts | BIGINT | Time in ms till epoch. Can be converted to time with "new Date(ts)" | +| val | BIT/BOOLEAN | Value | +| ack | BIT/BOOLEAN | Is acknowledged: 0 - not ack, 1 - ack | +| _from | INTEGER | ID of source from "Sources" table | +| q | INTEGER | Quality as number. You can find description [here](https://git.spacen.net/yunkong2/yunkong2/blob/master/doc/SCHEMA.md#states) | + +*Note:* MS-SQL uses BIT, and others use BOOLEAN. SQLite uses for ts INTEGER and all others BIGINT. + +## Custom queries +The user can execute custom queries on tables from javascript adapter: + +``` +sendTo('sql.0', 'query', 'SELECT * FROM datapoints', function (result) { + if (result.error) { + console.error(result.error); + } else { + // show result + console.log('Rows: ' + JSON.stringify(result.result)); + } +}); +``` + +Or get entries for the last hour for ID=system.adapter.admin.0.memRss +``` +sendTo('sql.0', 'query', 'SELECT id FROM datapoints WHERE name="system.adapter.admin.0.memRss"', function (result) { + if (result.error) { + console.error(result.error); + } else { + // show result + console.log('Rows: ' + JSON.stringify(result.result)); + var now = new Date(); + now.setHours(-1); + sendTo('sql.0', 'query', 'SELECT * FROM ts_number WHERE ts >= ' + now.getTime() + ' AND id=' + result.result[0].id, function (result) { + console.log('Rows: ' + JSON.stringify(result.result)); + }); + } +}); +``` + +## storeState +If you want to write other data into the InfluxDB you can use the build in system function **storeState**. +This function can also be used to convert data from other History adapters like History or SQL. + +The given ids are not checked against the yunkong2 database and do not need to be set up there, but can only be accessed directly. + +The Message can have one of the following three formats: +* one ID and one state object +* one ID and array of state objects +* array of multiple IDs with state objects + +## Get history +Additional to custom queries, you can use build in system function **getHistory**: +``` +var end = new Date().getTime(); +sendTo('sql.0', 'getHistory', { + id: 'system.adapter.admin.0.memRss', + options: { + start: end - 3600000, + end: end, + aggregate: 'minmax' // or 'none' to get raw values + } +}, function (result) { + for (var i = 0; i < result.result.length; i++) { + console.log(result.result[i].id + ' ' + new Date(result.result[i].ts).toISOString()); + } +}); +``` + +## History Logging Management via Javascript +The adapter supports enabling and disabling of history logging via JavaScript and also retrieving the list of enabled datapoints with their settings. + +### enable +The message requires to have the "id" of the datapoint.Additionally optional "options" to define the datapoint specific settings: + +``` +sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memRss', + options: { + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5, + aliasId: "" + } +}, function (result) { + if (result.error) { + console.log(result.error); + } + if (result.success) { + //successfull enabled + } +}); +``` + +### disable +The message requires to have the "id" of the datapoint. + +``` +sendTo('sql.0', 'disableHistory', { + id: 'system.adapter.sql.0.memRss', +}, function (result) { + if (result.error) { + console.log(result.error); + } + if (result.success) { + //successfull enabled + } +}); +``` + +### get List +The message has no parameters. + +``` +sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + //result is object like: + { + "system.adapter.sql.0.memRss": { + "changesOnly":true, + "debounce":0, + "retention":31536000, + "maxLength":3, + "changesMinDelta":0.5, + "enabled":true, + "changesRelogInterval":0, + "aliasId": "" + } + ... + } +}); +``` + +## Connection Settings +- **DB Type**: Type of the SQL DB: MySQL, PostgreSQL, MS-SQL or SQLite3 +- **Host**: IP address or host name with SQL Server +- **Port**: Port of SQL Server (leave blank if not sure) +- **Database name**: Database name. Default yunkong2 +- **User**: User name for SQL. Must exists in the DB. +- **Password**: Password for SQL. +- **Password confirm**: Just repeat password here. +- **Encrypt**: Some DBs support encryption. +- **Round real to**: Number of digits after comma. +- **Allow parallel requests**: Allow simultaneous SQL requests to DB. + +## Default Settings +- **De-bounce interval**: Do not store values often than this interval. +- **Log unchanged values any**: Write additionally the values every X seconds. +- **Minimum difference from last value to log**: Minimum interval between two values. +- **Storage retention**: How long the values will be stored in DB. + +## Changelog + +## 1.9.0 (2018-06-19) +* (Apollon77) Add option to log datapoints as other ID (alias) to easier migrate devices and such + +## 1.8.0 (2018-04-29) +* (Apollon77) Update sqlite3, nodejs 10 compatible +* (BuZZy1337) Admin fix + +## 1.7.4 (2018-04-15) +* (Apollon77) Fix getHistory + +## 1.7.3 (2018-03-28) +* (Apollon77) Respect 'keep forever' setting for retention from datapoint configuration + +## 1.7.2 (2018-03-24) +* (Apollon77) Disable to write NULLs for SQLite + +## 1.7.1 (2018-02-10) +* (Apollon77) Make option to write NULL values on start/stop boundaries configurable + +## 1.6.9 (2018-02-07) +* (bondrogeen) Admin3 Fixes +* (Apollon77) optimize relog feature and other things + +## 1.6.7 (2018-01-31) +* (Bluefox) Admin3 Fixes +* (Apollon77) Relog and null log fixes + +## 1.6.2 (2018-01-30) +* (Apollon77) Admin3 Fixes + +## 1.6.0 (2018-01-14) +* (bluefox) Ready for Admin3 + +## 1.5.8 (2017-10-05) +* (Apollon77) fix relog value feature + +## 1.5.7 (2017-08-10) +* (bluefox) add "save last value" option + +## 1.5.6 (2017-08-02) +* (Apollon77) fix behaviour of log interval to always log the current value + +## 1.5.4 (2017-06-12) +* (Apollon77) fix dependency to other library + +## 1.5.3 (2017-04-07) +* (Apollon77) fix in datatype conversions + +### 1.5.0 (2017-03-02) +* (Apollon77) Add option to define storage datatype per datapoint inclusing converting the value if needed + +### 1.4.6 (2017-02-25) +* (Apollon77) Fix typo with PostgrSQL + +### 1.4.5 (2017-02-18) +* (Apollon77) Small fix again for older configurations +* (Apollon77) fix for DBConverter Analyze function + +### 1.4.3 (2017-02-11) +* (Apollon77) Small fix for older configurations + +### 1.4.2 (2017-01-16) +* (bluefox) Fix handling of float values in Adapter config and Datapoint config. + +### 1.4.1 +* (Apollon77) Rollback to sql-client 0.7 to get rid of the mmagic dependecy that brings problems on older systems + +### 1.4.0 (2016-12-02) +* (Apollon77) Add messages enableHistory/disableHistory +* (Apollon77) add support to log changes only if value differs a minimum value for numbers + +### 1.3.4 (2016-11) +* (Apollon77) Allow database names with '-' for MySQL + +### 1.3.3 (2016-11) +* (Apollon77) Update dependecies + +### 1.3.2 (2016-11-21) +* (bluefox) Fix insert of string with ' + +### 1.3.0 (2016-10-29) +* (Apollon77) add option to re-log unchanged values to make it easier for visualization + +### 1.2.1 (2016-08-30) +* (bluefox) Fix selector for SQL objects + +### 1.2.0 (2016-08-30) +* (bluefox) сompatible only with new admin + +### 1.0.10 (2016-08-27) +* (bluefox) change name of object from "history" to "custom" + +### 1.0.10 (2016-07-31) +* (bluefox) fix multi requests if sqlite + +### 1.0.9 (2016-06-14) +* (bluefox) allow settings for parallel requests + +### 1.0.7 (2016-05-31) +* (bluefox) draw line to the end if ignore null + +### 1.0.6 (2016-05-30) +* (bluefox) allow setup DB name for mysql and mssql + +### 1.0.5 (2016-05-29) +* (bluefox) switch max and min with each other + +### 1.0.4 (2016-05-29) +* (bluefox) check retention of data if set "never" + +### 1.0.3 (2016-05-28) +* (bluefox) try to calculate old timestamps + +### 1.0.2 (2016-05-24) +* (bluefox) fix error with io-package + +### 1.0.1 (2016-05-24) +* (bluefox) fix error with SQLite + +### 1.0.0 (2016-05-20) +* (bluefox) change default aggregation name + +### 0.3.3 (2016-05-18) +* (bluefox) fix postgres + +### 0.3.2 (2016-05-13) +* (bluefox) queue select if IDs and FROMs queries for sqlite + +### 0.3.1 (2016-05-12) +* (bluefox) queue delete queries too for sqlite + +### 0.3.0 (2016-05-08) +* (bluefox) support of custom queries +* (bluefox) only one request simultaneously for sqlite +* (bluefox) add tests (primitive and only sql) + +### 0.2.0 (2016-04-30) +* (bluefox) support of milliseconds +* (bluefox) fix sqlite + +### 0.1.4 (2016-04-25) +* (bluefox) fix deletion of old entries + +### 0.1.3 (2016-03-08) +* (bluefox) do not print errors twice + +### 0.1.2 (2015-12-22) +* (bluefox) fix MS-SQL port settings + +### 0.1.1 (2015-12-19) +* (bluefox) fix error with double entries + +### 0.1.0 (2015-12-14) +* (bluefox) support of strings + +### 0.0.3 (2015-12-06) +* (smiling_Jack) Add demo Data ( todo: faster insert to db ) +* (smiling_Jack) change aggregation (now same as history Adapter) +* (bluefox) bug fixing + +### 0.0.2 (2015-12-06) +* (bluefox) allow only 1 client for SQLite + +### 0.0.1 (2015-11-19) +* (bluefox) initial commit + +## License + +The MIT License (MIT) + +Copyright (c) 2015-2018 bluefox , Apollon77 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/admin/.gitignore b/admin/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/admin/custom.html b/admin/custom.html new file mode 100644 index 0000000..3c541b5 --- /dev/null +++ b/admin/custom.html @@ -0,0 +1,169 @@ + + + + diff --git a/admin/custom_m.html b/admin/custom_m.html new file mode 100644 index 0000000..4ad7832 --- /dev/null +++ b/admin/custom_m.html @@ -0,0 +1,151 @@ + + + + diff --git a/admin/i18n/de/translations.json b/admin/i18n/de/translations.json new file mode 100644 index 0000000..f519465 --- /dev/null +++ b/admin/i18n/de/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "History-Adapter Einstellungen", + "note": "History Einstellungen mussen für jeden State einzeln gemacht werden auf der Lasche \"States\" und nur wenn adapter aktiv ist", + "Default settings:": "Default Einstellungen für Zuständen", + "Start or enable adapter first": "Adapter muss zuerst aktiviert werden", + "debounce": "Entprellzeit(ms)", + "log changes interval(s)": "Gleiche Werte aufzeichnen(s)", + "log changes minimal delta": "Minimale Abweichung vom letzten Wert für Aufzeichnung", + "0 = disable delta check": "0 = Abweichungsprüfung deaktivieren", + "0 = disable": "0 = Deaktivieren", + "DB settings": "DB Einstellungen", + "Default settings": "Default Einstellungen", + "retention": "Storage Vorhaltezeit", + "keep forever": "keine automatische Löschung", + "2 years": "2 Jahre", + "1 year": "1 Jahr", + "6 months": "6 Monate", + "3 months": "3 Monate", + "1 months": "1 Monat", + "2 weeks": "2 Wochen", + "1 week": "1 Woche", + "5 days": "5 Tage", + "3 days": "3 Tage", + "1 day": "1 Tag", + "SQL history adapter settings": "SQL history adapter Einstellungen", + "DB settings:": "DB Einstellungen", + "DB Type": "DB Typ", + "Host": "Host", + "DB Name": "Datenbankname", + "Port": "Port", + "User": "Login", + "Password": "Kennwort", + "Password confirm": "Kennwort-Wiederholung", + "File for sqlite:": "Datei für sqlite", + "Encrypt:": "Verschlüsseln", + "Test connection": "Verbindung testen", + "requestInterval": "Intervall zwischen DB Anfragen(ms)", + "Round real to:": "Aufrunden auf: ", + "Password confirmation is not equal with password!": "Kennwort-Bestätigung ist nicht gleich mit dem Kennwort!", + "Input path with the file name.": "Z.B. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Sind Sie sicher? ALLE Daten werden geöscht.", + "Reset DB": "Alle Daten in DB löschen", + "Ok": "Ok", + "Allow parallel requests:": "Parallelanfragen erlauben", + "Write NULL values on start/stop boundaries:": "Schreibe NULL-Werte an Start- / Stop-Grenzen:" +} diff --git a/admin/i18n/en/translations.json b/admin/i18n/en/translations.json new file mode 100644 index 0000000..0858800 --- /dev/null +++ b/admin/i18n/en/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "History adapter settings", + "note": "History settings must be done for every state individually on the tab \"States\" and only if adapter is active", + "Default settings:": "Default settings", + "Start or enable adapter first": "Start or enable adapter first", + "debounce": "De-bounce interval(ms)", + "log changes interval(s)": "Log unchanged values any(s)", + "log changes minimal delta": "Minimum difference from last value to log", + "0 = disable delta check": "0 = disable delta check", + "0 = disable": "0 = disable", + "DB settings": "DB settings", + "Default settings": "Default settings", + "retention": "Storage retention", + "keep forever": "keep forever", + "2 years": "2 years", + "1 year": "1 year", + "6 months": "6 months", + "3 months": "3 months", + "1 months": "1 months", + "2 weeks": "2 weeks", + "1 week": "1 week", + "5 days": "5 days", + "3 days": "3 days", + "1 day": "1 day", + "SQL history adapter settings": "SQL history adapter settings", + "DB settings:": "DB settings", + "DB Type": "DB Type", + "Host": "Host", + "DB Name": "Database name", + "Port": "Port", + "User": "User", + "Password": "Password", + "Password confirm": "Password confirm", + "File for sqlite:": "File for sqlite", + "Encrypt:": "Encrypt", + "Test connection": "Test connection", + "requestInterval": "Pause between requests(ms)", + "Round real to:": "Round real to", + "Password confirmation is not equal with password!": "Password confirmation is not equal with password!", + "Input path with the file name.": "E.g. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Are you sure? All data will be dropped.", + "Reset DB": "Reset DB", + "Ok": "Ok", + "Allow parallel requests:": "Allow parallel requests", + "Write NULL values on start/stop boundaries:": "Write NULL values on start/stop boundaries:" +} diff --git a/admin/i18n/es/translations.json b/admin/i18n/es/translations.json new file mode 100644 index 0000000..c91812b --- /dev/null +++ b/admin/i18n/es/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Configuración del adaptador de historial", + "note": "La configuración del historial se debe realizar para cada estado individualmente en la pestaña \"Estados\" y solo si el adaptador está activo", + "Default settings:": "Configuración por defecto", + "Start or enable adapter first": "Comience o habilite el adaptador primero", + "debounce": "Intervalo de rebote (ms)", + "log changes interval(s)": "Registre los valores sin modificar cualquier (s)", + "log changes minimal delta": "Diferencia mínima desde el último valor para registrar", + "0 = disable delta check": "0 = deshabilitar la verificación delta", + "0 = disable": "0 = desactivar", + "DB settings": "Configuración de DB", + "Default settings": "Configuración por defecto", + "retention": "Retención de almacenamiento", + "keep forever": "mantener para siempre", + "2 years": "2 años", + "1 year": "1 año", + "6 months": "6 meses", + "3 months": "3 meses", + "1 months": "1 mes", + "2 weeks": "2 semanas", + "1 week": "1 semana", + "5 days": "5 dias", + "3 days": "3 días", + "1 day": "1 día", + "SQL history adapter settings": "Configuración del adaptador de historial SQL", + "DB settings:": "Configuración de DB", + "DB Type": "Tipo de DB", + "Host": "Anfitrión", + "DB Name": "Nombre de la base de datos", + "Port": "Puerto", + "User": "Usuario", + "Password": "Contraseña", + "Password confirm": "Contraseña confirmada", + "File for sqlite:": "Archivo para sqlite", + "Encrypt:": "Encriptar", + "Test connection": "Conexión de prueba", + "requestInterval": "Pausa entre solicitudes (ms)", + "Round real to:": "Redondo real para", + "Password confirmation is not equal with password!": "La confirmación de la contraseña no es igual a la contraseña!", + "Input path with the file name.": "P.ej. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "¿Estás seguro? Todos los datos serán eliminados.", + "Reset DB": "Restablecer DB", + "Ok": "De acuerdo", + "Allow parallel requests:": "Permitir solicitudes paralelas", + "Write NULL values on start/stop boundaries:": "Escribe valores NULL en los límites de inicio / finalización:" +} diff --git a/admin/i18n/fr/translations.json b/admin/i18n/fr/translations.json new file mode 100644 index 0000000..daefea9 --- /dev/null +++ b/admin/i18n/fr/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Paramètres de l'adaptateur d'historique", + "note": "Les paramètres d'historique doivent être définis pour chaque état individuellement dans l'onglet \"Etats\" et uniquement si l'adaptateur est actif", + "Default settings:": "Paramètres par défaut", + "Start or enable adapter first": "Démarrer ou activer l'adaptateur en premier", + "debounce": "Intervalle de rebond (ms)", + "log changes interval(s)": "Consigner les valeurs inchangées any (s)", + "log changes minimal delta": "Différence minimale de la dernière valeur à la notation", + "0 = disable delta check": "0 = désactiver le contrôle delta", + "0 = disable": "0 = désactiver", + "DB settings": "Paramètres DB", + "Default settings": "Paramètres par défaut", + "retention": "Rétention de stockage", + "keep forever": "garde pour toujours", + "2 years": "2 ans", + "1 year": "1 an", + "6 months": "6 mois", + "3 months": "3 mois", + "1 months": "1 mois", + "2 weeks": "2 semaines", + "1 week": "1 semaine", + "5 days": "5 jours", + "3 days": "3 jours", + "1 day": "Un jour", + "SQL history adapter settings": "Paramètres de l'adaptateur d'historique SQL", + "DB settings:": "Paramètres de base de données", + "DB Type": "Type de DB", + "Host": "Hôte", + "DB Name": "Nom de la base de données", + "Port": "Port", + "User": "Utilisateur", + "Password": "Mot de passe", + "Password confirm": "Confirmer le mot de passe", + "File for sqlite:": "Fichier pour sqlite", + "Encrypt:": "Crypter", + "Test connection": "Test de connexion", + "requestInterval": "Pause entre les demandes (ms)", + "Round real to:": "Rond réel à", + "Password confirmation is not equal with password!": "La confirmation du mot de passe n'est pas égale au mot de passe!", + "Input path with the file name.": "Par exemple. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Êtes-vous sûr? Toutes les données seront supprimées.", + "Reset DB": "Reset DB", + "Ok": "D'accord", + "Allow parallel requests:": "Autoriser les demandes parallèles", + "Write NULL values on start/stop boundaries:": "Écrire des valeurs NULL sur les limites de démarrage / arrêt:" +} diff --git a/admin/i18n/it/translations.json b/admin/i18n/it/translations.json new file mode 100644 index 0000000..ba4de41 --- /dev/null +++ b/admin/i18n/it/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Impostazioni della scheda Cronologia", + "note": "Le impostazioni della cronologia devono essere eseguite singolarmente per ogni stato nella scheda \"Stati\" e solo se l'adattatore è attivo", + "Default settings:": "Impostazioni predefinite", + "Start or enable adapter first": "Inizia o abilita prima l'adattatore", + "debounce": "Intervallo di rimbalzo (ms)", + "log changes interval(s)": "Registra valori invariati nessuno (i)", + "log changes minimal delta": "Differenza minima dall'ultimo valore da registrare", + "0 = disable delta check": "0 = disabilita il controllo delta", + "0 = disable": "0 = disabilita", + "DB settings": "Impostazioni DB", + "Default settings": "Impostazioni predefinite", + "retention": "Conservazione dello spazio", + "keep forever": "tienilo per sempre", + "2 years": "2 anni", + "1 year": "1 anno", + "6 months": "6 mesi", + "3 months": "3 mesi", + "1 months": "1 mese", + "2 weeks": "2 settimane", + "1 week": "1 settimana", + "5 days": "5 giorni", + "3 days": "3 giorni", + "1 day": "1 giorno", + "SQL history adapter settings": "Impostazioni della scheda cronologia SQL", + "DB settings:": "Impostazioni DB", + "DB Type": "Tipo di DB", + "Host": "Ospite", + "DB Name": "Nome del database", + "Port": "Porta", + "User": "Utente", + "Password": "Parola d'ordine", + "Password confirm": "Conferma la password", + "File for sqlite:": "File per sqlite", + "Encrypt:": "Encrypt", + "Test connection": "Connessione di prova", + "requestInterval": "Pausa tra le richieste (ms)", + "Round real to:": "Round reale a", + "Password confirmation is not equal with password!": "La conferma della password non è uguale alla password!", + "Input path with the file name.": "Per esempio. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Sei sicuro? Tutti i dati verranno eliminati.", + "Reset DB": "Ripristina DB", + "Ok": "Ok", + "Allow parallel requests:": "Consenti richieste parallele", + "Write NULL values on start/stop boundaries:": "Scrivi valori NULL sui limiti di avvio / arresto:" +} diff --git a/admin/i18n/nl/translations.json b/admin/i18n/nl/translations.json new file mode 100644 index 0000000..91d3083 --- /dev/null +++ b/admin/i18n/nl/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Geschiedenis adapter instellingen", + "note": "Geschiedenisinstellingen moeten voor elke afzonderlijke status worden uitgevoerd op het tabblad \"Staten\" en alleen als de adapter actief is", + "Default settings:": "Standaard instellingen", + "Start or enable adapter first": "Start of schakel eerst de adapter in", + "debounce": "De-bounce interval (ms)", + "log changes interval(s)": "Log onveranderde waarden voor elke", + "log changes minimal delta": "Minimaal verschil van laatste waarde tot log", + "0 = disable delta check": "0 = delta-controle uitschakelen", + "0 = disable": "0 = uitschakelen", + "DB settings": "DB-instellingen", + "Default settings": "Standaard instellingen", + "retention": "Bewaring van opslag", + "keep forever": "blijf voor altijd", + "2 years": "2 jaar", + "1 year": "1 jaar", + "6 months": "6 maanden", + "3 months": "3 maanden", + "1 months": "1 maanden", + "2 weeks": "2 weken", + "1 week": "1 week", + "5 days": "5 dagen", + "3 days": "3 dagen", + "1 day": "1 dag", + "SQL history adapter settings": "Instellingen voor SQL-geschiedenisadapter", + "DB settings:": "DB-instellingen", + "DB Type": "DB Type", + "Host": "Host", + "DB Name": "Database naam", + "Port": "Haven", + "User": "Gebruiker", + "Password": "Wachtwoord", + "Password confirm": "Wachtwoord bevestigen", + "File for sqlite:": "Bestand voor sqlite", + "Encrypt:": "versleutelen", + "Test connection": "Test verbinding", + "requestInterval": "Pauzeren tussen verzoeken (ms)", + "Round real to:": "Rond echt naar", + "Password confirmation is not equal with password!": "Wachtwoordbevestiging is niet gelijk aan wachtwoord!", + "Input path with the file name.": "Bijv. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Weet je het zeker? Alle gegevens worden verwijderd.", + "Reset DB": "Stel DB opnieuw in", + "Ok": "OK", + "Allow parallel requests:": "Sta parallelle verzoeken toe", + "Write NULL values on start/stop boundaries:": "Schrijf NULL-waarden op start / stop-grenzen:" +} diff --git a/admin/i18n/pl/translations.json b/admin/i18n/pl/translations.json new file mode 100644 index 0000000..ae2cc75 --- /dev/null +++ b/admin/i18n/pl/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Ustawienia adaptera historii", + "note": "Ustawienia historii muszą być wykonane dla każdego stanu indywidualnie na karcie \"Stany\" i tylko wtedy, gdy adapter jest aktywny", + "Default settings:": "Ustawienia domyślne", + "Start or enable adapter first": "Najpierw włącz lub włącz adapter", + "debounce": "Interwał odbicia (ms)", + "log changes interval(s)": "Zaloguj niezmienione wartości dowolne (s)", + "log changes minimal delta": "Minimalna różnica od ostatniej wartości do dziennika", + "0 = disable delta check": "0 = wyłącz kontrolę delta", + "0 = disable": "0 = wyłącz", + "DB settings": "Ustawienia DB", + "Default settings": "Ustawienia domyślne", + "retention": "Zatrzymanie przechowywania", + "keep forever": "zachowaj na zawsze", + "2 years": "2 lata", + "1 year": "1 rok", + "6 months": "6 miesięcy", + "3 months": "3 miesiące", + "1 months": "1 miesiąc", + "2 weeks": "2 tygodnie", + "1 week": "1 tydzień", + "5 days": "5 dni", + "3 days": "3 dni", + "1 day": "1 dzień", + "SQL history adapter settings": "Ustawienia adaptera historii SQL", + "DB settings:": "Ustawienia DB", + "DB Type": "Typ DB", + "Host": "Gospodarz", + "DB Name": "Nazwa bazy danych", + "Port": "Port", + "User": "Użytkownik", + "Password": "Hasło", + "Password confirm": "Potwierdź hasło", + "File for sqlite:": "Plik dla sqlite", + "Encrypt:": "Szyfruj", + "Test connection": "Testuj połączenie", + "requestInterval": "Wstrzymaj między żądaniami (ms)", + "Round real to:": "Runda prawdziwa do", + "Password confirmation is not equal with password!": "Potwierdzenie hasła nie jest równe hasłu!", + "Input path with the file name.": "Na przykład. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Jesteś pewny? Wszystkie dane zostaną usunięte.", + "Reset DB": "Zresetuj DB", + "Ok": "Ok", + "Allow parallel requests:": "Pozwól na równoległe żądania", + "Write NULL values on start/stop boundaries:": "Napisz wartości NULL na granicach start / stop:" +} diff --git a/admin/i18n/pt/translations.json b/admin/i18n/pt/translations.json new file mode 100644 index 0000000..7dd487c --- /dev/null +++ b/admin/i18n/pt/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Configurações do adaptador de histórico", + "note": "As configurações de histórico devem ser feitas para cada estado individual na guia \"Estados\" e somente se o adaptador estiver ativo", + "Default settings:": "Configurações padrão", + "Start or enable adapter first": "Inicie ou ative o adaptador primeiro", + "debounce": "Intervalo de desvio (ms)", + "log changes interval(s)": "Registre valores inalterados qualquer (s)", + "log changes minimal delta": "Diferença mínima do último valor ao log", + "0 = disable delta check": "0 = desativar o cheque delta", + "0 = disable": "0 = desativar", + "DB settings": "Configurações de banco de dados", + "Default settings": "Configurações padrão", + "retention": "Retenção de armazenamento", + "keep forever": "mantenha para sempre", + "2 years": "2 anos", + "1 year": "1 ano", + "6 months": "6 meses", + "3 months": "3 meses", + "1 months": "1 mês", + "2 weeks": "2 semanas", + "1 week": "1 semana", + "5 days": "5 dias", + "3 days": "3 dias", + "1 day": "1 dia", + "SQL history adapter settings": "Configurações do adaptador de histórico SQL", + "DB settings:": "Configurações de banco de dados", + "DB Type": "DB Type", + "Host": "Hospedeiro", + "DB Name": "Nome do banco de dados", + "Port": "Porta", + "User": "Do utilizador", + "Password": "Senha", + "Password confirm": "Confirmação de senha", + "File for sqlite:": "Arquivo para sqlite", + "Encrypt:": "Criptografar", + "Test connection": "Conexão de teste", + "requestInterval": "Pausa entre solicitações (ms)", + "Round real to:": "Round real to", + "Password confirmation is not equal with password!": "A confirmação da senha não é igual com a senha!", + "Input path with the file name.": "Por exemplo. /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Você tem certeza? Todos os dados serão descartados.", + "Reset DB": "Redefinir DB", + "Ok": "Está bem", + "Allow parallel requests:": "Permitir pedidos paralelos", + "Write NULL values on start/stop boundaries:": "Escreva valores NULL nos limites de início / parada:" +} diff --git a/admin/i18n/ru/translations.json b/admin/i18n/ru/translations.json new file mode 100644 index 0000000..c5899f6 --- /dev/null +++ b/admin/i18n/ru/translations.json @@ -0,0 +1,46 @@ +{ + "History adapter settings": "Настройки профайлера", + "note": "Настройки профайлера делаются для каждого состояния отдельно в закладке \"Состояния\" и только, когда драйвер активирован", + "Default settings:": "Настройки по умолчанию для состояний", + "Start or enable adapter first": "Сначала надо активировать драйвер SQL", + "debounce": "Минимальный интервал(ms)", + "log changes interval(s)": "Запись неизменённых значений каждые (сек)", + "log changes minimal delta": "Минимальная разница с последним записанным значением", + "0 = disable delta check": "0 = не проверять минимальное изменение", + "0 = disable": "0 = не активно", + "DB settings": "Настройки соединения с DB", + "Default settings": "Настройки по умолчанию", + "retention": "Время хранения в базе", + "keep forever": "хранить вечно", + "2 years": "2 года", + "1 year": "1 год", + "6 months": "6 месяцев", + "3 months": "3 месяца", + "1 months": "1 месяц", + "2 weeks": "2 недели", + "1 week": "1 неделя", + "5 days": "5 дней", + "3 days": "3 дня", + "1 day": "1 день", + "SQL history adapter settings": "Настройки профайлера в SQL базу данных", + "DB settings:": "DB settings", + "DB Type": "DB Type", + "Host": "Хост", + "DB Name": "Имя базы данных", + "Port": "Порт", + "User": "Пользователь", + "Password": "Пароль", + "Password confirm": "Подтверждение пароля", + "File for sqlite:": "Файл для sqlite", + "Encrypt:": "Шифрование", + "Test connection": "Проверить соединение", + "requestInterval": "Пауза между запросами к базе", + "Round real to:": "Округлять до", + "Password confirmation is not equal with password!": "Подтверждение пароля не совпадает с паролем!", + "Input path with the file name.": "Например /opt/sqlite/data.db", + "Are you sure? All data will be dropped.": "Вы уверенны? Все данные в базе будут удалены.", + "Reset DB": "Сбросить DB", + "Ok": "Ok", + "Allow parallel requests:": "Разрешить параллельные запросы к базе", + "Write NULL values on start/stop boundaries:": "Запись значений NULL на границах начала / остановки:" +} diff --git a/admin/index.html b/admin/index.html new file mode 100644 index 0000000..859e622 --- /dev/null +++ b/admin/index.html @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + +
+ + + +

SQL history adapter settings

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Input path with the file name.
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + +

Default settings:

0 = disable
0 = disable delta check
+
+
+
+ + diff --git a/admin/index_m.html b/admin/index_m.html new file mode 100644 index 0000000..478360f --- /dev/null +++ b/admin/index_m.html @@ -0,0 +1,296 @@ + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+
+ +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + Input path with the file name. +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+
+
+
+ + +
+
+ + + (0 = disable) +
+
+
+
+ + + (0 = disable delta check) +
+
+ + +
+
+
+
+
+ + + diff --git a/admin/sql.png b/admin/sql.png new file mode 100644 index 0000000..fe73150 Binary files /dev/null and b/admin/sql.png differ diff --git a/admin/words.js b/admin/words.js new file mode 100644 index 0000000..975f46b --- /dev/null +++ b/admin/words.js @@ -0,0 +1,60 @@ +// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n +/*global systemDictionary:true */ +'use strict'; + +systemDictionary = { + "History adapter settings": { "en": "History adapter settings", "de": "History-Adapter Einstellungen", "ru": "Настройки профайлера", "pt": "Configurações do adaptador de histórico", "nl": "Geschiedenis adapter instellingen", "fr": "Paramètres de l'adaptateur d'historique", "it": "Impostazioni della scheda Cronologia", "es": "Configuración del adaptador de historial", "pl": "Ustawienia adaptera historii"}, + "note": { "en": "History settings must be done for every state individually on the tab \"States\" and only if adapter is active", "de": "History Einstellungen mussen für jeden State einzeln gemacht werden auf der Lasche \"States\" und nur wenn adapter aktiv ist", "ru": "Настройки профайлера делаются для каждого состояния отдельно в закладке \"Состояния\" и только, когда драйвер активирован", "pt": "As configurações de histórico devem ser feitas para cada estado individual na guia \"Estados\" e somente se o adaptador estiver ativo", "nl": "Geschiedenisinstellingen moeten voor elke afzonderlijke status worden uitgevoerd op het tabblad \"Staten\" en alleen als de adapter actief is", "fr": "Les paramètres d'historique doivent être définis pour chaque état individuellement dans l'onglet \"Etats\" et uniquement si l'adaptateur est actif", "it": "Le impostazioni della cronologia devono essere eseguite singolarmente per ogni stato nella scheda \"Stati\" e solo se l'adattatore è attivo", "es": "La configuración del historial se debe realizar para cada estado individualmente en la pestaña \"Estados\" y solo si el adaptador está activo", "pl": "Ustawienia historii muszą być wykonane dla każdego stanu indywidualnie na karcie \"Stany\" i tylko wtedy, gdy adapter jest aktywny"}, + "Default settings:": { "en": "Default settings", "de": "Default Einstellungen für Zuständen", "ru": "Настройки по умолчанию для состояний", "pt": "Configurações padrão", "nl": "Standaard instellingen", "fr": "Paramètres par défaut", "it": "Impostazioni predefinite", "es": "Configuración por defecto", "pl": "Ustawienia domyślne"}, + "Start or enable adapter first": { "en": "Start or enable adapter first", "de": "Adapter muss zuerst aktiviert werden", "ru": "Сначала надо активировать драйвер SQL", "pt": "Inicie ou ative o adaptador primeiro", "nl": "Start of schakel eerst de adapter in", "fr": "Démarrer ou activer l'adaptateur en premier", "it": "Inizia o abilita prima l'adattatore", "es": "Comience o habilite el adaptador primero", "pl": "Najpierw włącz lub włącz adapter"}, + "debounce": { "en": "De-bounce interval(ms)", "de": "Entprellzeit(ms)", "ru": "Минимальный интервал(ms)", "pt": "Intervalo de desvio (ms)", "nl": "De-bounce interval (ms)", "fr": "Intervalle de rebond (ms)", "it": "Intervallo di rimbalzo (ms)", "es": "Intervalo de rebote (ms)", "pl": "Interwał odbicia (ms)"}, + "log changes interval(s)": { "en": "Log unchanged values any(s)", "de": "Gleiche Werte aufzeichnen(s)", "ru": "Запись неизменённых значений каждые (сек)", "pt": "Registre valores inalterados qualquer (s)", "nl": "Log onveranderde waarden voor elke", "fr": "Consigner les valeurs inchangées any (s)", "it": "Registra valori invariati nessuno (i)", "es": "Registre los valores sin modificar cualquier (s)", "pl": "Zaloguj niezmienione wartości dowolne (s)"}, + "log changes minimal delta": { "en": "Minimum difference from last value to log", "de": "Minimale Abweichung vom letzten Wert für Aufzeichnung", "ru": "Минимальная разница с последним записанным значением", "pt": "Diferença mínima do último valor ao log", "nl": "Minimaal verschil van laatste waarde tot log", "fr": "Différence minimale de la dernière valeur à la notation", "it": "Differenza minima dall'ultimo valore da registrare", "es": "Diferencia mínima desde el último valor para registrar", "pl": "Minimalna różnica od ostatniej wartości do dziennika"}, + "0 = disable delta check": { "en": "0 = disable delta check", "de": "0 = Abweichungsprüfung deaktivieren", "ru": "0 = не проверять минимальное изменение", "pt": "0 = desativar o cheque delta", "nl": "0 = delta-controle uitschakelen", "fr": "0 = désactiver le contrôle delta", "it": "0 = disabilita il controllo delta", "es": "0 = deshabilitar la verificación delta", "pl": "0 = wyłącz kontrolę delta"}, + "0 = disable": { "en": "0 = disable", "de": "0 = Deaktivieren", "ru": "0 = не активно", "pt": "0 = desativar", "nl": "0 = uitschakelen", "fr": "0 = désactiver", "it": "0 = disabilita", "es": "0 = desactivar", "pl": "0 = wyłącz"}, + "DB settings": { "en": "DB settings", "de": "DB Einstellungen", "ru": "Настройки соединения с DB", "pt": "Configurações de banco de dados", "nl": "DB-instellingen", "fr": "Paramètres DB", "it": "Impostazioni DB", "es": "Configuración de DB", "pl": "Ustawienia DB"}, + "Default settings": { "en": "Default settings", "de": "Default Einstellungen", "ru": "Настройки по умолчанию", "pt": "Configurações padrão", "nl": "Standaard instellingen", "fr": "Paramètres par défaut", "it": "Impostazioni predefinite", "es": "Configuración por defecto", "pl": "Ustawienia domyślne"}, + "retention": { "en": "Storage retention", "de": "Storage Vorhaltezeit", "ru": "Время хранения в базе", "pt": "Retenção de armazenamento", "nl": "Bewaring van opslag", "fr": "Rétention de stockage", "it": "Conservazione dello spazio", "es": "Retención de almacenamiento", "pl": "Zatrzymanie przechowywania"}, + "keep forever": { "en": "keep forever", "de": "keine automatische Löschung", "ru": "хранить вечно", "pt": "mantenha para sempre", "nl": "blijf voor altijd", "fr": "garde pour toujours", "it": "tienilo per sempre", "es": "mantener para siempre", "pl": "zachowaj na zawsze"}, + "2 years": { "en": "2 years", "de": "2 Jahre", "ru": "2 года", "pt": "2 anos", "nl": "2 jaar", "fr": "2 ans", "it": "2 anni", "es": "2 años", "pl": "2 lata"}, + "1 year": { "en": "1 year", "de": "1 Jahr", "ru": "1 год", "pt": "1 ano", "nl": "1 jaar", "fr": "1 an", "it": "1 anno", "es": "1 año", "pl": "1 rok"}, + "6 months": { "en": "6 months", "de": "6 Monate", "ru": "6 месяцев", "pt": "6 meses", "nl": "6 maanden", "fr": "6 mois", "it": "6 mesi", "es": "6 meses", "pl": "6 miesięcy"}, + "3 months": { "en": "3 months", "de": "3 Monate", "ru": "3 месяца", "pt": "3 meses", "nl": "3 maanden", "fr": "3 mois", "it": "3 mesi", "es": "3 meses", "pl": "3 miesiące"}, + "1 months": { "en": "1 months", "de": "1 Monat", "ru": "1 месяц", "pt": "1 mês", "nl": "1 maanden", "fr": "1 mois", "it": "1 mese", "es": "1 mes", "pl": "1 miesiąc"}, + "2 weeks": { "en": "2 weeks", "de": "2 Wochen", "ru": "2 недели", "pt": "2 semanas", "nl": "2 weken", "fr": "2 semaines", "it": "2 settimane", "es": "2 semanas", "pl": "2 tygodnie"}, + "1 week": { "en": "1 week", "de": "1 Woche", "ru": "1 неделя", "pt": "1 semana", "nl": "1 week", "fr": "1 semaine", "it": "1 settimana", "es": "1 semana", "pl": "1 tydzień"}, + "5 days": { "en": "5 days", "de": "5 Tage", "ru": "5 дней", "pt": "5 dias", "nl": "5 dagen", "fr": "5 jours", "it": "5 giorni", "es": "5 dias", "pl": "5 dni"}, + "3 days": { "en": "3 days", "de": "3 Tage", "ru": "3 дня", "pt": "3 dias", "nl": "3 dagen", "fr": "3 jours", "it": "3 giorni", "es": "3 días", "pl": "3 dni"}, + "1 day": { "en": "1 day", "de": "1 Tag", "ru": "1 день", "pt": "1 dia", "nl": "1 dag", "fr": "Un jour", "it": "1 giorno", "es": "1 día", "pl": "1 dzień"}, + "SQL history adapter settings": { "en": "SQL history adapter settings", "de": "SQL history adapter Einstellungen", "ru": "Настройки профайлера в SQL базу данных", "pt": "Configurações do adaptador de histórico SQL", "nl": "Instellingen voor SQL-geschiedenisadapter", "fr": "Paramètres de l'adaptateur d'historique SQL", "it": "Impostazioni della scheda cronologia SQL", "es": "Configuración del adaptador de historial SQL", "pl": "Ustawienia adaptera historii SQL"}, + "DB settings:": { "en": "DB settings", "de": "DB Einstellungen", "ru": "DB settings", "pt": "Configurações de banco de dados", "nl": "DB-instellingen", "fr": "Paramètres de base de données", "it": "Impostazioni DB", "es": "Configuración de DB", "pl": "Ustawienia DB"}, + "DB Type": { "en": "DB Type", "de": "DB Typ", "ru": "DB Type", "pt": "DB Type", "nl": "DB Type", "fr": "Type de DB", "it": "Tipo di DB", "es": "Tipo de DB", "pl": "Typ DB"}, + "Host": { "en": "Host", "de": "Host", "ru": "Хост", "pt": "Hospedeiro", "nl": "Host", "fr": "Hôte", "it": "Ospite", "es": "Anfitrión", "pl": "Gospodarz"}, + "DB Name": { "en": "Database name", "de": "Datenbankname", "ru": "Имя базы данных", "pt": "Nome do banco de dados", "nl": "Database naam", "fr": "Nom de la base de données", "it": "Nome del database", "es": "Nombre de la base de datos", "pl": "Nazwa bazy danych"}, + "Port": { "en": "Port", "de": "Port", "ru": "Порт", "pt": "Porta", "nl": "Haven", "fr": "Port", "it": "Porta", "es": "Puerto", "pl": "Port"}, + "User": { "en": "User", "de": "Login", "ru": "Пользователь", "pt": "Do utilizador", "nl": "Gebruiker", "fr": "Utilisateur", "it": "Utente", "es": "Usuario", "pl": "Użytkownik"}, + "Password": { "en": "Password", "de": "Kennwort", "ru": "Пароль", "pt": "Senha", "nl": "Wachtwoord", "fr": "Mot de passe", "it": "Parola d'ordine", "es": "Contraseña", "pl": "Hasło"}, + "Password confirm": { "en": "Password confirm", "de": "Kennwort-Wiederholung", "ru": "Подтверждение пароля", "pt": "Confirmação de senha", "nl": "Wachtwoord bevestigen", "fr": "Confirmer le mot de passe", "it": "Conferma la password", "es": "Contraseña confirmada", "pl": "Potwierdź hasło"}, + "File for sqlite:": { "en": "File for sqlite", "de": "Datei für sqlite", "ru": "Файл для sqlite", "pt": "Arquivo para sqlite", "nl": "Bestand voor sqlite", "fr": "Fichier pour sqlite", "it": "File per sqlite", "es": "Archivo para sqlite", "pl": "Plik dla sqlite"}, + "Encrypt:": { "en": "Encrypt", "de": "Verschlüsseln", "ru": "Шифрование", "pt": "Criptografar", "nl": "versleutelen", "fr": "Crypter", "it": "Encrypt", "es": "Encriptar", "pl": "Szyfruj"}, + "Test connection": { "en": "Test connection", "de": "Verbindung testen", "ru": "Проверить соединение", "pt": "Conexão de teste", "nl": "Test verbinding", "fr": "Test de connexion", "it": "Connessione di prova", "es": "Conexión de prueba", "pl": "Testuj połączenie"}, + "requestInterval": { "en": "Pause between requests(ms)", "de": "Intervall zwischen DB Anfragen(ms)", "ru": "Пауза между запросами к базе", "pt": "Pausa entre solicitações (ms)", "nl": "Pauzeren tussen verzoeken (ms)", "fr": "Pause entre les demandes (ms)", "it": "Pausa tra le richieste (ms)", "es": "Pausa entre solicitudes (ms)", "pl": "Wstrzymaj między żądaniami (ms)"}, + "Round real to:": { "en": "Round real to", "de": "Aufrunden auf: ", "ru": "Округлять до", "pt": "Round real to", "nl": "Rond echt naar", "fr": "Rond réel à", "it": "Round reale a", "es": "Redondo real para", "pl": "Runda prawdziwa do"}, + "Password confirmation is not equal with password!": {"en": "Password confirmation is not equal with password!", "de": "Kennwort-Bestätigung ist nicht gleich mit dem Kennwort!", "ru": "Подтверждение пароля не совпадает с паролем!", "pt": "A confirmação da senha não é igual com a senha!", "nl": "Wachtwoordbevestiging is niet gelijk aan wachtwoord!", "fr": "La confirmation du mot de passe n'est pas égale au mot de passe!", "it": "La conferma della password non è uguale alla password!", "es": "La confirmación de la contraseña no es igual a la contraseña!", "pl": "Potwierdzenie hasła nie jest równe hasłu!"}, + "Input path with the file name.": { "en": "E.g. /opt/sqlite/data.db", "de": "Z.B. /opt/sqlite/data.db", "ru": "Например /opt/sqlite/data.db", "pt": "Por exemplo. /opt/sqlite/data.db", "nl": "Bijv. /opt/sqlite/data.db", "fr": "Par exemple. /opt/sqlite/data.db", "it": "Per esempio. /opt/sqlite/data.db", "es": "P.ej. /opt/sqlite/data.db", "pl": "Na przykład. /opt/sqlite/data.db"}, + "Are you sure? All data will be dropped.": { "en": "Are you sure? All data will be dropped.", "de": "Sind Sie sicher? ALLE Daten werden geöscht.", "ru": "Вы уверенны? Все данные в базе будут удалены.", "pt": "Você tem certeza? Todos os dados serão descartados.", "nl": "Weet je het zeker? Alle gegevens worden verwijderd.", "fr": "Êtes-vous sûr? Toutes les données seront supprimées.", "it": "Sei sicuro? Tutti i dati verranno eliminati.", "es": "¿Estás seguro? Todos los datos serán eliminados.", "pl": "Jesteś pewny? Wszystkie dane zostaną usunięte."}, + "Reset DB": { "en": "Reset DB", "de": "Alle Daten in DB löschen", "ru": "Сбросить DB", "pt": "Redefinir DB", "nl": "Stel DB opnieuw in", "fr": "Reset DB", "it": "Ripristina DB", "es": "Restablecer DB", "pl": "Zresetuj DB"}, + "Ok": { "en": "Ok", "de": "Ok", "ru": "Ok", "pt": "Está bem", "nl": "OK", "fr": "D'accord", "it": "Ok", "es": "De acuerdo", "pl": "Ok"}, + "Allow parallel requests:": { "en": "Allow parallel requests", "de": "Parallelanfragen erlauben", "ru": "Разрешить параллельные запросы к базе", "pt": "Permitir pedidos paralelos", "nl": "Sta parallelle verzoeken toe", "fr": "Autoriser les demandes parallèles", "it": "Consenti richieste parallele", "es": "Permitir solicitudes paralelas", "pl": "Pozwól na równoległe żądania"}, + "Write NULL values on start/stop boundaries:": { + "en": "Write NULL values on start/stop boundaries:", + "de": "Schreibe NULL-Werte an Start- / Stop-Grenzen:", + "ru": "Запись значений NULL на границах начала / остановки:", + "pt": "Escreva valores NULL nos limites de início / parada:", + "nl": "Schrijf NULL-waarden op start / stop-grenzen:", + "fr": "Écrire des valeurs NULL sur les limites de démarrage / arrêt:", + "it": "Scrivi valori NULL sui limiti di avvio / arresto:", + "es": "Escribe valores NULL en los límites de inicio / finalización:", + "pl": "Napisz wartości NULL na granicach start / stop:" + } +}; diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..59d512e --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,31 @@ +version: 'test-{build}' +os: Visual Studio 2013 +environment: + matrix: + - nodejs_version: '4' + - nodejs_version: '6' + - nodejs_version: '8' + - nodejs_version: '10' +platform: + - x86 + - x64 +clone_folder: 'c:\projects\%APPVEYOR_PROJECT_NAME%' +services: + - mssql2014 + - mysql + - postgresql +install: + - ps: 'Install-Product node $env:nodejs_version $env:platform' + - ps: '$NpmVersion = (npm -v).Substring(0,1)' + - ps: 'if($NpmVersion -eq 5) { npm install -g npm@5 }' + - ps: npm --version + - npm install + - npm install winston@2.3.1 + - 'npm install https://git.spacen.net/yunkong2/yunkong2.js-controller/tarball/master --production' +test_script: + - echo %cd% + - node --version + - npm --version + - ps: Start-Sleep -s 15 + - npm test +build: 'off' diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..fd48c48 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,401 @@ +'use strict'; + +var gulp = require('gulp'); +var fs = require('fs'); +var pkg = require('./package.json'); +var iopackage = require('./io-package.json'); +var version = (pkg && pkg.version) ? pkg.version : iopackage.common.version; +/*var appName = getAppName(); + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 1].split('.')[0].toLowerCase(); +} +*/ +const fileName = 'words.js'; +var languages = { + en: {}, + de: {}, + ru: {}, + pt: {}, + nl: {}, + fr: {}, + it: {}, + es: {}, + pl: {} +}; + +function lang2data(lang, isFlat) { + var str = isFlat ? '' : '{\n'; + var count = 0; + for (var w in lang) { + if (lang.hasOwnProperty(w)) { + count++; + if (isFlat) { + str += (lang[w] === '' ? (isFlat[w] || w) : lang[w]) + '\n'; + } else { + var key = ' "' + w.replace(/"/g, '\\"') + '": '; + str += key + '"' + lang[w].replace(/"/g, '\\"') + '",\n'; + } + } + } + if (!count) return isFlat ? '' : '{\n}'; + if (isFlat) { + return str; + } else { + return str.substring(0, str.length - 2) + '\n}'; + } +} + +function readWordJs(src) { + try { + var words; + if (fs.existsSync(src + 'js/' + fileName)) { + words = fs.readFileSync(src + 'js/' + fileName).toString(); + } else { + words = fs.readFileSync(src + fileName).toString(); + } + + var lines = words.split(/\r\n|\r|\n/g); + var i = 0; + while (!lines[i].match(/^systemDictionary = {/)) { + i++; + } + lines.splice(0, i); + + // remove last empty lines + i = lines.length - 1; + while (!lines[i]) { + i--; + } + if (i < lines.length - 1) { + lines.splice(i + 1); + } + + lines[0] = lines[0].replace('systemDictionary = ', ''); + lines[lines.length - 1] = lines[lines.length - 1].trim().replace(/};$/, '}'); + words = lines.join('\n'); + var resultFunc = new Function('return ' + words + ';'); + + return resultFunc(); + } catch (e) { + return null; + } +} +function padRight(text, totalLength) { + return text + (text.length < totalLength ? new Array(totalLength - text.length).join(' ') : ''); +} +function writeWordJs(data, src) { + var text = '// DO NOT EDIT THIS FILE!!! IT WILL BE AUTOMATICALLY GENERATED FROM src/i18n\n'; + text += '/*global systemDictionary:true */\n'; + text += '\'use strict\';\n\n'; + + text += 'systemDictionary = {\n'; + for (var word in data) { + if (data.hasOwnProperty(word)) { + text += ' ' + padRight('"' + word.replace(/"/g, '\\"') + '": {', 50); + var line = ''; + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + line += '"' + lang + '": "' + padRight(data[word][lang].replace(/"/g, '\\"') + '",', 50) + ' '; + } + } + if (line) { + line = line.trim(); + line = line.substring(0, line.length - 1); + } + text += line + '},\n'; + } + } + text = text.replace(/},\n$/, '}\n'); + text += '};'; + + if (fs.existsSync(src + 'js/' + fileName)) { + fs.writeFileSync(src + 'js/' + fileName, text); + } else { + fs.writeFileSync(src + '' + fileName, text); + } +} + +const EMPTY = ''; + +function words2languages(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var keys = Object.keys(langs[l]); + //keys.sort(); + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + if (!fs.existsSync(src + 'i18n/' + l)) { + fs.mkdirSync(src + 'i18n/' + l); + } + + fs.writeFileSync(src + 'i18n/' + l + '/translations.json', lang2data(obj)); + } + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function words2languagesFlat(src) { + var langs = Object.assign({}, languages); + var data = readWordJs(src); + if (data) { + for (var word in data) { + if (data.hasOwnProperty(word)) { + for (var lang in data[word]) { + if (data[word].hasOwnProperty(lang)) { + langs[lang][word] = data[word][lang]; + // pre-fill all other languages + for (var j in langs) { + if (langs.hasOwnProperty(j)) { + langs[j][word] = langs[j][word] || EMPTY; + } + } + } + } + } + } + var keys = Object.keys(langs.en); + //keys.sort(); + for (var l in langs) { + if (!langs.hasOwnProperty(l)) continue; + var obj = {}; + for (var k = 0; k < keys.length; k++) { + obj[keys[k]] = langs[l][keys[k]]; + } + langs[l] = obj; + } + if (!fs.existsSync(src + 'i18n/')) { + fs.mkdirSync(src + 'i18n/'); + } + for (var ll in langs) { + if (!langs.hasOwnProperty(ll)) continue; + if (!fs.existsSync(src + 'i18n/' + ll)) { + fs.mkdirSync(src + 'i18n/' + ll); + } + + fs.writeFileSync(src + 'i18n/' + ll + '/flat.txt', lang2data(langs[ll], langs.en)); + } + fs.writeFileSync(src + 'i18n/flat.txt', keys.join('\n')); + } else { + console.error('Cannot read or parse ' + fileName); + } +} +function languagesFlat2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + var keys = fs.readFileSync(src + 'i18n/flat.txt').toString().split('\n'); + + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + var values = fs.readFileSync(src + 'i18n/' + lang + '/flat.txt').toString().split('\n'); + langs[lang] = {}; + keys.forEach(function (word, i) { + langs[lang][word] = values[i].replace(/<\/ i>/g, '').replace(/<\/ b>/g, '').replace(/<\/ span>/g, '').replace(/% s/g, ' %s'); + }); + + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'flat.txt']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} +function languages2words(src) { + var dirs = fs.readdirSync(src + 'i18n/'); + var langs = {}; + var bigOne = {}; + var order = Object.keys(languages); + dirs.sort(function (a, b) { + var posA = order.indexOf(a); + var posB = order.indexOf(b); + if (posA === -1 && posB === -1) { + if (a > b) return 1; + if (a < b) return -1; + return 0; + } else if (posA === -1) { + return -1; + } else if (posB === -1) { + return 1; + } else { + if (posA > posB) return 1; + if (posA < posB) return -1; + return 0; + } + }); + for (var l = 0; l < dirs.length; l++) { + if (dirs[l] === 'flat.txt') continue; + var lang = dirs[l]; + langs[lang] = fs.readFileSync(src + 'i18n/' + lang + '/translations.json').toString(); + langs[lang] = JSON.parse(langs[lang]); + var words = langs[lang]; + for (var word in words) { + if (words.hasOwnProperty(word)) { + bigOne[word] = bigOne[word] || {}; + if (words[word] !== EMPTY) { + bigOne[word][lang] = words[word]; + } + } + } + } + // read actual words.js + var aWords = readWordJs(); + + var temporaryIgnore = ['pt', 'fr', 'nl', 'it']; + if (aWords) { + // Merge words together + for (var w in aWords) { + if (aWords.hasOwnProperty(w)) { + if (!bigOne[w]) { + console.warn('Take from actual words.js: ' + w); + bigOne[w] = aWords[w] + } + dirs.forEach(function (lang) { + if (temporaryIgnore.indexOf(lang) !== -1) return; + if (!bigOne[w][lang]) { + console.warn('Missing "' + lang + '": ' + w); + } + }); + } + } + + } + + writeWordJs(bigOne, src); +} + +gulp.task('adminWords2languages', function (done) { + words2languages('./admin/'); + done(); +}); + +gulp.task('adminWords2languagesFlat', function (done) { + words2languagesFlat('./admin/'); + done(); +}); + +gulp.task('adminLanguagesFlat2words', function (done) { + languagesFlat2words('./admin/'); + done(); +}); + +gulp.task('adminLanguages2words', function (done) { + languages2words('./admin/'); + done(); +}); + + +gulp.task('updatePackages', function (done) { + iopackage.common.version = pkg.version; + iopackage.common.news = iopackage.common.news || {}; + if (!iopackage.common.news[pkg.version]) { + var news = iopackage.common.news; + var newNews = {}; + + newNews[pkg.version] = { + en: 'news', + de: 'neues', + ru: 'новое' + }; + iopackage.common.news = Object.assign(newNews, news); + } + fs.writeFileSync('io-package.json', JSON.stringify(iopackage, null, 4)); + done(); +}); + +gulp.task('updateReadme', function (done) { + var readme = fs.readFileSync('README.md').toString(); + var pos = readme.indexOf('## Changelog\n'); + if (pos !== -1) { + var readmeStart = readme.substring(0, pos + '## Changelog\n'.length); + var readmeEnd = readme.substring(pos + '## Changelog\n'.length); + + if (readme.indexOf(version) === -1) { + var timestamp = new Date(); + var date = timestamp.getFullYear() + '-' + + ('0' + (timestamp.getMonth() + 1).toString(10)).slice(-2) + '-' + + ('0' + (timestamp.getDate()).toString(10)).slice(-2); + + var news = ''; + if (iopackage.common.news && iopackage.common.news[pkg.version]) { + news += '* ' + iopackage.common.news[pkg.version].en; + } + + fs.writeFileSync('README.md', readmeStart + '### ' + version + ' (' + date + ')\n' + (news ? news + '\n\n' : '\n') + readmeEnd); + } + } + done(); +}); + +gulp.task('default', ['updatePackages', 'updateReadme']); \ No newline at end of file diff --git a/io-package.json b/io-package.json new file mode 100644 index 0000000..e7ce4fa --- /dev/null +++ b/io-package.json @@ -0,0 +1,95 @@ +{ + "common": { + "name": "sql", + "title": "SQL History", + "titleLang": { + "en": "SQL logging" + }, + "desc": { + "en": "Logging of states into SQL DB" + }, + "version": "1.9.2", + "mode": "daemon", + "platform": "Node.js", + "loglevel": "info", + "messagebox": true, + "subscribe": "messagebox", + "keywords": ["charts", "sql", "logging", "graphs", "archive"], + "preserveSettings": "custom", + "supportCustoms": true, + "getHistory": true, + "enabled": true, + "stopBeforeUpdate": true, + "materialize": true, + "authors": [ + "bluefox ", + "Apollon77 " + ], + "license": "MIT", + "readme": "https://git.spacen.net/yunkong2/yunkong2.sql/blob/master/README.md", + "icon": "sql.png", + "extIcon": "https://git.spacen.net/yunkong2/yunkong2.sql/raw/master/admin/sql.png", + "type": "storage", + "dependencies": [{"js-controller": ">=0.12.0","admin": ">=1.6.10"}], + "supportStopInstance": 2000, + "config":{ + "minWidth": 570, + "width ": 435, + "minHeight": 200, + "height": 540 + } + }, + "native": { + "connLink": "", + "debounce": 1000, + "retention": 31536000, + "host": "localhost", + "port": 0, + "user": "", + "password": "", + "dbtype": "sqlite", + "fileName": "sqlite.db", + "requestInterval": 0, + "encrypt": false, + "round": 4, + "dbname": "yunkong2", + "multiRequests": true, + "changesRelogInterval": 0, + "changesMinDelta": 0, + "writeNulls": true + }, + "instanceObjects": [ + { + "_id": "info", + "type": "channel", + "common": { + "name": "Information" + }, + "native": {} + }, + { + "_id": "info.connection", + "type": "state", + "common": { + "role": "indicator.connected", + "name": "If connected to DB", + "type": "boolean", + "read": true, + "write": false, + "def": false + }, + "native": {} + } + ], + "objects": [ + { + "_id": "_design/custom", + "language": "javascript", + "views": { + "state": { + "map": "function(doc) { if (doc.type==='state' && (doc.common.custom || doc.common.history)) emit(doc._id, doc.common.custom || doc.common.history) }" + } + } + } + ] +} diff --git a/lib/aggregate.js b/lib/aggregate.js new file mode 100644 index 0000000..e86531b --- /dev/null +++ b/lib/aggregate.js @@ -0,0 +1,476 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +"use strict"; +// THIS file should be identical with sql and history adapter's one + +function initAggregate(options) { + + //step; // 1 Step is 1 second + if (options.step === null) { + options.step = (options.end - options.start) / options.count; + } + + // Limit 2000 + if ((options.end - options.start) / options.step > options.limit) { + options.step = (options.end - options.start) / options.limit; + } + + options.maxIndex = Math.ceil(((options.end - options.start) / options.step) - 1); + options.result = []; + options.averageCount = []; + options.aggregate = options.aggregate || 'minmax'; + options.overallLength = 0; + + // pre-fill the result with timestamps (add one before start and one after end) + for (var i = 0; i <= options.maxIndex + 2; i++){ + + options.result[i] = { + val: {ts: null, val: null}, + max: {ts: null, val: null}, + min: {ts: null, val: null}, + start: {ts: null, val: null}, + end: {ts: null, val: null} + }; + + if (options.aggregate === 'average') options.averageCount[i] = 0; + } + return options; +} + +function aggregation(options, data) { + var index; + var preIndex; + for (var i = 0; i < data.length; i++) { + if (!data[i]) continue; + if (typeof data[i].ts !== 'number') data[i].ts = parseInt(data[i].ts, 10); + if (data[i].ts < 946681200000) data[i].ts *= 1000; + + preIndex = Math.floor((data[i].ts - options.start) / options.step); + + // store all border values + if (preIndex < 0) { + index = 0; + } else if (preIndex > options.maxIndex) { + index = options.maxIndex + 2; + } else { + index = preIndex + 1; + } + options.overallLength++; + + if (!options.result[index]) { + console.error('Cannot find index ' + index); + continue; + } + + if (options.aggregate === 'max') { + if (!options.result[index].val.ts) options.result[index].val.ts = Math.round(options.start + (((index - 1) + 0.5) * options.step)); + if (options.result[index].val.val === null || options.result[index].val.val < data[i].val) options.result[index].val.val = data[i].val; + } else if (options.aggregate === 'min') { + if (!options.result[index].val.ts) options.result[index].val.ts = Math.round(options.start + (((index - 1) + 0.5) * options.step)); + if (options.result[index].val.val === null || options.result[index].val.val > data[i].val) options.result[index].val.val = data[i].val; + } else if (options.aggregate === 'average') { + if (!options.result[index].val.ts) options.result[index].val.ts = Math.round(options.start + (((index - 1) + 0.5) * options.step)); + options.result[index].val.val += parseFloat(data[i].val); + options.averageCount[index]++; + } else if (options.aggregate === 'total') { + if (!options.result[index].val.ts) options.result[index].val.ts = Math.round(options.start + (((index - 1) + 0.5) * options.step)); + options.result[index].val.val += parseFloat(data[i].val); + } else if (options.aggregate === 'minmax') { + if (options.result[index].min.ts === null) { + options.result[index].min.ts = data[i].ts; + options.result[index].min.val = data[i].val; + + options.result[index].max.ts = data[i].ts; + options.result[index].max.val = data[i].val; + + options.result[index].start.ts = data[i].ts; + options.result[index].start.val = data[i].val; + + options.result[index].end.ts = data[i].ts; + options.result[index].end.val = data[i].val; + } else { + if (data[i].val !== null) { + if (data[i].val > options.result[index].max.val) { + options.result[index].max.ts = data[i].ts; + options.result[index].max.val = data[i].val; + } else if (data[i].val < options.result[index].min.val) { + options.result[index].min.ts = data[i].ts; + options.result[index].min.val = data[i].val; + } + if (data[i].ts > options.result[index].end.ts) { + options.result[index].end.ts = data[i].ts; + options.result[index].end.val = data[i].val; + } + } else { + if (data[i].ts > options.result[index].end.ts) { + options.result[index].end.ts = data[i].ts; + options.result[index].end.val = null; + } + } + } + } + } + + return {result: options.result, step: options.step, sourceLength: data.length} ; +} + +function finishAggregation(options) { + if (options.aggregate === 'minmax') { + for (var ii = options.result.length - 1; ii >= 0; ii--) { + // no one value in this period + if (options.result[ii].start.ts === null) { + options.result.splice(ii, 1); + } else { + // just one value in this period: max == min == start == end + if (options.result[ii].start.ts === options.result[ii].end.ts) { + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } else + if (options.result[ii].min.ts === options.result[ii].max.ts) { + // if just 2 values: start == min == max, end + if (options.result[ii].start.ts === options.result[ii].min.ts || + options.result[ii].end.ts === options.result[ii].min.ts) { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } // if just 3 values: start, min == max, end + else { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].max.ts, + val: options.result[ii].max.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } + } else + if (options.result[ii].start.ts === options.result[ii].max.ts) { + // just one value in this period: start == max, min == end + if (options.result[ii].min.ts === options.result[ii].end.ts) { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } // start == max, min, end + else { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].min.ts, + val: options.result[ii].min.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } + } else + if (options.result[ii].end.ts === options.result[ii].max.ts) { + // just one value in this period: start == min, max == end + if (options.result[ii].min.ts === options.result[ii].start.ts) { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } // start, min, max == end + else { + + options.result.splice(ii + 1, 0, { + ts: options.result[ii].min.ts, + val: options.result[ii].min.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } + } else + if (options.result[ii].start.ts === options.result[ii].min.ts || + options.result[ii].end.ts === options.result[ii].min.ts) { + // just one value in this period: start == min, max, end + options.result.splice(ii + 1, 0, { + ts: options.result[ii].max.ts, + val: options.result[ii].max.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } else { + // just one value in this period: start == min, max, end + if (options.result[ii].max.ts > options.result[ii].min.ts) { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].min.ts, + val: options.result[ii].min.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].max.ts, + val: options.result[ii].max.val + }); + } else { + options.result.splice(ii + 1, 0, { + ts: options.result[ii].max.ts, + val: options.result[ii].max.val + }); + options.result.splice(ii + 2, 0, { + ts: options.result[ii].min.ts, + val: options.result[ii].min.val + }); + } + options.result.splice(ii + 3, 0, { + ts: options.result[ii].end.ts, + val: options.result[ii].end.val + }); + options.result[ii] = { + ts: options.result[ii].start.ts, + val: options.result[ii].start.val + }; + } + } + } + } else if (options.aggregate === 'average') { + for (var k = options.result.length - 1; k >= 0; k--) { + if (options.result[k].val.ts) { + options.result[k] = { + ts: options.result[k].val.ts, + val: (options.result[k].val.val !== null) ? Math.round(options.result[k].val.val / options.averageCount[k] * 100) / 100 : null + }; + } else { + // no one value in this interval + options.result.splice(k, 1); + } + } + } else { + for (var j = options.result.length - 1; j >= 0; j--) { + if (options.result[j].val.ts) { + options.result[j] = { + ts: options.result[j].val.ts, + val: options.result[j].val.val + }; + } else { + // no one value in this interval + options.result.splice(j, 1); + } + } + } + beautify(options); +} + +function beautify(options) { + var preFirstValue = null; + var postLastValue = null; + if (options.ignoreNull === 'true') options.ignoreNull = true; // include nulls and replace them with last value + if (options.ignoreNull === 'false') options.ignoreNull = false; // include nulls + if (options.ignoreNull === '0') options.ignoreNull = 0; // include nulls and replace them with 0 + if (options.ignoreNull !== true && options.ignoreNull !== false && options.ignoreNull !== 0) options.ignoreNull = false; + + // process null values, remove points outside the span and find first points after end and before start + for (var i = 0; i < options.result.length; i++) { + if (options.ignoreNull !== false) { + // if value is null + if (options.result[i].val === null) { + // null value must be replaced with last not null value + if (options.ignoreNull === true) { + // remove value + options.result.splice(i, 1); + i--; + continue; + } else { + // null value must be replaced with 0 + options.result[i].val = options.ignoreNull; + } + } + } + + // remove all not requested points + if (options.result[i].ts < options.start) { + preFirstValue = options.result[i].val !== null ? options.result[i] : null; + options.result.splice(i, 1); + i--; + continue; + } + + postLastValue = options.result[i].val !== null ? options.result[i] : null; + + if (options.result[i].ts > options.end) { + options.result.splice(i, options.result.length - i); + break; + } + } + + // check start and stop + if (options.result.length && options.aggregate !== 'none') { + var firstTS = options.result[0].ts; + + if (firstTS > options.start) { + if (preFirstValue) { + var firstY = options.result[0].val; + // if steps + if (options.aggregate === 'onchange' || !options.aggregate) { + if (preFirstValue.ts !== firstTS) { + options.result.unshift({ts: options.start, val: preFirstValue.val}); + } else { + if (options.ignoreNull) { + options.result.unshift({ts: options.start, val: firstY}); + } + } + } else { + if (preFirstValue.ts !== firstTS) { + if (firstY !== null) { + // interpolate + var y = preFirstValue.val + (firstY - preFirstValue.val) * (options.start - preFirstValue.ts) / (firstTS - preFirstValue.ts); + options.result.unshift({ts: options.start, val: y}); + } else { + options.result.unshift({ts: options.start, val: null}); + } + } else { + if (options.ignoreNull) { + options.result.unshift({ts: options.start, val: firstY}); + } + } + } + } else { + if (options.ignoreNull) { + options.result.unshift({ts: options.start, val: options.result[0].val}); + } else { + options.result.unshift({ts: options.start, val: null}); + } + } + } + + var lastTS = options.result[options.result.length - 1].ts; + if (lastTS < options.end) { + if (postLastValue) { + // if steps + if (options.aggregate === 'onchange' || !options.aggregate) { + // if more data following, draw line to the end of chart + if (postLastValue.ts !== lastTS) { + options.result.push({ts: options.end, val: postLastValue.val}); + } else { + if (options.ignoreNull) { + options.result.push({ts: options.end, val: postLastValue.val}); + } + } + } else { + if (postLastValue.ts !== lastTS) { + var lastY = options.result[options.result.length - 1].val; + if (lastY !== null) { + // make interpolation + var _y = lastY + (postLastValue.val - lastY) * (options.end - lastTS) / (postLastValue.ts - lastTS); + options.result.push({ts: options.end, val: _y}); + } else { + options.result.push({ts: options.end, val: null}); + } + } else { + if (options.ignoreNull) { + options.result.push({ts: options.end, val: postLastValue.val}); + } + } + } + } else { + if (options.ignoreNull) { + var lastY = options.result[options.result.length - 1].val; + // if no more data, that means do not draw line + options.result.push({ts: options.end, val: lastY}); + } else { + // if no more data, that means do not draw line + options.result.push({ts: options.end, val: null}); + } + } + } + } + else if (options.aggregate === 'none') { + if ((options.count) && (options.result.length > options.count)) { + options.result = options.result.slice(0, options.count); + } + } +} + +function sendResponse(adapter, msg, options, data, startTime) { + var aggregateData; + if (typeof data === 'string') { + adapter.log.error(data); + return adapter.sendTo(msg.from, msg.command, { + result: [], + step: 0, + error: data, + sessionId: options.sessionId + }, msg.callback); + } + + if (options.count && !options.start && data.length > options.count) { + data.splice(0, data.length - options.count); + } + if (data[0]) { + options.start = options.start || data[0].ts; + + if (!options.aggregate || options.aggregate === 'onchange' || options.aggregate === 'none') { + aggregateData = {result: data, step: 0, sourceLength: data.length}; + + // convert ack from 0/1 to false/true + if (options.ack && aggregateData.result) { + for (var i = 0; i < aggregateData.result.length; i++) { + aggregateData.result[i].ack = !!aggregateData.result[i].ack; + } + } + options.result = aggregateData.result; + beautify(options); + if ((options.aggregate === 'none') && (options.count) && (options.result.length > options.count)) { + options.result = options.result.slice(0, options.count); + } + aggregateData.result = options.result; + } else { + initAggregate(options); + aggregateData = aggregation(options, data); + finishAggregation(options); + aggregateData.result = options.result; + } + + adapter.log.debug('Send: ' + aggregateData.result.length + ' of: ' + aggregateData.sourceLength + ' in: ' + (new Date().getTime() - startTime) + 'ms'); + adapter.sendTo(msg.from, msg.command, { + result: aggregateData.result, + step: aggregateData.step, + error: null, + sessionId: options.sessionId + }, msg.callback); + } else { + adapter.log.info('No Data'); + adapter.sendTo(msg.from, msg.command, {result: [], step: null, error: null, sessionId: options.sessionId}, msg.callback); + } +} + +module.exports.sendResponse = sendResponse; +module.exports.initAggregate = initAggregate; +module.exports.aggregation = aggregation; +module.exports.beautify = beautify; +module.exports.finishAggregation = finishAggregation; diff --git a/lib/mssql-client.js b/lib/mssql-client.js new file mode 100644 index 0000000..98a3d65 --- /dev/null +++ b/lib/mssql-client.js @@ -0,0 +1,86 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var ConnectionFactory, SQLClient, SQLClientPool, MSSQLClient, MSSQLClientPool, MSSQLConnectionFactory, mssql, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + + SQLClient = require('sql-client/lib/sql-client').SQLClient; + + SQLClientPool = require('sql-client/lib/sql-client-pool').SQLClientPool; + + ConnectionFactory = require('sql-client/lib/connection-factory').ConnectionFactory; + + mssql = require('mssql'); + + MSSQLConnectionFactory = (function(superClass) { + extend(MSSQLConnectionFactory, superClass); + + function MSSQLConnectionFactory() { + this.execute = bind(this.execute, this); + this.open_connection = bind(this.open_connection, this); + return MSSQLConnectionFactory.__super__.constructor.apply(this, arguments); + } + + MSSQLConnectionFactory.prototype.open_connection = function(config, callback) { + var connection; + var pos = config.server.indexOf(':'); + if (pos != -1) { + config.port = parseInt(config.server.substring(pos + 1), 10); + config.server = config.server.substring(0, pos); + } + + return connection = new mssql.Connection(config, function (err) { + if (err != null) { + return callback(err); + } else { + return callback(null, connection); + } + }.bind(this)); + }; + + MSSQLConnectionFactory.prototype.execute = function(connection, sql, bindvars, callback) { + var request = new mssql.Request(connection); + return request.query(sql, function(err, recordset) { + callback(err, recordset); + }); + }; + + return MSSQLConnectionFactory; + + })(ConnectionFactory); + + MSSQLClient = (function(superClass) { + extend(MSSQLClient, superClass); + + function MSSQLClient() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + MSSQLClient.__super__.constructor.apply(this, slice.call(options).concat([new MSSQLConnectionFactory()])); + } + + return MSSQLClient; + + })(SQLClient); + + MSSQLClientPool = (function(superClass) { + extend(MSSQLClientPool, superClass); + + function MSSQLClientPool() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + MSSQLClientPool.__super__.constructor.apply(this, slice.call(options).concat([new MSSQLConnectionFactory()])); + } + + return MSSQLClientPool; + + })(SQLClientPool); + + exports.MSSQLConnectionFactory = MSSQLConnectionFactory; + + exports.MSSQLClient = MSSQLClient; + + exports.MSSQLClientPool = MSSQLClientPool; + +}).call(this); diff --git a/lib/mssql.js b/lib/mssql.js new file mode 100644 index 0000000..ea0181f --- /dev/null +++ b/lib/mssql.js @@ -0,0 +1,151 @@ +exports.init = function (dbname) { + return [ + "CREATE DATABASE " + dbname + ";", + "CREATE TABLE " + dbname + ".dbo.sources (id INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1), name varchar(255));", + "CREATE TABLE " + dbname + ".dbo.datapoints (id INTEGER NOT NULL PRIMARY KEY IDENTITY(1,1), name varchar(255), type INTEGER);", + "CREATE TABLE " + dbname + ".dbo.ts_number (id INTEGER, ts BIGINT, val REAL, ack BIT, _from INTEGER, q INTEGER);", + "CREATE TABLE " + dbname + ".dbo.ts_string (id INTEGER, ts BIGINT, val TEXT, ack BIT, _from INTEGER, q INTEGER);", + "CREATE TABLE " + dbname + ".dbo.ts_bool (id INTEGER, ts BIGINT, val BIT, ack BIT, _from INTEGER, q INTEGER);" + ]; +}; + +exports.destroy = function (dbname) { + return [ + "DROP TABLE " + dbname + ".dbo.ts_number;", + "DROP TABLE " + dbname + ".dbo.ts_string;", + "DROP TABLE " + dbname + ".dbo.ts_bool;", + "DROP TABLE " + dbname + ".dbo.sources;", + "DROP TABLE " + dbname + ".dbo.datapoints;", + "DROP DATABASE " + dbname + ";", + "DBCC FREEPROCCACHE;" + ]; +}; + +exports.getFirstTs = function (dbname, db) { + return "SELECT id, MIN(ts) AS ts FROM " + dbname + ".dbo." + db + " GROUP BY id;"; +}; + +exports.insert = function (dbname, index, state, from, db) { + if (state.val === null) state.val = 'NULL'; + else if (db === "ts_bool") state.val = state.val ? 1 : 0; + else if (db === "ts_string") state.val = "'" + state.val.toString().replace(/'/g, '') + "'"; + + return "INSERT INTO " + dbname + ".dbo." + db + " (id, ts, val, ack, _from, q) VALUES(" + index + ", " + state.ts + ", " + state.val + ", " + (state.ack ? 1 : 0) + ", " + (from || 0) + ", " + (state.q || 0) + ");"; +}; + +exports.retention = function (dbname, index, db, retention) { + var d = new Date(); + d.setSeconds(-retention); + var query = "DELETE FROM " + dbname + ".dbo." + db + " WHERE"; + query += " id=" + index; + query += " AND ts < " + d.getTime(); + query += ";"; + return query; +}; + +exports.getIdSelect = function (dbname, name) { + if (!name) { + return "SELECT id, type, name FROM " + dbname + ".dbo.datapoints;"; + } else { + return "SELECT id, type, name FROM " + dbname + ".dbo.datapoints WHERE name='" + name + "';"; + } +}; + +exports.getIdInsert = function (dbname, name, type) { + return "INSERT INTO " + dbname + ".dbo.datapoints (name, type) VALUES('" + name + "', " + type + ");"; +}; + +exports.getIdUpdate = function (dbname, id, type) { + return "UPDATE " + dbname + ".dbo.datapoints SET type = " + type + " WHERE id = " + id + ";"; +}; + +exports.getFromSelect = function (dbname, from) { + return "SELECT id FROM " + dbname + ".dbo.sources WHERE name='" + from + "';"; +}; + +exports.getFromInsert = function (dbname, from) { + return "INSERT INTO " + dbname + ".dbo.sources (name) VALUES('" + from + "');"; +}; + +exports.getHistory = function (dbname, db, options) { + var query = "SELECT "; + if (!options.start && options.count) { + query += " TOP " + options.count; + } + query += " ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", " + dbname + ".dbo.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + dbname + ".dbo." + db; + + if (options.from) { + query += " INNER JOIN " + dbname + ".dbo.sources ON " + dbname + ".dbo.sources.id=" + dbname + ".dbo." + db + "._from"; + } + + var where = ""; + + if (options.id) { + where += " " + dbname + ".dbo." + db + ".id=" + options.id; + } + if (options.end) { + where += (where ? " AND" : "") + " " + dbname + ".dbo." + db + ".ts < " + options.end; + } + if (options.start) { + where += (where ? " AND" : "") + " " + dbname + ".dbo." + db + ".ts >= " + options.start; + + // add last value before start + var subQuery; + var subWhere; + subQuery = " SELECT TOP 1 ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", " + dbname + ".dbo.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + dbname + ".dbo." + db; + if (options.from) { + subQuery += " INNER JOIN " + dbname + ".dbo.sources ON " + dbname + ".dbo.sources.id=" + dbname + ".dbo." + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + dbname + ".dbo." + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + dbname + ".dbo." + db + ".ts < " + options.start; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + dbname + ".dbo." + db + ".ts DESC"; + where += " UNION SELECT * FROM (" + subQuery + ") a"; + + // add next value after end + subQuery = " SELECT TOP 1 ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", " + dbname + ".dbo.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + dbname + ".dbo." + db; + if (options.from) { + subQuery += " INNER JOIN " + dbname + ".dbo.sources ON " + dbname + ".dbo.sources.id=" + dbname + ".dbo." + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + dbname + ".dbo." + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + dbname + ".dbo." + db + ".ts >= " + options.end; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + dbname + ".dbo." + db + ".ts ASC"; + where += " UNION SELECT * FROM (" + subQuery + ") b"; + } + + if (where) query += " WHERE " + where; + + query += " ORDER BY ts"; + if (!options.start && options.count) { + query += " DESC"; + } else { + query += " ASC"; + } + query += ";"; + return query; +}; diff --git a/lib/mysql.js b/lib/mysql.js new file mode 100644 index 0000000..97d291f --- /dev/null +++ b/lib/mysql.js @@ -0,0 +1,146 @@ +exports.init = function (dbname) { + return [ + "CREATE DATABASE `" + dbname + "` DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_general_ci;", + "CREATE TABLE `" + dbname + "`.sources (id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, name TEXT);", + "CREATE TABLE `" + dbname + "`.datapoints (id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, name TEXT, type INTEGER);", + "CREATE TABLE `" + dbname + "`.ts_number (id INTEGER, ts BIGINT, val REAL, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE `" + dbname + "`.ts_string (id INTEGER, ts BIGINT, val TEXT, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE `" + dbname + "`.ts_bool (id INTEGER, ts BIGINT, val BOOLEAN, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));" + ]; +}; + +exports.destroy = function (dbname) { + return [ + "DROP TABLE `" + dbname + "`.ts_number;", + "DROP TABLE `" + dbname + "`.ts_string;", + "DROP TABLE `" + dbname + "`.ts_bool;", + "DROP TABLE `" + dbname + "`.sources;", + "DROP TABLE `" + dbname + "`.datapoints;", + "DROP DATABASE `" + dbname + "`;" + ]; +}; + +exports.getFirstTs = function (dbname, db) { + return "SELECT id, MIN(ts) AS ts FROM `" + dbname + "`." + db + " GROUP BY id;"; +}; + +exports.insert = function (dbname, index, state, from, db) { + if (state.val === null) state.val = 'NULL'; + else if (db === "ts_string") state.val = "'" + state.val.toString().replace(/'/g, '') + "'"; + return "INSERT INTO `" + dbname + "`." + db + " (id, ts, val, ack, _from, q) VALUES(" + index + ", " + state.ts + ", " + state.val + ", " + (state.ack ? 1 : 0) + ", " + (from || 0) + ", " + (state.q || 0) + ");"; +}; + +exports.retention = function (dbname, index, db, retention) { + var d = new Date(); + d.setSeconds(-retention); + var query = "DELETE FROM `" + dbname + "`." + db + " WHERE"; + query += " id=" + index; + query += " AND ts < " + d.getTime(); + query += ";"; + return query; +}; + +exports.getIdSelect = function (dbname, name) { + if (!name) { + return "SELECT id, type, name FROM `" + dbname + "`.datapoints;"; + } else { + return "SELECT id, type, name FROM `" + dbname + "`.datapoints WHERE name='" + name + "';"; + } +}; + +exports.getIdInsert = function (dbname, name, type) { + return "INSERT INTO `" + dbname + "`.datapoints (name, type) VALUES('" + name + "', " + type + ");"; +}; + +exports.getIdUpdate = function (dbname, id, type) { + return "UPDATE `" + dbname + "`.datapoints SET type = " + type + " WHERE id = " + id + ";"; +}; + +exports.getFromSelect = function (dbname, from) { + return "SELECT id FROM `" + dbname + "`.sources WHERE name='" + from + "';"; +}; + +exports.getFromInsert = function (dbname, from) { + return "INSERT INTO `" + dbname + "`.sources (name) VALUES('" + from + "');"; +}; + +exports.getHistory = function (dbname, db, options) { + var query = "SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", `" + dbname + "`.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM `" + dbname + "`." + db; + + if (options.from) { + query += " INNER JOIN `" + dbname + "`.sources ON `" + dbname + "`.sources.id=`" + dbname + "`." + db + "._from"; + } + + var where = ""; + + if (options.id) { + where += " `" + dbname + "`." + db + ".id=" + options.id; + } + if (options.end) { + where += (where ? " AND" : "") + " `" + dbname + "`." + db + ".ts < " + options.end; + } + if (options.start) { + where += (where ? " AND" : "") + " `" + dbname + "`." + db + ".ts >= " + options.start; + + var subQuery; + var subWhere; + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", `" + dbname + "`.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM `" + dbname + "`." + db; + if (options.from) { + subQuery += " INNER JOIN `" + dbname + "`.sources ON `" + dbname + "`.sources.id=`" + dbname + "`." + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " `" + dbname + "`." + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " `" + dbname + "`." + db + ".ts < " + options.start; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY `" + dbname + "`." + db + ".ts DESC LIMIT 1"; + where += " UNION (" + subQuery + ")"; + + //add next value after end + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", `" + dbname + "`.sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM `" + dbname + "`." + db; + if (options.from) { + subQuery += " INNER JOIN `" + dbname + "`.sources ON `" + dbname + "`.sources.id=`" + dbname + "`." + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " `" + dbname + "`." + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " `" + dbname + "`." + db + ".ts >= " + options.end; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY `" + dbname + "`." + db + ".ts ASC LIMIT 1"; + where += " UNION (" + subQuery + ")"; + + } + + if (where) query += " WHERE " + where; + + query += " ORDER BY ts"; + + if (!options.start && options.count) { + query += " DESC LIMIT " + options.count; + } else { + query += " ASC"; + } + + query += ";"; + return query; +}; diff --git a/lib/postgresql-client.js b/lib/postgresql-client.js new file mode 100644 index 0000000..6693209 --- /dev/null +++ b/lib/postgresql-client.js @@ -0,0 +1,158 @@ +// Generated by CoffeeScript 1.10.0 +(function() { + var ConnectionFactory, PostgreSQLClient, PostgreSQLClient2, PostgreSQLClientPool, PostgreSQLClientPool2, PostgreSQLConnectionFactory, PostgreSQLConnectionFactory2, SQLClient, SQLClientPool, pg, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice; + + SQLClient = require('sql-client/lib/sql-client').SQLClient; + + SQLClientPool = require('sql-client/lib/sql-client-pool').SQLClientPool; + + ConnectionFactory = require('sql-client/lib/connection-factory').ConnectionFactory; + + pg = require('pg'); + try { + if ((pg != null ? pg["native"] : void 0) != null) { + pg = pg["native"]; + } + } catch(err) { + + } + + + PostgreSQLConnectionFactory = (function(superClass) { + extend(PostgreSQLConnectionFactory, superClass); + + function PostgreSQLConnectionFactory() { + this.pre_process_sql = bind(this.pre_process_sql, this); + this.open_connection = bind(this.open_connection, this); + return PostgreSQLConnectionFactory.__super__.constructor.apply(this, arguments); + } + + PostgreSQLConnectionFactory.prototype.open_connection = function(connect_string, callback) { + var connection; + connection = new pg.Client(connect_string); + return connection.connect((function(_this) { + return function(err) { + return callback(err, connection); + }; + })(this)); + }; + + PostgreSQLConnectionFactory.prototype.pre_process_sql = function(sql, bindvars, callback) { + var index; + if ((sql != null) && (bindvars != null)) { + index = 1; + sql = sql.replace(/\?/g, (function() { + return '$' + index++; + })); + } + return callback(null, sql, bindvars); + }; + + return PostgreSQLConnectionFactory; + + })(ConnectionFactory); + + PostgreSQLClient = (function(superClass) { + extend(PostgreSQLClient, superClass); + + function PostgreSQLClient() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + PostgreSQLClient.__super__.constructor.apply(this, slice.call(options).concat([new PostgreSQLConnectionFactory()])); + } + + return PostgreSQLClient; + + })(SQLClient); + + PostgreSQLClientPool = (function(superClass) { + extend(PostgreSQLClientPool, superClass); + + function PostgreSQLClientPool() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + PostgreSQLClientPool.__super__.constructor.apply(this, slice.call(options).concat([new PostgreSQLConnectionFactory()])); + } + + return PostgreSQLClientPool; + + })(SQLClientPool); + + exports.PostgreSQLConnectionFactory = PostgreSQLConnectionFactory; + + exports.PostgreSQLClient = PostgreSQLClient; + + exports.PostgreSQLClientPool = PostgreSQLClientPool; + + PostgreSQLConnectionFactory2 = (function(superClass) { + extend(PostgreSQLConnectionFactory2, superClass); + + function PostgreSQLConnectionFactory2() { + this.close_connection = bind(this.close_connection, this); + this.open_connection = bind(this.open_connection, this); + return PostgreSQLConnectionFactory2.__super__.constructor.apply(this, arguments); + } + + PostgreSQLConnectionFactory2.prototype.open_connection = function(connect_string, callback) { + return pg.connect(connect_string, (function(_this) { + return function(err, client, done_fn) { + var connection; + connection = client; + if (connection != null) { + connection._sqlclient_done = done_fn; + } + return callback(err, connection); + }; + })(this)); + }; + + PostgreSQLConnectionFactory2.prototype.close_connection = function(connection, callback) { + if ((connection != null ? connection._sqlclient_done : void 0) != null) { + connection._sqlclient_done(); + return typeof callback === "function" ? callback(null) : void 0; + } else { + return PostgreSQLConnectionFactory2.__super__.close_connection.apply(this, arguments).close_connection(connection, callback); + } + }; + + return PostgreSQLConnectionFactory2; + + })(PostgreSQLConnectionFactory); + + PostgreSQLClient2 = (function(superClass) { + extend(PostgreSQLClient2, superClass); + + function PostgreSQLClient2() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + PostgreSQLClient2.__super__.constructor.apply(this, slice.call(options).concat([new PostgreSQLConnectionFactory2()])); + } + + return PostgreSQLClient2; + + })(SQLClient); + + PostgreSQLClientPool2 = (function(superClass) { + extend(PostgreSQLClientPool2, superClass); + + function PostgreSQLClientPool2() { + var options; + options = 1 <= arguments.length ? slice.call(arguments, 0) : []; + PostgreSQLClientPool2.__super__.constructor.apply(this, slice.call(options).concat([new PostgreSQLConnectionFactory2()])); + } + + return PostgreSQLClientPool2; + + })(SQLClientPool); + + exports.PostgreSQLConnectionFactory2 = PostgreSQLConnectionFactory2; + + exports.PostgreSQLClient2 = PostgreSQLClient2; + + exports.PostgreSQLClientPool2 = PostgreSQLClientPool2; + +}).call(this); diff --git a/lib/postgresql.js b/lib/postgresql.js new file mode 100644 index 0000000..3f047e6 --- /dev/null +++ b/lib/postgresql.js @@ -0,0 +1,145 @@ +exports.init = function (dbname) { + return [ + "CREATE TABLE sources (id SERIAL NOT NULL PRIMARY KEY, name TEXT);", + "CREATE TABLE datapoints (id SERIAL NOT NULL PRIMARY KEY, name TEXT, type INTEGER);", + "CREATE TABLE ts_number (id INTEGER NOT NULL, ts BIGINT, val REAL, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE ts_string (id INTEGER NOT NULL, ts BIGINT, val TEXT, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE ts_bool (id INTEGER NOT NULL, ts BIGINT, val BOOLEAN, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));" + ]; +}; + +exports.destroy = function (dbname) { + return [ + "DROP TABLE ts_number;", + "DROP TABLE ts_string;", + "DROP TABLE ts_bool;", + "DROP TABLE sources;", + "DROP TABLE datapoints;" + ]; +}; + +exports.getFirstTs = function (dbname, db) { + return "SELECT id, MIN(ts) AS ts FROM " + db + " GROUP BY id;"; +}; + +exports.insert = function (dbname, index, state, from, db) { + if (state.val === null) state.val = 'NULL'; + else if (db === "ts_string") state.val = "'" + state.val.toString().replace(/'/g, '') + "'"; + return "INSERT INTO " + db + " (id, ts, val, ack, _from, q) VALUES(" + index + ", " + state.ts + ", " + state.val + ", " + (!!state.ack) + ", " + (from || 0) + ", " + (state.q || 0) + ");"; +}; + +exports.retention = function (dbname, index, db, retention) { + var d = new Date(); + d.setSeconds(-retention); + var query = "DELETE FROM " + db + " WHERE"; + query += " id=" + index; + query += " AND ts < " + d.getTime(); + query += ";"; + + return query; +}; + +exports.getIdSelect = function (dbname, name) { + if (!name) { + return "SELECT id, type, name FROM datapoints;"; + } else { + return "SELECT id, type, name FROM datapoints WHERE name='" + name + "';"; + } +}; + +exports.getIdInsert = function (dbname, name, type) { + return "INSERT INTO datapoints (name, type) VALUES('" + name + "', " + type + ");"; +}; + +exports.getIdUpdate = function (dbname, id, type) { + return "UPDATE datapoints SET type = " + type + " WHERE id = " + id + ";"; +}; + +exports.getFromSelect = function (dbname, from) { + return "SELECT id FROM sources WHERE name='" + from + "';"; +}; + +exports.getFromInsert = function (dbname, from) { + return "INSERT INTO sources (name) VALUES('" + from + "');"; +}; +exports.getHistory = function (dbname, db, options) { + var query = "SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as from") : "") + + (options.q ? ", q" : "") + " FROM " + db; + + if (options.from) { + query += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + + var where = ""; + + if (options.id) { + where += " " + db + ".id=" + options.id; + } + if (options.end) { + where += (where ? " AND" : "") + " " + db + ".ts < " + options.end; + } + if (options.start) { + where += (where ? " AND" : "") + " " + db + ".ts >= " + options.start; + + //add last value before start + var subQuery; + var subWhere; + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as from") : "") + + (options.q ? ", q" : "") + " FROM " + db; + if (options.from) { + subQuery += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + db + ".ts < " + options.start; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + db + ".ts DESC LIMIT 1"; + where += " UNION (" + subQuery + ")"; + + //add next value after end + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as from") : "") + + (options.q ? ", q" : "") + " FROM " + db; + if (options.from) { + subQuery += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + db + ".ts >= " + options.end; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + db + ".ts ASC LIMIT 1"; + where += " UNION (" + subQuery + ")"; + } + + if (where) query += " WHERE " + where; + + + query += " ORDER BY ts"; + + if (!options.start && options.count) { + query += " DESC LIMIT " + options.count; + } else { + query += " ASC"; + } + + query += ";"; + return query; +}; diff --git a/lib/sqlite.js b/lib/sqlite.js new file mode 100644 index 0000000..02d8945 --- /dev/null +++ b/lib/sqlite.js @@ -0,0 +1,149 @@ +exports.init = function (dbname) { + return [ + "CREATE TABLE sources (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT);", + "CREATE TABLE datapoints (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name TEXT, type INTEGER);", + "CREATE TABLE ts_number (id INTEGER, ts INTEGER, val REAL, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE ts_string (id INTEGER, ts INTEGER, val TEXT, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));", + "CREATE TABLE ts_bool (id INTEGER, ts INTEGER, val BOOLEAN, ack BOOLEAN, _from INTEGER, q INTEGER, PRIMARY KEY(id, ts));" + ]; +}; + +exports.destroy = function (dbname) { + return [ + "DROP TABLE ts_number;", + "DROP TABLE ts_string;", + "DROP TABLE ts_bool;", + "DROP TABLE sources;", + "DROP TABLE datapoints;" + ]; +}; + +exports.getFirstTs = function (dbname, db) { + return "SELECT id, MIN(ts) AS ts FROM " + db + " GROUP BY id;"; +}; + +exports.insert = function (dbname, index, state, from, db) { + if (state.val === null) state.val = 'NULL'; + else if (db === "ts_bool") state.val = state.val ? 1 : 0; + else if (db === "ts_string") state.val = "'" + state.val.toString().replace(/'/g, '') + "'"; + return "INSERT INTO " + db + " (id, ts, val, ack, _from, q) VALUES(" + index + ", " + state.ts + ", " + state.val + ", " + (state.ack ? 1 : 0) + ", " + (from || 0) + ", " + (state.q || 0) + ");"; +}; + +exports.retention = function (dbname, index, db, retention) { + var d = new Date(); + d.setSeconds(-retention); + var query = "DELETE FROM " + db + " WHERE"; + query += " id=" + index; + query += " AND ts < " + d.getTime(); + query += ";"; + return query; +}; + +exports.getIdSelect = function (dbname, name) { + if (!name) { + return "SELECT id, type, name FROM datapoints;"; + } else { + return "SELECT id, type, name FROM datapoints WHERE name='" + name + "';"; + } +}; + +exports.getIdInsert = function (dbname, name, type) { + return "INSERT INTO datapoints (name, type) VALUES('" + name + "', " + type + ");"; +}; + +exports.getIdUpdate = function (dbname, id, type) { + return "UPDATE datapoints SET type = " + type + " WHERE id = " + id + ";"; +}; + +exports.getFromSelect = function (dbname, from) { + if (!from) { + return "SELECT id, name FROM sources;"; + } else { + return "SELECT id FROM sources WHERE name='" + from + "';"; + } +}; + +exports.getFromInsert = function (dbname, from) { + return "INSERT INTO sources (name) VALUES('" + from + "');"; +}; + +exports.getHistory = function (dbname, db, options) { + var query = "SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + db; + + if (options.from) { + query += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + + var where = ""; + + if (options.id) { + where += " " + db + ".id=" + options.id; + } + if (options.end) { + where += (where ? " AND" : "") + " " + db + ".ts < " + options.end; + } + if (options.start) { + where += (where ? " AND" : "") + " " + db + ".ts >= " + options.start; + + //add last value before start + var subQuery; + var subWhere; + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + db; + if (options.from) { + subQuery += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + db + ".ts < " + options.start; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + db + ".ts DESC LIMIT 1"; + where += " UNION SELECT * from (" + subQuery + ")"; + + //add next value after end + subQuery = " SELECT ts, val" + + (!options.id ? (", " + db + ".id as id") : "") + + (options.ack ? ", ack" : "") + + (options.from ? (", sources.name as 'from'") : "") + + (options.q ? ", q" : "") + " FROM " + db; + if (options.from) { + subQuery += " INNER JOIN sources ON sources.id=" + db + "._from"; + } + subWhere = ""; + if (options.id) { + subWhere += " " + db + ".id=" + options.id; + } + if (options.ignoreNull) { + //subWhere += (subWhere ? " AND" : "") + " val <> NULL"; + } + subWhere += (subWhere ? " AND" : "") + " " + db + ".ts >= " + options.end; + if (subWhere) subQuery += " WHERE " + subWhere; + subQuery += " ORDER BY " + db + ".ts ASC LIMIT 1"; + where += " UNION SELECT * from (" + subQuery + ") "; + } + + if (where) query += " WHERE " + where; + + query += " ORDER BY ts"; + + if (!options.start && options.count) { + query += " DESC LIMIT " + options.count; + } else { + query += " ASC"; + } + + query += ";"; + return query; +}; diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..6af37ff --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,83 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +let controllerDir; +let appName; + +/** + * returns application name + * + * The name of the application can be different and this function finds it out. + * + * @returns {string} + */ + function getAppName() { + const parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 2].split('.')[0]; +} + +/** + * looks for js-controller home folder + * + * @param {boolean} isInstall + * @returns {string} + */ +function getControllerDir(isInstall) { + // Find the js-controller location + const possibilities = [ + 'yunkong2.js-controller', + 'yunkong2.js-controller', + ]; + /** @type {string} */ + let controllerPath; + for (const pkg of possibilities) { + try { + const possiblePath = require.resolve(pkg); + if (fs.existsSync(possiblePath)) { + controllerPath = possiblePath; + break; + } + } catch (e) { /* not found */ } + } + if (controllerPath == null) { + if (!isInstall) { + console.log('Cannot find js-controller'); + process.exit(10); + } else { + process.exit(); + } + } + // we found the controller + return path.dirname(controllerPath); +} + +/** + * reads controller base settings + * + * @alias getConfig + * @returns {object} + */ + function getConfig() { + let configPath; + if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', appName + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else if (fs.existsSync( + configPath = path.join(controllerDir, 'conf', + appName.toLowerCase() + '.json') + )) { + return JSON.parse(fs.readFileSync(configPath, 'utf8')); + } else { + throw new Error('Cannot find ' + controllerDir + '/conf/' + appName + '.json'); + } +} +appName = getAppName(); +controllerDir = getControllerDir(typeof process !== 'undefined' && process.argv && process.argv.indexOf('--install') !== -1); +const adapter = require(path.join(controllerDir, 'lib/adapter.js')); + +exports.controllerDir = controllerDir; +exports.getConfig = getConfig; +exports.Adapter = adapter; +exports.appName = appName; diff --git a/main.js b/main.js new file mode 100644 index 0000000..6618857 --- /dev/null +++ b/main.js @@ -0,0 +1,2070 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +'use strict'; + +var utils = require(__dirname + '/lib/utils'); // Get common adapter utils +var SQL = require('sql-client'); +var commons = require(__dirname + '/lib/aggregate'); +var SQLFuncs = null; +var fs = require('fs'); + +var clients = { + postgresql: {name: 'PostgreSQLClient', multiRequests: true}, + mysql: {name: 'MySQLClient', multiRequests: true}, + sqlite: {name: 'SQLite3Client', multiRequests: false}, + mssql: {name: 'MSSQLClient', multiRequests: true} +}; + +var types = { + 'number': 0, + 'string': 1, + 'boolean': 2, + 'object': 1 +}; + +var dbNames = [ + 'ts_number', + 'ts_string', + 'ts_bool' +]; + +var storageTypes = [ + 'Number', + 'String', + 'Boolean' +]; + +var clientPool; +var sqlDPs = {}; +var from = {}; +var subscribeAll = false; +var tasks = []; +var tasksReadType = []; +var multiRequests = true; +var tasksStart = []; +var finished = false; +var connected = null; +var isFromRunning = {}; +var aliasMap = {}; + +var adapter = utils.Adapter('sql'); +adapter.on('objectChange', function (id, obj) { + var tmpState; + var now = new Date().getTime(); + var formerAliasId = aliasMap[id] ? aliasMap[id] : id; + if (obj && obj.common && + ( + // todo remove history sometime (2016.08) - Do not forget object selector in io-package.json + (obj.common.history && obj.common.history[adapter.namespace] && obj.common.history[adapter.namespace].enabled) || + (obj.common.custom && obj.common.custom[adapter.namespace] && obj.common.custom[adapter.namespace].enabled) + ) + ) { + var realId = id; + var checkForRemove = true; + if (obj.common.custom && obj.common.custom[adapter.namespace] && obj.common.custom[adapter.namespace].aliasId) { + if (obj.common.custom[adapter.namespace].aliasId !== id) { + aliasMap[id] = obj.common.custom[adapter.namespace].aliasId; + adapter.log.debug('Registered Alias: ' + id + ' --> ' + aliasMap[id]); + id = aliasMap[id]; + checkForRemove = false; + } + else { + adapter.log.warn('Ignoring Alias-ID because identical to ID for ' + id); + obj.common.custom[adapter.namespace].aliasId = ''; + } + } + if (checkForRemove && aliasMap[id]) { + adapter.log.debug('Removed Alias: ' + id + ' !-> ' + aliasMap[id]); + delete aliasMap[id]; + } + + if (!(sqlDPs[formerAliasId] && sqlDPs[formerAliasId][adapter.namespace]) && !subscribeAll) { + // un-subscribe + for (var _id in sqlDPs) { + if (sqlDPs.hasOwnProperty(sqlDPs[_id].realId)) { + adapter.unsubscribeForeignStates(sqlDPs[_id].realId); + } + } + subscribeAll = true; + adapter.subscribeForeignStates('*'); + } + var writeNull = !(sqlDPs[id] && sqlDPs[id][adapter.namespace]); + if (sqlDPs[formerAliasId] && sqlDPs[formerAliasId].relogTimeout) { + clearTimeout(sqlDPs[formerAliasId].relogTimeout); + } + + var storedIndex = null; + var storedType = null; + if (sqlDPs[id] && sqlDPs[id].index !== undefined) storedIndex = sqlDPs[id].index; + if (sqlDPs[id] && sqlDPs[id].dbtype !== undefined) storedType = sqlDPs[id].dbtype; + else if (sqlDPs[formerAliasId] && sqlDPs[formerAliasId].dbtype !== undefined) storedType = sqlDPs[formerAliasId].dbtype; + // todo remove history sometime (2016.08) + sqlDPs[id] = obj.common.custom || obj.common.history; + if (storedIndex !== null) sqlDPs[id].index = storedIndex; + if (storedType !== null) sqlDPs[id].dbtype = storedType; + + if (sqlDPs[id].index === undefined) { + getId(id, sqlDPs[id].dbtype, reInit); + } + else { + reInit(); + } + + function reInit() { + adapter.log.debug('remembered Index/Type ' + sqlDPs[id].index + ' / ' + sqlDPs[id].dbtype); + sqlDPs[id].realId = realId; + + if (sqlDPs[id][adapter.namespace].retention !== undefined && sqlDPs[id][adapter.namespace].retention !== null && sqlDPs[id][adapter.namespace].retention !== '') { + sqlDPs[id][adapter.namespace].retention = parseInt(sqlDPs[id][adapter.namespace].retention, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].retention = adapter.config.retention; + } + if (sqlDPs[id][adapter.namespace].debounce !== undefined && sqlDPs[id][adapter.namespace].debounce !== null && sqlDPs[id][adapter.namespace].debounce !== '') { + sqlDPs[id][adapter.namespace].debounce = parseInt(sqlDPs[id][adapter.namespace].debounce, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].debounce = adapter.config.debounce; + } + sqlDPs[id][adapter.namespace].changesOnly = sqlDPs[id][adapter.namespace].changesOnly === 'true' || sqlDPs[id][adapter.namespace].changesOnly === true; + if (sqlDPs[id][adapter.namespace].changesRelogInterval !== undefined && sqlDPs[id][adapter.namespace].changesRelogInterval !== null && sqlDPs[id][adapter.namespace].changesRelogInterval !== '') { + sqlDPs[id][adapter.namespace].changesRelogInterval = parseInt(sqlDPs[id][adapter.namespace].changesRelogInterval, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].changesRelogInterval = adapter.config.changesRelogInterval; + } + if (sqlDPs[id][adapter.namespace].changesRelogInterval > 0) { + sqlDPs[id].relogTimeout = setTimeout(reLogHelper, (sqlDPs[id][adapter.namespace].changesRelogInterval * 500 * Math.random()) + sqlDPs[id][adapter.namespace].changesRelogInterval * 500, id); + } + if (sqlDPs[id][adapter.namespace].changesMinDelta !== undefined && sqlDPs[id][adapter.namespace].changesMinDelta !== null && sqlDPs[id][adapter.namespace].changesMinDelta !== '') { + sqlDPs[id][adapter.namespace].changesMinDelta = parseFloat(sqlDPs[id][adapter.namespace].changesMinDelta.toString().replace(/,/g, '.')) || 0; + } else { + sqlDPs[id][adapter.namespace].changesMinDelta = adapter.config.changesMinDelta; + } + if (!sqlDPs[id][adapter.namespace].storageType) sqlDPs[id][adapter.namespace].storageType = false; + + // add one day if retention is too small + if (sqlDPs[id][adapter.namespace].retention && sqlDPs[id][adapter.namespace].retention <= 604800) { + sqlDPs[id][adapter.namespace].retention += 86400; + } + if (writeNull && adapter.config.writeNulls) { + writeNulls(id); + } + adapter.log.info('enabled logging of ' + id + ', Alias=' + (id !== realId)); + } + } + else { + if (aliasMap[id]) { + adapter.log.debug('Removed Alias: ' + id + ' !-> ' + aliasMap[id]); + delete aliasMap[id]; + } + id = formerAliasId; + if (sqlDPs[id]) { + adapter.log.info('disabled logging of ' + id); + if (sqlDPs[id].relogTimeout) clearTimeout(sqlDPs[id].relogTimeout); + if (sqlDPs[id].timeout) clearTimeout(sqlDPs[id].timeout); + + if (Object.assign) { + tmpState = Object.assign({}, sqlDPs[id].state); + } + else { + tmpState = JSON.parse(JSON.stringify(sqlDPs[id].state)); + } + var state = sqlDPs[id].state ? tmpState : null; + + if (sqlDPs[id].skipped) { + pushValueIntoDB(id, sqlDPs[id].skipped); + sqlDPs[id].skipped = null; + } + + var nullValue = {val: null, ts: now, lc: now, q: 0x40, from: 'system.adapter.' + adapter.namespace}; + if (sqlDPs[id][adapter.namespace] && adapter.config.writeNulls) { + if (sqlDPs[id][adapter.namespace].changesOnly && state && state.val !== null) { + (function (_id, _state, _nullValue) { + _state.ts = now; + _state.from = 'system.adapter.' + adapter.namespace; + nullValue.ts += 4; + nullValue.lc += 4; // because of MS SQL + adapter.log.debug('Write 1/2 "' + _state.val + '" _id: ' + _id); + pushValueIntoDB(_id, _state, function () { + // terminate values with null to indicate adapter stop. timestamp + 1# + adapter.log.debug('Write 2/2 "null" _id: ' + _id); + pushValueIntoDB(_id, _nullValue, function() { + delete sqlDPs[id][adapter.namespace]; + }); + }); + })(id, state, nullValue); + } + else { + // terminate values with null to indicate adapter stop. timestamp + 1 + adapter.log.debug('Write 0 NULL _id: ' + id); + pushValueIntoDB(id, nullValue, function() { + delete sqlDPs[id][adapter.namespace]; + }); + } + } + else { + delete sqlDPs[id][adapter.namespace]; + } + } + } +}); + +adapter.on('stateChange', function (id, state) { + id = aliasMap[id] ? aliasMap[id] : id; + pushHistory(id, state); +}); + +adapter.on('unload', function (callback) { + finish(callback); +}); + +adapter.on('ready', function () { + main(); +}); + +adapter.on('message', function (msg) { + processMessage(msg); +}); + +process.on('SIGINT', function () { + // close connection to DB + finish(); +}); +process.on('SIGTERM', function () { + // close connection to DB + finish(); +}); + +function setConnected(isConnected) { + if (connected !== isConnected) { + connected = isConnected; + adapter.setState('info.connection', connected, true); + } +} + +var _client = false; +function connect() { + if (!clientPool) { + setConnected(false); + + var params = { + server: adapter.config.host, // needed for MSSQL + host: adapter.config.host, // needed for PostgeSQL , MySQL + user: adapter.config.user, + password: adapter.config.password, + max_idle: (adapter.config.dbtype === 'sqlite') ? 1 : 2 + }; + if (adapter.config.port) { + params.port = adapter.config.port; + } + if (adapter.config.encrypt) { + params.options = { + encrypt: true // Use this if you're on Windows Azure + }; + } + + if (adapter.config.dbtype === 'postgres') { + params.database = 'postgres'; + } + + if (adapter.config.dbtype === 'sqlite') { + params = getSqlLiteDir(adapter.config.fileName); + } + else + // special solution for postgres. Connect first to Db "postgres", create new DB "yunkong2" and then connect to "yunkong2" DB. + if (_client !== true && adapter.config.dbtype === 'postgresql') { + if (adapter.config.dbtype === 'postgresql') { + params.database = 'postgres'; + } + + if (!adapter.config.dbtype) { + adapter.log.error('DB Type is not defined!'); + return; + } + if (!clients[adapter.config.dbtype] || !clients[adapter.config.dbtype].name) { + adapter.log.error('Unknown type "' + adapter.config.dbtype + '"'); + return; + } + if (!SQL[clients[adapter.config.dbtype].name]) { + adapter.log.error('SQL package "' + clients[adapter.config.dbtype].name + '" is not installed.'); + return; + } + + // connect first to DB postgres and create yunkong2 DB + _client = new SQL[clients[adapter.config.dbtype].name](params); + return _client.connect(function (err) { + if (err) { + adapter.log.error(err); + setTimeout(function () { + connect(); + }, 30000); + return; + } + _client.execute('CREATE DATABASE ' + adapter.config.dbname + ';', function (err /* , rows, fields */) { + _client.disconnect(); + if (err && err.code !== '42P04') { // if error not about yet exists + _client = false; + adapter.log.error(err); + setTimeout(function () { + connect(); + }, 30000); + } else { + _client = true; + setTimeout(function () { + connect(); + }, 100); + } + }); + }); + } + + if (adapter.config.dbtype === 'postgresql') { + params.database = adapter.config.dbname; + } + + try { + if (!clients[adapter.config.dbtype].name) { + adapter.log.error('Unknown SQL type selected: "' + adapter.config.dbtype + '"'); + } else if (!SQL[clients[adapter.config.dbtype].name + 'Pool']) { + adapter.log.error('Selected SQL DB was not installed properly: "' + adapter.config.dbtype + '". SQLite requires build tools on system. See README.md'); + } else { + clientPool = new SQL[clients[adapter.config.dbtype].name + 'Pool'](params); + return clientPool.open(function (err) { + if (err) { + adapter.log.error(err); + setTimeout(function () { + connect(); + }, 30000); + } else { + setTimeout(function () { + connect(); + }, 0); + } + }); + } + } catch (ex) { + if (ex.toString() === 'TypeError: undefined is not a function') { + adapter.log.error('Node.js DB driver for "' + adapter.config.dbtype + '" could not be installed.'); + } else { + adapter.log.error(ex.toString()); + } + setConnected(false); + return setTimeout(function () { + connect(); + }, 30000); + } + } + + allScripts(SQLFuncs.init(adapter.config.dbname), function (err) { + if (err) { + //adapter.log.error(err); + return setTimeout(function () { + connect(); + }, 30000); + } else { + adapter.log.info('Connected to ' + adapter.config.dbtype); + setConnected(true); + // read all DB IDs and all FROM ids + if (!multiRequests) { + getAllIds(function () { + getAllFroms(); + processStartValues(); + }); + } else { + getAllIds(function () { + processStartValues(); + }); + } + } + }); +} + +// Find sqlite data directory +function getSqlLiteDir(fileName) { + fileName = fileName || 'sqlite.db'; + fileName = fileName.replace(/\\/g, '/'); + if (fileName[0] === '/' || fileName.match(/^\w:\//)) { + return fileName; + } + else { + // normally /opt/yunkong2/node_modules/yunkong2.js-controller + // but can be /example/yunkong2.js-controller + var tools = require(utils.controllerDir + '/lib/tools'); + var config = tools.getConfigFileName().replace(/\\/g, '/'); + var parts = config.split('/'); + parts.pop(); + config = parts.join('/') + '/sqlite'; + // create sqlite directory + if (!fs.existsSync(config)) { + fs.mkdirSync(config); + } + + return config + '/' + fileName; + } +} + +function testConnection(msg) { + msg.message.config.port = parseInt(msg.message.config.port, 10) || 0; + var params = { + server: msg.message.config.host, + host: msg.message.config.host, + user: msg.message.config.user, + password: msg.message.config.password + }; + if (msg.message.config.port) { + params.port = msg.message.config.port; + } + + if (msg.message.config.dbtype === 'postgresql' && !SQL.PostgreSQLClient) { + var postgres = require(__dirname + '/lib/postgresql-client'); + for (var attr in postgres) { + if (!SQL[attr]) SQL[attr] = postgres[attr]; + } + } else + if (msg.message.config.dbtype === 'mssql' && !SQL.MSSQLClient) { + var mssql = require(__dirname + '/lib/mssql-client'); + for (var _attr in mssql) { + if (!SQL[_attr]) SQL[_attr] = mssql[_attr]; + } + } + + if (msg.message.config.dbtype === 'postgresql') { + params.database = 'postgres'; + } else if (msg.message.config.dbtype === 'sqlite') { + params = getSqlLiteDir(msg.message.config.fileName); + } + var timeout; + try { + var client = new SQL[clients[msg.message.config.dbtype].name](params); + timeout = setTimeout(function () { + timeout = null; + adapter.sendTo(msg.from, msg.command, {error: 'connect timeout'}, msg.callback); + }, 5000); + + client.connect(function (err) { + if (err) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + return adapter.sendTo(msg.from, msg.command, {error: err.toString()}, msg.callback); + } + client.execute("SELECT 2 + 3 AS x", function (err /* , rows, fields */) { + client.disconnect(); + if (timeout) { + clearTimeout(timeout); + timeout = null; + return adapter.sendTo(msg.from, msg.command, {error: err ? err.toString() : null}, msg.callback); + } + }); + }); + } catch (ex) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + if (ex.toString() === 'TypeError: undefined is not a function') { + return adapter.sendTo(msg.from, msg.command, {error: 'Node.js DB driver could not be installed.'}, msg.callback); + } else { + return adapter.sendTo(msg.from, msg.command, {error: ex.toString()}, msg.callback); + } + } +} + +function destroyDB(msg) { + try { + allScripts(SQLFuncs.destroy(adapter.config.dbname), function (err) { + if (err) { + adapter.log.error(err); + adapter.sendTo(msg.from, msg.command, {error: err.toString()}, msg.callback); + } else { + adapter.sendTo(msg.from, msg.command, {error: null}, msg.callback); + // restart adapter + setTimeout(function () { + adapter.getForeignObject('system.adapter.' + adapter.namespace, function (err, obj) { + if (!err) { + adapter.setForeignObject(obj._id, obj); + } else { + adapter.log.error('Cannot read object "system.adapter.' + adapter.namespace + '": ' + err); + adapter.stop(); + } + }); + }, 2000); + } + }); + } catch (ex) { + return adapter.sendTo(msg.from, msg.command, {error: ex.toString()}, msg.callback); + } +} + +function _userQuery(msg, callback) { + try { + adapter.log.debug(msg.message); + + clientPool.borrow(function (err, client) { + if (err) { + adapter.sendTo(msg.from, msg.command, {error: err.toString()}, msg.callback); + if (callback) callback(); + } else { + client.execute(msg.message, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + clientPool.return(client); + adapter.sendTo(msg.from, msg.command, {error: err ? err.toString() : null, result: rows}, msg.callback); + if (callback) callback(); + }); + } + }); + } catch (err) { + adapter.sendTo(msg.from, msg.command, {error: err.toString()}, msg.callback); + if (callback) callback(); + } +} +// execute custom query +function query(msg) { + if (!multiRequests) { + if (tasks.length > 100) { + adapter.log.error('Cannot queue new requests, because more than 100'); + adapter.sendTo(msg.from, msg.command, {error: 'Cannot queue new requests, because more than 100'}, msg.callback); + return; + } + tasks.push({operation: 'userQuery', msg: msg}); + if (tasks.length === 1) processTasks(); + } else { + _userQuery(msg); + } +} + +// one script +function oneScript(script, cb) { + try { + clientPool.borrow(function (err, client) { + if (err || !client) { + clientPool.close(); + clientPool = null; + adapter.log.error(err); + if (cb) cb(err); + return; + } + adapter.log.debug(script); + client.execute(script, function(err /* , rows, fields */) { + adapter.log.debug('Response: ' + JSON.stringify(err)); + if (err) { + // Database 'yunkong2' already exists. Choose a different database name. + if (err.number === 1801 || + //There is already an object named 'sources' in the database. + err.number === 2714) { + // do nothing + err = null; + } else + if (err.message && err.message.match(/^SQLITE_ERROR: table [\w_]+ already exists$/)) { + // do nothing + err = null; + } else + if (err.errno == 1007 || err.errno == 1050) { // if database exists or table exists + // do nothing + err = null; + } else + if (err.code === '42P04') {// if database exists or table exists + // do nothing + err = null; + } + else if (err.code === '42P07') { + var match = script.match(/CREATE\s+TABLE\s+(\w*)\s+\(/); + if (match) { + adapter.log.debug('OK. Table "' + match[1] + '" yet exists'); + err = null; + } else { + adapter.log.error(script); + adapter.log.error(err); + } + } else { + adapter.log.error(script); + adapter.log.error(err); + } + } + if (cb) cb(err); + clientPool.return(client); + }); + }); + } catch(ex) { + adapter.log.error(ex); + if (cb) cb(ex); + } + +} + +// all scripts +function allScripts(scripts, index, cb) { + if (typeof index === 'function') { + cb = index; + index = 0; + } + index = index || 0; + + if (scripts && index < scripts.length) { + oneScript(scripts[index], function (err) { + if (err) { + if (cb) cb(err); + } else { + allScripts(scripts, index + 1, cb); + } + }); + } else { + if (cb) cb(); + } +} + +function finish(callback) { + + function finishId(id) { + if (sqlDPs[id].relogTimeout) { + clearTimeout(sqlDPs[id].relogTimeout); + sqlDPs[id].relogTimeout = null; + } + if (sqlDPs[id].timeout) { + clearTimeout(sqlDPs[id].timeout); + sqlDPs[id].timeout = null; + } + var tmpState; + if (Object.assign) { + tmpState = Object.assign({}, sqlDPs[id].state); + } + else { + tmpState = JSON.parse(JSON.stringify(sqlDPs[id].state)); + } + var state = sqlDPs[id].state ? tmpState : null; + + if (sqlDPs[id].skipped) { + count++; + pushValueIntoDB(id, sqlDPs[id].skipped, function () { + if (!--count) { + if (clientPool) { + clientPool.close(); + clientPool = null; + } + if (typeof finished === 'object') { + setTimeout(function (cb) { + for (var f = 0; f < cb.length; f++) { + cb[f](); + } + }, 500, finished); + finished = true; + } + } + }); + sqlDPs[id].skipped = null; + } + + var nullValue = {val: null, ts: now, lc: now, q: 0x40, from: 'system.adapter.' + adapter.namespace}; + if (sqlDPs[id][adapter.namespace] && adapter.config.writeNulls) { + if (sqlDPs[id][adapter.namespace].changesOnly && state && state.val !== null) { + count++; + (function (_id, _state, _nullValue) { + _state.ts = now; + _state.from = 'system.adapter.' + adapter.namespace; + nullValue.ts += 4; + nullValue.lc += 4; // because of MS SQL + adapter.log.debug('Write 1/2 "' + _state.val + '" _id: ' + _id); + pushValueIntoDB(_id, _state, function () { + // terminate values with null to indicate adapter stop. timestamp + 1# + adapter.log.debug('Write 2/2 "null" _id: ' + _id); + pushValueIntoDB(_id, _nullValue, function () { + if (!--count) { + if (clientPool) { + clientPool.close(); + clientPool = null; + } + if (typeof finished === 'object') { + setTimeout(function (cb) { + for (var f = 0; f < cb.length; f++) { + cb[f](); + } + }, 500, finished); + finished = true; + } + } + }); + }); + })(id, state, nullValue); + } else { + // terminate values with null to indicate adapter stop. timestamp + 1 + count++; + adapter.log.debug('Write 0 NULL _id: ' + id); + pushValueIntoDB(id, nullValue, function () { + if (!--count) { + if (clientPool) { + clientPool.close(); + clientPool = null; + } + if (typeof finished === 'object') { + setTimeout(function (cb) { + for (var f = 0; f < cb.length; f++) { + cb[f](); + } + }, 500, finished); + finished = true; + } + } + }); + } + } + } + + adapter.unsubscribeForeignStates('*'); + var count = 0; + if (finished) { + if (callback) { + if (finished === true) { + callback(); + } else { + finished.push(callback); + } + } + return; + } + finished = [callback]; + var now = new Date().getTime(); + var dpcount = 0; + var delay = 0; + for (var id in sqlDPs) { + if (!sqlDPs.hasOwnProperty(id)) continue; + dpcount++; + delay += (dpcount%50 === 0) ? 1000: 0; + setTimeout(finishId, delay, id); + } + + if (!dpcount && callback) { + if (clientPool) { + clientPool.close(); + clientPool = null; + } + callback(); + } +} + +function processMessage(msg) { + if (msg.command === 'getHistory') { + getHistory(msg); + } + else if (msg.command === 'test') { + testConnection(msg); + } + else if (msg.command === 'destroy') { + destroyDB(msg); + } + else if (msg.command === 'query') { + query(msg); + } + else if (msg.command === 'storeState') { + storeState(msg); + } + else if (msg.command === 'getDpOverview') { + getDpOverview(msg); + } + else if (msg.command === 'enableHistory') { + enableHistory(msg); + } + else if (msg.command === 'disableHistory') { + disableHistory(msg); + } + else if (msg.command === 'getEnabledDPs') { + getEnabledDPs(msg); + } else if (msg.command === 'stopInstance') { + finish(function () { + if (msg.callback) { + adapter.sendTo(msg.from, msg.command, 'stopped', msg.callback); + setTimeout(function () { + process.exit(0); + }, 200); + } + }); + } +} + +function fixSelector(callback) { + // fix _design/custom object + adapter.getForeignObject('_design/custom', function (err, obj) { + if (!obj || obj.views.state.map.indexOf('common.history') === -1 || obj.views.state.map.indexOf('common.custom') === -1) { + obj = { + _id: '_design/custom', + language: 'javascript', + views: { + state: { + map: 'function(doc) { if (doc.type===\'state\' && (doc.common.custom || doc.common.history)) emit(doc._id, doc.common.custom || doc.common.history) }' + } + } + }; + adapter.setForeignObject('_design/custom', obj, function (err) { + if (callback) callback(err); + }); + } else { + if (callback) callback(err); + } + }); +} + +function processStartValues() { + if (tasksStart && tasksStart.length) { + var task = tasksStart.shift(); + if (sqlDPs[task.id][adapter.namespace].changesOnly) { + adapter.getForeignState(sqlDPs[task.id].realId, function (err, state) { + var now = task.now || new Date().getTime(); + pushHistory(task.id, { + val: null, + ts: state ? now - 4 : now, // 4 is because of MS SQL + lc: state ? now - 4 : now, // 4 is because of MS SQL + ack: true, + q: 0x40, + from: 'system.adapter.' + adapter.namespace + }); + if (state) { + state.ts = now; + state.lc = now; + state.from = 'system.adapter.' + adapter.namespace; + pushHistory(task.id, state); + } + setTimeout(processStartValues, 0); + }); + } + else { + pushHistory(task.id, { + val: null, + ts: task.now || new Date().getTime(), + lc: task.now || new Date().getTime(), + ack: true, + q: 0x40, + from: 'system.adapter.' + adapter.namespace + }); + setTimeout(processStartValues, 0); + } + if (sqlDPs[task.id][adapter.namespace] && sqlDPs[task.id][adapter.namespace].changesRelogInterval > 0) { + if (sqlDPs[task.id].relogTimeout) clearTimeout(sqlDPs[task.id].relogTimeout); + sqlDPs[task.id].relogTimeout = setTimeout(reLogHelper, (sqlDPs[task.id][adapter.namespace].changesRelogInterval * 500 * Math.random()) + sqlDPs[task.id][adapter.namespace].changesRelogInterval * 500, task.id); + } + } +} + +function writeNulls(id, now) { + if (!id) { + now = new Date().getTime(); + for (var _id in sqlDPs) { + if (sqlDPs.hasOwnProperty(_id) && sqlDPs[_id] && sqlDPs[_id][adapter.namespace]) { + writeNulls(_id, now); + } + } + } else { + now = now || new Date().getTime(); + tasksStart.push({id: id, now: now}); + if (tasksStart.length === 1 && connected) { + processStartValues(); + } + } +} + +function main() { + setConnected(false); + + adapter.config.dbname = adapter.config.dbname || 'yunkong2'; + + if (adapter.config.writeNulls === undefined) adapter.config.writeNulls = true; + + adapter.config.retention = parseInt(adapter.config.retention, 10) || 0; + adapter.config.debounce = parseInt(adapter.config.debounce, 10) || 0; + adapter.config.requestInterval = (adapter.config.requestInterval === undefined || adapter.config.requestInterval === null || adapter.config.requestInterval === '') ? 0 : parseInt(adapter.config.requestInterval, 10) || 0; + + if (adapter.config.changesRelogInterval !== null && adapter.config.changesRelogInterval !== undefined) { + adapter.config.changesRelogInterval = parseInt(adapter.config.changesRelogInterval, 10); + } else { + adapter.config.changesRelogInterval = 0; + } + + if (!clients[adapter.config.dbtype]) { + adapter.log.error('Unknown DB type: ' + adapter.config.dbtype); + adapter.stop(); + } + if (adapter.config.multiRequests !== undefined && adapter.config.dbtype !== 'SQLite3Client' && adapter.config.dbtype !== 'sqlite') { + clients[adapter.config.dbtype].multiRequests = adapter.config.multiRequests; + } + + if (adapter.config.changesMinDelta !== null && adapter.config.changesMinDelta !== undefined) { + adapter.config.changesMinDelta = parseFloat(adapter.config.changesMinDelta.toString().replace(/,/g, '.')); + } else { + adapter.config.changesMinDelta = 0; + } + + multiRequests = clients[adapter.config.dbtype].multiRequests; + if (!multiRequests) adapter.config.writeNulls = false; + + adapter.config.port = parseInt(adapter.config.port, 10) || 0; + if (adapter.config.round !== null && adapter.config.round !== undefined) { + adapter.config.round = Math.pow(10, parseInt(adapter.config.round, 10)); + } else { + adapter.config.round = null; + } + if (adapter.config.dbtype === 'postgresql' && !SQL.PostgreSQLClient) { + var postgres = require(__dirname + '/lib/postgresql-client'); + for (var attr in postgres) { + if (postgres.hasOwnProperty(attr) && !SQL[attr]) { + SQL[attr] = postgres[attr]; + } + } + } else + if (adapter.config.dbtype === 'mssql' && !SQL.MSSQLClient) { + var mssql = require(__dirname + '/lib/mssql-client'); + for (var attr_ in mssql) { + if (mssql.hasOwnProperty(attr_) && !SQL[attr_]) { + SQL[attr_] = mssql[attr_]; + } + } + } + SQLFuncs = require(__dirname + '/lib/' + adapter.config.dbtype); + + fixSelector(function () { + // read all custom settings + adapter.objects.getObjectView('custom', 'state', {}, function (err, doc) { + var count = 0; + if (doc && doc.rows) { + for (var i = 0, l = doc.rows.length; i < l; i++) { + if (doc.rows[i].value) { + var id = doc.rows[i].id; + var realId = id; + if (doc.rows[i].value[adapter.namespace] && doc.rows[i].value[adapter.namespace].aliasId) { + aliasMap[id] = doc.rows[i].value[adapter.namespace].aliasId; + adapter.log.debug('Found Alias: ' + id + ' --> ' + aliasMap[id]); + id = aliasMap[id]; + } + sqlDPs[id] = doc.rows[i].value; + + if (!sqlDPs[id][adapter.namespace]) { + delete sqlDPs[id]; + } else { + count++; + adapter.log.info('enabled logging of ' + id + ', Alias=' + (id !== realId) + ', ' + count + ' points now activated'); + if (sqlDPs[id][adapter.namespace].retention !== undefined && sqlDPs[id][adapter.namespace].retention !== null && sqlDPs[id][adapter.namespace].retention !== '') { + sqlDPs[id][adapter.namespace].retention = parseInt(sqlDPs[id][adapter.namespace].retention, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].retention = adapter.config.retention; + } + + if (sqlDPs[id][adapter.namespace].debounce !== undefined && sqlDPs[id][adapter.namespace].debounce !== null && sqlDPs[id][adapter.namespace].debounce !== '') { + sqlDPs[id][adapter.namespace].debounce = parseInt(sqlDPs[id][adapter.namespace].debounce, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].debounce = adapter.config.debounce; + } + sqlDPs[id][adapter.namespace].changesOnly = sqlDPs[id][adapter.namespace].changesOnly === 'true' || sqlDPs[id][adapter.namespace].changesOnly === true; + + if (sqlDPs[id][adapter.namespace].changesRelogInterval !== undefined && sqlDPs[id][adapter.namespace].changesRelogInterval !== null && sqlDPs[id][adapter.namespace].changesRelogInterval !== '') { + sqlDPs[id][adapter.namespace].changesRelogInterval = parseInt(sqlDPs[id][adapter.namespace].changesRelogInterval, 10) || 0; + } else { + sqlDPs[id][adapter.namespace].changesRelogInterval = adapter.config.changesRelogInterval; + } + if (sqlDPs[id][adapter.namespace].changesRelogInterval > 0) { + sqlDPs[id].relogTimeout = setTimeout(reLogHelper, (sqlDPs[id][adapter.namespace].changesRelogInterval * 500 * Math.random()) + sqlDPs[id][adapter.namespace].changesRelogInterval * 500, id); + } + if (sqlDPs[id][adapter.namespace].changesMinDelta !== undefined && sqlDPs[id][adapter.namespace].changesMinDelta !== null && sqlDPs[id][adapter.namespace].changesMinDelta !== '') { + sqlDPs[id][adapter.namespace].changesMinDelta = parseFloat(sqlDPs[id][adapter.namespace].changesMinDelta) || 0; + } else { + sqlDPs[id][adapter.namespace].changesMinDelta = adapter.config.changesMinDelta; + } + if (!sqlDPs[id][adapter.namespace].storageType) sqlDPs[id][adapter.namespace].storageType = false; + + // add one day if retention is too small + if (sqlDPs[id][adapter.namespace].retention && sqlDPs[id][adapter.namespace].retention <= 604800) { + sqlDPs[id][adapter.namespace].retention += 86400; + } + if (sqlDPs[id][adapter.namespace] && sqlDPs[id][adapter.namespace].changesRelogInterval > 0) { + if (sqlDPs[id].relogTimeout) clearTimeout(sqlDPs[id].relogTimeout); + sqlDPs[id].relogTimeout = setTimeout(reLogHelper, (sqlDPs[id][adapter.namespace].changesRelogInterval * 500 * Math.random()) + sqlDPs[id][adapter.namespace].changesRelogInterval * 500, id); + } + + sqlDPs[id].realId = realId; + } + } + } + } + + if (adapter.config.writeNulls) writeNulls(); + + if (count < 20) { + for (var _id in sqlDPs) { + if (sqlDPs.hasOwnProperty(_id)) { + adapter.subscribeForeignStates(sqlDPs[_id].realId); + } + } + } else { + subscribeAll = true; + adapter.subscribeForeignStates('*'); + } + }); + }); + + adapter.subscribeForeignObjects('*'); + + if (adapter.config.dbtype === 'sqlite' || adapter.config.host) { + connect(); + } +} + +function pushHistory(id, state, timerRelog) { + if (timerRelog === undefined) timerRelog = false; + // Push into DB + if (sqlDPs[id]) { + var settings = sqlDPs[id][adapter.namespace]; + + if (!settings || !state) return; + + adapter.log.debug('new value received for ' + id + ', new-value=' + state.val + ', ts=' + state.ts + ', relog=' + timerRelog); + if (state.val !== null && typeof state.val === 'string' && settings.storageType !== 'String') { + var f = parseFloat(state.val); + if (f == state.val) { + state.val = f; + } + } + + if (sqlDPs[id].state && settings.changesOnly && !timerRelog) { + if (settings.changesRelogInterval === 0) { + if (state.ts !== state.lc) { + sqlDPs[id].skipped = state; // remember new timestamp + adapter.log.debug('value not changed ' + id + ', last-value=' + sqlDPs[id].state.val + ', new-value=' + state.val + ', ts=' + state.ts); + return; + } + } + else if (sqlDPs[id].lastLogTime) { + if ((state.ts !== state.lc) && (Math.abs(sqlDPs[id].lastLogTime - state.ts) < settings.changesRelogInterval * 1000)) { + sqlDPs[id].skipped = state; // remember new timestamp + adapter.log.debug('value not changed relog' + id + ', last-value=' + sqlDPs[id].state.val + ', new-value=' + state.val + ', ts=' + state.ts); + return; + } + if (state.ts !== state.lc) { + adapter.log.debug('value-changed-relog ' + id + ', value=' + state.val + ', lastLogTime=' + sqlDPs[id].lastLogTime + ', ts=' + state.ts); + } + } + if (sqlDPs[id].state.val !== null && (settings.changesMinDelta !== 0) && (typeof state.val === 'number') && (Math.abs(sqlDPs[id].state.val - state.val) < settings.changesMinDelta)) { + adapter.log.debug('Min-Delta not reached ' + id + ', last-value=' + sqlDPs[id].state.val + ', new-value=' + state.val + ', ts=' + state.ts); + sqlDPs[id].skipped = state; // remember new timestamp + return; + } + else if (typeof state.val === 'number') { + adapter.log.debug('Min-Delta reached ' + id + ', last-value=' + sqlDPs[id].state.val + ', new-value=' + state.val + ', ts=' + state.ts); + } + else { + adapter.log.debug('Min-Delta ignored because no number ' + id + ', last-value=' + sqlDPs[id].state.val + ', new-value=' + state.val + ', ts=' + state.ts); + } + } + + if (sqlDPs[id].relogTimeout) { + clearTimeout(sqlDPs[id].relogTimeout); + sqlDPs[id].relogTimeout = null; + } + if (settings.changesRelogInterval > 0) { + sqlDPs[id].relogTimeout = setTimeout(reLogHelper, settings.changesRelogInterval * 1000, id); + } + + var ignoreDebonce = false; + if (timerRelog) { + state.ts = new Date().getTime(); + adapter.log.debug('timed-relog ' + id + ', value=' + state.val + ', lastLogTime=' + sqlDPs[id].lastLogTime + ', ts=' + state.ts); + ignoreDebonce = true; + } else { + if (settings.changesOnly && sqlDPs[id].skipped) { + sqlDPs[id].state = sqlDPs[id].skipped; + pushHelper(id); + } + if (sqlDPs[id].state && ((sqlDPs[id].state.val === null && state.val !== null) || (sqlDPs[id].state.val !== null && state.val === null))) { + ignoreDebonce = true; + } + else if (!sqlDPs[id].state && state.val === null) { + ignoreDebonce = true; + } + + // only store state if really changed + sqlDPs[id].state = state; + } + sqlDPs[id].lastLogTime = state.ts; + sqlDPs[id].skipped = null; + + if (settings.debounce && !ignoreDebonce) { + // Discard changes in debounce time to store last stable value + if (sqlDPs[id].timeout) clearTimeout(sqlDPs[id].timeout); + sqlDPs[id].timeout = setTimeout(pushHelper, settings.debounce, id); + } else { + pushHelper(id); + } + } +} + +function reLogHelper(_id) { + if (!sqlDPs[_id]) { + adapter.log.info('non-existing id ' + _id); + return; + } + sqlDPs[_id].relogTimeout = null; + if (sqlDPs[_id].skipped) { + sqlDPs[_id].state = sqlDPs[_id].skipped; + sqlDPs[_id].state.from = 'system.adapter.' + adapter.namespace; + sqlDPs[_id].skipped = null; + pushHistory(_id, sqlDPs[_id].state, true); + } + else { + adapter.getForeignState(sqlDPs[_id].realId, function (err, state) { + if (err) { + adapter.log.info('init timed Relog: can not get State for ' + _id + ' : ' + err); + } + else if (!state) { + adapter.log.info('init timed Relog: disable relog because state not set so far for ' + _id + ': ' + JSON.stringify(state)); + } + else { + adapter.log.debug('init timed Relog: getState ' + _id + ': Value=' + state.val + ', ack=' + state.ack + ', ts=' + state.ts + ', lc=' + state.lc); + sqlDPs[_id].state = state; + pushHistory(_id, sqlDPs[_id].state, true); + } + }); + } +} + +function pushHelper(_id) { + if (!sqlDPs[_id] || !sqlDPs[_id].state) return; + var _settings = sqlDPs[_id][adapter.namespace]; + // if it was not deleted in this time + if (_settings) { + sqlDPs[_id].timeout = null; + + if (sqlDPs[_id].state.val !== null) { + if (typeof sqlDPs[_id].state.val === 'object') { + sqlDPs[_id].state.val = JSON.stringify(sqlDPs[_id].state.val); + } + + adapter.log.debug('Datatype ' + _id + ': Currently: ' + typeof sqlDPs[_id].state.val + ', StorageType: ' + _settings.storageType); + if (typeof sqlDPs[_id].state.val === 'string' && _settings.storageType !== 'String') { + adapter.log.debug('Do Automatic Datatype conversion for ' + _id); + var f = parseFloat(sqlDPs[_id].state.val); + if (f == sqlDPs[_id].state.val) { + sqlDPs[_id].state.val = f; + } else if (sqlDPs[_id].state.val === 'true') { + sqlDPs[_id].state.val = true; + } else if (sqlDPs[_id].state.val === 'false') { + sqlDPs[_id].state.val = false; + } + } + if (_settings.storageType === 'String' && typeof sqlDPs[_id].state.val !== 'string') { + sqlDPs[_id].state.val = sqlDPs[_id].state.val.toString(); + } + else if (_settings.storageType === 'Number' && typeof sqlDPs[_id].state.val !== 'number') { + if (typeof sqlDPs[_id].state.val === 'boolean') { + sqlDPs[_id].state.val = sqlDPs[_id].state.val?1:0; + } + else { + adapter.log.info('Do not store value "' + sqlDPs[_id].state.val + '" for ' + _id + ' because no number'); + return; + } + } + else if (_settings.storageType === 'Boolean' && typeof sqlDPs[_id].state.val !== 'boolean') { + sqlDPs[_id].state.val = !!sqlDPs[_id].state.val; + } + } + else { + adapter.log.debug('Datatype ' + _id + ': Currently: null'); + } + pushValueIntoDB(_id, sqlDPs[_id].state); + } +} + +function getAllIds(cb) { + var query = SQLFuncs.getIdSelect(adapter.config.dbname); + adapter.log.debug(query); + clientPool.borrow(function (err, client) { + if (err) { + if (cb) cb(err); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err); + clientPool.return(client); + return; + } + if (rows.length) { + var id; + for (var r = 0; r < rows.length; r++) { + id = rows[r].name; + sqlDPs[id] = sqlDPs[id] || {}; + sqlDPs[id].index = rows[r].id; + if (rows[r].type !== null) sqlDPs[id].dbtype = rows[r].type; + } + + if (cb) cb(); + clientPool.return(client); + } + }); + }); +} + +function getAllFroms(cb) { + var query = SQLFuncs.getFromSelect(adapter.config.dbname); + adapter.log.debug(query); + clientPool.borrow(function (err, client) { + if (err) { + if (cb) cb(err); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err); + clientPool.return(client); + return; + } + if (rows.length) { + for (var r = 0; r < rows.length; r++) { + from[rows[r].name] = rows[r].id; + } + + if (cb) cb(); + clientPool.return(client); + } + }); + }); +} + +function _checkRetention(query, cb) { + adapter.log.debug(query); + + clientPool.borrow(function (err, client) { + if (err) { + adapter.log.error(err); + if (cb) cb(); + return; + } + client.execute(query, function (err /* , rows, fields */ ) { + if (err) adapter.log.error('Cannot delete ' + query + ': ' + err); + clientPool.return(client); + if (cb) cb(); + }); + }); +} + +function checkRetention(id) { + if (sqlDPs[id] && sqlDPs[id][adapter.namespace] && sqlDPs[id][adapter.namespace].retention) { + var d = new Date(); + var dt = d.getTime(); + // check every 6 hours + if (!sqlDPs[id].lastCheck || dt - sqlDPs[id].lastCheck >= 21600000/* 6 hours */) { + sqlDPs[id].lastCheck = dt; + var query = SQLFuncs.retention(adapter.config.dbname, sqlDPs[id].index, dbNames[sqlDPs[id].type], sqlDPs[id][adapter.namespace].retention); + + if (!multiRequests) { + if (tasks.length > 100) { + adapter.log.error('Cannot queue new requests, because more than 100'); + return; + } + tasks.push({operation: 'delete', query: query}); + if (tasks.length === 1) processTasks(); + } else { + _checkRetention(query); + } + } + } +} + +function _insertValueIntoDB(query, id, cb) { + adapter.log.debug(query); + + clientPool.borrow(function (err, client) { + if (err) { + adapter.log.error(err); + if (cb) cb(); + return; + } + client.execute(query, function (err /* , rows, fields */) { + if (err) adapter.log.error('Cannot insert ' + query + ': ' + err); + clientPool.return(client); + checkRetention(id); + if (cb) cb(); + }); + }); +} + +function processReadTypes() { + if (tasksReadType && tasksReadType.length) { + var task = tasksReadType.shift(); + adapter.log.debug('Type set in Def for ' + task.id + ': ' + sqlDPs[task.id][adapter.namespace].storageType); + if (sqlDPs[task.id][adapter.namespace].storageType) { + sqlDPs[task.id].type = types[sqlDPs[task.id][adapter.namespace].storageType.toLowerCase()]; + adapter.log.debug('Type (from Def) for ' + task.id + ': ' + sqlDPs[task.id].type); + processVerifyTypes(task); + } + else if (sqlDPs[task.id].dbtype !== undefined) { + sqlDPs[task.id].type = sqlDPs[task.id].dbtype; + sqlDPs[task.id][adapter.namespace].storageType = storageTypes[sqlDPs[task.id].type]; + adapter.log.debug('Type (from DB-Type) for ' + task.id + ': ' + sqlDPs[task.id].type); + processVerifyTypes(task); + } + else { + adapter.getForeignObject(sqlDPs[task.id].realId, function (err, obj) { + if (err) { + adapter.log.warn('Error while get Object for Def: ' + err); + } + if (obj && obj.common && obj.common.type) { + adapter.log.debug(obj.common.type.toLowerCase() + ' / ' + types[obj.common.type.toLowerCase()] + ' / ' + JSON.stringify(obj.common)); + sqlDPs[task.id].type = types[obj.common.type.toLowerCase()]; + sqlDPs[task.id][adapter.namespace].storageType = storageTypes[sqlDPs[task.id].type]; + adapter.log.debug('Type (from Obj) for ' + task.id + ': ' + sqlDPs[task.id].type); + processVerifyTypes(task); + } + if (sqlDPs[task.id].type === undefined) { + adapter.getForeignState(sqlDPs[task.id].realId, function (err, state) { + if (err) { + adapter.log.warn('Store data for ' + task.id + ' as string because no other valid type found (' + obj.common.type.toLowerCase() + ' and no state)'); + sqlDPs[task.id].type = 1; // string + } + else if (state && state.val !== null && state.val !== undefined && types[typeof state.val] !== undefined) { + sqlDPs[task.id].type = types[typeof state.val]; + sqlDPs[task.id][adapter.namespace].storageType = storageTypes[sqlDPs[task.id].type]; + } + else { + adapter.log.warn('Store data for ' + task.id + ' as string because no other valid type found (' + (state?(typeof state.val):'state not existing') + ')'); + sqlDPs[task.id].type = 1; // string + } + adapter.log.debug('Type (from State) for ' + task.id + ': ' + sqlDPs[task.id].type); + processVerifyTypes(task); + }); + } + }); + } + } +} + +function processVerifyTypes(task) { + if (sqlDPs[task.id].index !== undefined && sqlDPs[task.id].type !== undefined && sqlDPs[task.id].type !== sqlDPs[task.id].dbtype) { + sqlDPs[task.id].dbtype = sqlDPs[task.id].type; + + var query = SQLFuncs.getIdUpdate(adapter.config.dbname, sqlDPs[task.id].index, sqlDPs[task.id].type); + adapter.log.debug(query); + clientPool.borrow(function (err, client) { + if (err) { + processVerifyTypes(task); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (err) { + adapter.log.error('error updating history config for ' + task.id + ' to pin datatype: ' + query + ': ' + err); + } + else { + adapter.log.info('changed history configuration to pin detected datatype for ' + task.id); + } + clientPool.return(client); + processVerifyTypes(task); + }); + }); + + return; + } + + pushValueIntoDB(task.id, task.state); + + setTimeout(processReadTypes, 50); +} + +function pushValueIntoDB(id, state, cb) { + if (!sqlDPs[id]) return; + if (!clientPool) { + adapter.log.warn('No connection to SQL-DB'); + if (cb) cb('No connection to SQL-DB'); + return; + } + var type; + + if (sqlDPs[id].type !== undefined) { + type = sqlDPs[id].type; + } + else { + // read type from DB + tasksReadType.push({id: id, state: state}); + if (tasksReadType.length === 1) { + processReadTypes(); + } + + return; + } + + if (type === undefined) { // Can not happen anymore + if (state.val === null) { + adapter.log.warn('Ignore null value for ' + id + ' because no type defined till now.'); + if (cb) cb('Ignore null value for ' + id + ' because no type defined till now.'); + return; + } + adapter.log.warn('Cannot store values of type "' + typeof state.val + '" for ' + id); + if (cb) cb('Cannot store values of type "' + typeof state.val + '" ' + id); + return; + } + var tmpState; + // get id if state + if (sqlDPs[id].index === undefined) { + sqlDPs[id].isRunning = sqlDPs[id].isRunning || []; + if (Object.assign) { + tmpState = Object.assign({}, state); + } + else { + tmpState = JSON.parse(JSON.stringify(state)); + } + sqlDPs[id].isRunning.push({id: id, state: tmpState, cb: cb}); + + if (sqlDPs[id].isRunning.length === 1) { + // read or create in DB + return getId(id, type, function (err, _id) { + if (err) { + adapter.log.warn('Cannot get index of "' + _id + '": ' + err); + if (sqlDPs[_id].isRunning) { + for (var t = 0; t < sqlDPs[_id].isRunning.length; t++) { + if (sqlDPs[_id].isRunning[t].cb) sqlDPs[_id].isRunning[t].cb('Cannot get index of "' + sqlDPs[_id].isRunning[t].id + '": ' + err); + } + } + } else { + if (sqlDPs[_id].isRunning) { + for (var k = 0; k < sqlDPs[_id].isRunning.length; k++) { + pushValueIntoDB(sqlDPs[_id].isRunning[k].id, sqlDPs[_id].isRunning[k].state, sqlDPs[_id].isRunning[k].cb); + } + } + } + sqlDPs[_id].isRunning = null; + }); + } + return; + } + + // get from + if (state.from && !from[state.from]) { + isFromRunning[state.from] = isFromRunning[state.from] || []; + if (Object.assign) { + tmpState = Object.assign({}, state); + } + else { + tmpState = JSON.parse(JSON.stringify(state)); + } + isFromRunning[state.from].push({id: id, state: tmpState, cb: cb}); + + if (isFromRunning[state.from].length === 1) { + // read or create in DB + return getFrom(state.from, function (err, from) { + if (err) { + adapter.log.warn('Cannot get "from" for "' + from + '": ' + err); + if (isFromRunning[from]) { + for (var t = 0; t < isFromRunning[from].length; t++) { + if (isFromRunning[from][t].cb) isFromRunning[from][t].cb('Cannot get "from" for "' + from + '": ' + err); + } + } + } else { + if (isFromRunning[from]) { + for (var k = 0; k < isFromRunning[from].length; k++) { + pushValueIntoDB(isFromRunning[from][k].id, isFromRunning[from][k].state, isFromRunning[from][k].cb); + } + } + } + isFromRunning[from] = null; + }); + } + return; + } + // if greater than 2000.01.01 00:00:00 + if (state.ts > 946681200000) { + state.ts = parseInt(state.ts, 10); + } else { + state.ts = parseInt(state.ts, 10) * 1000 + (parseInt(state.ms, 10) || 0); + } + + try { + if (state.val !== null && typeof state.val === 'object') { + state.val = JSON.stringify(state.val); + } + } catch (err) { + adapter.log.error('Cannot convert the object value "' + id + '"'); + if (cb) cb('Cannot convert the object value "' + id + '"'); + return; + } + + // increase timestamp if last is the same + if (sqlDPs[id].ts && state.ts === sqlDPs[id].ts) { + state.ts++; + } + // remember last timestamp + sqlDPs[id].ts = state.ts; + + var query = SQLFuncs.insert(adapter.config.dbname, sqlDPs[id].index, state, from[state.from] || 0, dbNames[type]); + if (!multiRequests) { + if (tasks.length > 100) { + adapter.log.error('Cannot queue new requests, because more than 100'); + if (cb) cb('Cannot queue new requests, because more than 100'); + return; + } + + tasks.push({operation: 'insert', query: query, id: id, callback: cb}); + if (tasks.length === 1) { + processTasks(); + } + } else { + _insertValueIntoDB(query, id, cb); + } +} + +var lockTasks = false; +function processTasks() { + if (lockTasks) { + adapter.log.debug('Tries to execute task, but last one not finished!'); + return; + } + lockTasks = true; + if (tasks.length) { + if (tasks[0].operation === 'insert') { + _insertValueIntoDB(tasks[0].query, tasks[0].id, function () { + if (tasks[0].callback) tasks[0].callback(); + tasks.shift(); + lockTasks = false; + if (tasks.length) setTimeout(processTasks, adapter.config.requestInterval); + }); + } + else if (tasks[0].operation === 'select') { + _getDataFromDB(tasks[0].query, tasks[0].options, function (err, rows) { + if (tasks[0].callback) tasks[0].callback(err, rows); + tasks.shift(); + lockTasks = false; + if (tasks.length) setTimeout(processTasks, adapter.config.requestInterval); + }); + } + else if (tasks[0].operation === 'userQuery') { + _userQuery(tasks[0].msg, function () { + if (tasks[0].callback) tasks[0].callback(); + tasks.shift(); + lockTasks = false; + if (tasks.length) setTimeout(processTasks, adapter.config.requestInterval); + }); + } + else if (tasks[0].operation === 'delete') { + _checkRetention(tasks[0].query, function () { + if (tasks[0].callback) tasks[0].callback(); + tasks.shift(); + lockTasks = false; + if (tasks.length) setTimeout(processTasks, adapter.config.requestInterval); + }); + } else { + adapter.log.error('unknown task: ' + tasks[0].operation); + if (tasks[0].callback) tasks[0].callback(); + tasks.shift(); + lockTasks = false; + if (tasks.length) setTimeout(processTasks, adapter.config.requestInterval); + } + } +} +// my be it is required to cache all the data in memory +function getId(id, type, cb) { + var query = SQLFuncs.getIdSelect(adapter.config.dbname, id); + adapter.log.debug(query); + + if (!clientPool) { + if (cb) cb('No connection', id); + return; + } + + clientPool.borrow(function (err, client) { + if (err) { + if (cb) cb(err, id); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err, id); + clientPool.return(client); + return; + } + if (!rows.length) { + if (type !== null) { + // insert + query = SQLFuncs.getIdInsert(adapter.config.dbname, id, type); + adapter.log.debug(query); + client.execute(query, function (err /* , rows, fields */) { + if (err) { + adapter.log.error('Cannot insert ' + query + ': ' + err); + if (cb) cb(err, id); + clientPool.return(client); + return; + } + query = SQLFuncs.getIdSelect(adapter.config.dbname,id); + adapter.log.debug(query); + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) { + rows = rows.rows; + } + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err, id); + clientPool.return(client); + return; + } + sqlDPs[id].index = rows[0].id; + sqlDPs[id].type = rows[0].type; + + if (cb) cb(null, id); + clientPool.return(client); + }); + }); + } else { + if (cb) cb('id not found', id); + clientPool.return(client); + } + } else { + sqlDPs[id].index = rows[0].id; + sqlDPs[id].type = rows[0].type; + + if (cb) cb(null, id); + clientPool.return(client); + } + }); + }); +} +// my be it is required to cache all the data in memory +function getFrom(_from, cb) { + // var sources = (adapter.config.dbtype !== 'postgresql' ? (adapter.config.dbname + '.') : '') + 'sources'; + var query = SQLFuncs.getFromSelect(adapter.config.dbname, _from); + adapter.log.debug(query); + + if (!clientPool) { + if (cb) cb('No connection', _from); + return; + } + + clientPool.borrow(function (err, client) { + if (err) { + if (cb) cb(err, _from); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err, _from); + clientPool.return(client); + return; + } + if (!rows.length) { + // insert + query = SQLFuncs.getFromInsert(adapter.config.dbname, _from); + adapter.log.debug(query); + client.execute(query, function (err /* , rows, fields */) { + if (err) { + adapter.log.error('Cannot insert ' + query + ': ' + err); + if (cb) cb(err, _from); + clientPool.return(client); + return; + } + + query = SQLFuncs.getFromSelect(adapter.config.dbname, _from); + adapter.log.debug(query); + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + if (cb) cb(err, _from); + clientPool.return(client); + return; + } + from[_from] = rows[0].id; + + if (cb) cb(null, _from); + clientPool.return(client); + }); + }); + } else { + from[_from] = rows[0].id; + + if (cb) cb(null, _from); + clientPool.return(client); + } + }); + }); +} + +function sortByTs(a, b) { + var aTs = a.ts; + var bTs = b.ts; + return ((aTs < bTs) ? -1 : ((aTs > bTs) ? 1 : 0)); +} + +function _getDataFromDB(query, options, callback) { + adapter.log.debug(query); + + clientPool.borrow(function (err, client) { + if (err) { + if (callback) callback(err); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + // because descending + if (!err && rows && !options.start && options.count) { + rows.sort(sortByTs); + } + + if (rows) { + var isNumber = null; + for (var c = 0; c < rows.length; c++) { + if (isNumber === null && rows[c].val !== null) { + isNumber = (parseFloat(rows[c].val) == rows[c].val); + } + if (typeof rows[c].ts === 'string') { + rows[c].ts = parseInt(rows[c].ts, 10); + } + + // if less than 2000.01.01 00:00:00 + if (rows[c].ts < 946681200000) { + rows[c].ts *= 1000; + } + + if (adapter.common.loglevel === 'debug') { + rows[c].date = new Date(parseInt(rows[c].ts, 10)); + } + if (options.ack) { + rows[c].ack = !!rows[c].ack; + } + if (isNumber && adapter.config.round && rows[c].val !== null) { + rows[c].val = Math.round(rows[c].val * adapter.config.round) / adapter.config.round; + } + if (sqlDPs[options.index].type === 2) { + rows[c].val = !!rows[c].val; + } + } + } + + clientPool.return(client); + if (callback) callback(err, rows); + }); + }); +} + +function getDataFromDB(db, options, callback) { + var query = SQLFuncs.getHistory(adapter.config.dbname, db, options); + adapter.log.debug(query); + if (!multiRequests) { + if (tasks.length > 100) { + adapter.log.error('Cannot queue new requests, because more than 100'); + if (callback) callback('Cannot queue new requests, because more than 100'); + return; + } + tasks.push({operation: 'select', query: query, options: options, callback: callback}); + if (tasks.length === 1) processTasks(); + } else { + _getDataFromDB(query, options, callback); + } +} + +function getHistory(msg) { + var startTime = new Date().getTime(); + + var options = { + id: msg.message.id === '*' ? null : msg.message.id, + start: msg.message.options.start, + end: msg.message.options.end || ((new Date()).getTime() + 5000000), + step: parseInt(msg.message.options.step, 10) || null, + count: parseInt(msg.message.options.count, 10) || 500, + ignoreNull: msg.message.options.ignoreNull, + aggregate: msg.message.options.aggregate || 'average', // One of: max, min, average, total + limit: msg.message.options.limit || adapter.config.limit || 2000, + from: msg.message.options.from || false, + q: msg.message.options.q || false, + ack: msg.message.options.ack || false, + ms: msg.message.options.ms || false, + addId: msg.message.options.addId || false, + sessionId: msg.message.options.sessionId + }; + if (options.id && aliasMap[options.id]) { + options.id = aliasMap[options.id]; + } + + if (options.ignoreNull === 'true') options.ignoreNull = true; // include nulls and replace them with last value + if (options.ignoreNull === 'false') options.ignoreNull = false; // include nulls + if (options.ignoreNull === '0') options.ignoreNull = 0; // include nulls and replace them with 0 + if (options.ignoreNull !== true && options.ignoreNull !== false && options.ignoreNull !== 0) options.ignoreNull = false; + + if (!sqlDPs[options.id]) { + commons.sendResponse(adapter, msg, options, [], startTime); + return; + } + + if (options.start > options.end) { + var _end = options.end; + options.end = options.start; + options.start =_end; + } + + if (!options.start && !options.count) { + options.start = (new Date()).getTime() - 5030000; // - 1 year + } + + if (sqlDPs[options.id].type === undefined && sqlDPs[options.id].dbtype !== undefined) { + if (sqlDPs[options.id][adapter.namespace] && sqlDPs[options.id][adapter.namespace].storageType) { + if (storageTypes.indexOf(sqlDPs[options.id][adapter.namespace].storageType) === sqlDPs[options.id].dbtype) { + adapter.log.debug('For getHistory for id ' + options.id + ': Type empty, use storageType dbtype ' + sqlDPs[options.id].dbtype); + sqlDPs[options.id].type = sqlDPs[options.id].dbtype; + } + } + else { + adapter.log.debug('For getHistory for id ' + options.id + ': Type empty, use dbtype ' + sqlDPs[options.id].dbtype); + sqlDPs[options.id].type = sqlDPs[options.id].dbtype; + } + } + if (options.id && sqlDPs[options.id].index === undefined) { + // read or create in DB + return getId(options.id, null, function (err) { + if (err) { + adapter.log.warn('Cannot get index of "' + options.id + '": ' + err); + commons.sendResponse(adapter, msg, options, [], startTime); + } else { + getHistory(msg); + } + }); + } + if (options.id && sqlDPs[options.id].type === undefined) { + adapter.log.warn('For getHistory for id ' + options.id + ': Type empty. Need to write data first. Index = ' + sqlDPs[options.id].index); + commons.sendResponse(adapter, msg, options, 'Please wait till next data record is logged and reload.', startTime); + return; + } + var type = sqlDPs[options.id].type; + if (options.id) { + options.index = options.id; + options.id = sqlDPs[options.id].index; + } + + // if specific id requested + if (options.id || options.id === 0) { + getDataFromDB(dbNames[type], options, function (err, data) { + commons.sendResponse(adapter, msg, options, (err ? err.toString() : null) || data, startTime); + }); + } else { + // if all IDs requested + var rows = []; + var count = 0; + for (var db = 0; db < dbNames.length; db++) { + count++; + getDataFromDB(dbNames[db], options, function (err, data) { + if (data) rows = rows.concat(data); + if (!--count) { + rows.sort(sortByTs); + commons.sendResponse(adapter, msg, options, rows, startTime); + } + }); + } + } +} + +function storeState(msg) { + if (!msg.message || !msg.message.id || !msg.message.state) { + adapter.log.error('storeState called with invalid data'); + adapter.sendTo(msg.from, msg.command, { + error: 'Invalid call: ' + JSON.stringify(msg) + }, msg.callback); + return; + } + + var id; + if (Array.isArray(msg.message)) { + for (var i = 0; i < msg.message.length; i++) { + id = aliasMap[msg.message[i].id] ? aliasMap[msg.message[i].id] : msg.message[i].id; + pushValueIntoDB(id, msg.message[i].state); + } + } else if (Array.isArray(msg.message.state)) { + for (var j = 0; j < msg.message.state.length; j++) { + id = aliasMap[msg.message.id] ? aliasMap[msg.message.id] : msg.message.id; + pushValueIntoDB(id, msg.message.state[j]); + } + } else { + id = aliasMap[msg.message.id] ? aliasMap[msg.message.id] : msg.message.id; + pushValueIntoDB(id, msg.message.state); + } + + adapter.sendTo(msg.from, msg.command, { + success: true, + connected: !!clientPool + }, msg.callback); +} + +function getDpOverview(msg) { + var result = {}; + var query = SQLFuncs.getIdSelect(adapter.config.dbname); + adapter.log.info(query); + clientPool.borrow(function (err, client) { + if (err) { + adapter.sendTo(msg.from, msg.command, { + error: 'Cannot select ' + query + ': ' + err + }, msg.callback); + return; + } + client.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + adapter.sendTo(msg.from, msg.command, { + error: 'Cannot select ' + query + ': ' + err + }, msg.callback); + clientPool.return(client); + return; + } + adapter.log.info('Query result ' + JSON.stringify(rows)); + if (rows.length) { + for (var r = 0; r < rows.length; r++) { + if (!result[rows[r].type]) result[rows[r].type] = {}; + result[rows[r].type][rows[r].id] = {}; + result[rows[r].type][rows[r].id].name = rows[r].name; + switch(dbNames[rows[r].type]) { + case 'ts_number': result[rows[r].type][rows[r].id].type = 'number'; + break; + case 'ts_string': result[rows[r].type][rows[r].id].type = 'string'; + break; + case 'ts_bool': result[rows[r].type][rows[r].id].type = 'boolean'; + break; + } + } + + adapter.log.info('inited result: ' + JSON.stringify(result)); + getFirstTsForIds(client, 0, result, msg); + } + }); + }); + +} + +function getFirstTsForIds(dbClient, typeId, resultData, msg) { + if (typeId < dbNames.length) { + if (!resultData[typeId]) { + getFirstTsForIds(dbClient, typeId + 1, resultData, msg); + } else { + var query = SQLFuncs.getFirstTs(adapter.config.dbname, dbNames[typeId]); + adapter.log.info(query); + dbClient.execute(query, function (err, rows /* , fields */) { + if (rows && rows.rows) rows = rows.rows; + if (err) { + adapter.log.error('Cannot select ' + query + ': ' + err); + adapter.sendTo(msg.from, msg.command, { + error: 'Cannot select ' + query + ': ' + err + }, msg.callback); + clientPool.return(dbClient); + return; + } + adapter.log.info('Query result ' + JSON.stringify(rows)); + if (rows.length) { + for (var r = 0; r < rows.length; r++) { + if (resultData[typeId][rows[r].id]) { + resultData[typeId][rows[r].id].ts = rows[r].ts; + } + } + } + adapter.log.info('enhanced result (' + typeId + '): ' + JSON.stringify(resultData)); + setTimeout(getFirstTsForIds, 5000, dbClient, typeId + 1, resultData, msg); + }); + } + } else { + clientPool.return(dbClient); + adapter.log.info('consolidate data ...'); + var result = {}; + for (var ti = 0; ti < dbNames.length; ti++ ) { + if (resultData[ti]) { + for (var index in resultData[ti]) { + if (!resultData[ti].hasOwnProperty(index)) continue; + + var id = resultData[ti][index].name; + if (!result[id]) { + result[id] = {}; + result[id].type = resultData[ti][index].type; + result[id].ts = resultData[ti][index].ts; + } else { + result[id].type = 'undefined'; + if (resultData[ti][index].ts < result[id].ts) { + result[id].ts = resultData[ti][index].ts; + } + } + } + } + } + adapter.log.info('Result: ' + JSON.stringify(result)); + adapter.sendTo(msg.from, msg.command, { + succes: true, + result: result + }, msg.callback); + } +} + +function enableHistory(msg) { + if (!msg.message || !msg.message.id) { + adapter.log.error('enableHistory called with invalid data'); + adapter.sendTo(msg.from, msg.command, { + error: 'Invalid call' + }, msg.callback); + return; + } + var obj = {}; + obj.common = {}; + obj.common.custom = {}; + if (msg.message.options) { + obj.common.custom[adapter.namespace] = msg.message.options; + } + else { + obj.common.custom[adapter.namespace] = {}; + } + obj.common.custom[adapter.namespace].enabled = true; + adapter.extendForeignObject(msg.message.id, obj, function (err) { + if (err) { + adapter.log.error('enableHistory: ' + err); + adapter.sendTo(msg.from, msg.command, { + error: err + }, msg.callback); + } else { + adapter.log.info(JSON.stringify(obj)); + adapter.sendTo(msg.from, msg.command, { + success: true + }, msg.callback); + } + }); +} + +function disableHistory(msg) { + if (!msg.message || !msg.message.id) { + adapter.log.error('disableHistory called with invalid data'); + adapter.sendTo(msg.from, msg.command, { + error: 'Invalid call' + }, msg.callback); + return; + } + var obj = {}; + obj.common = {}; + obj.common.custom = {}; + obj.common.custom[adapter.namespace] = {}; + obj.common.custom[adapter.namespace].enabled = false; + adapter.extendForeignObject(msg.message.id, obj, function (err) { + if (err) { + adapter.log.error('disableHistory: ' + err); + adapter.sendTo(msg.from, msg.command, { + error: err + }, msg.callback); + } else { + adapter.log.info(JSON.stringify(obj)); + adapter.sendTo(msg.from, msg.command, { + success: true + }, msg.callback); + } + }); +} + +function getEnabledDPs(msg) { + var data = {}; + for (var id in sqlDPs) { + if (sqlDPs.hasOwnProperty(id) && sqlDPs[id] && sqlDPs[id][adapter.namespace]) { + data[sqlDPs[id].realId] = sqlDPs[id][adapter.namespace]; + } + } + + adapter.sendTo(msg.from, msg.command, data, msg.callback); +} + +process.on('uncaughtException', function(err) { + adapter.log.warn('Exception: ' + err); +}); diff --git a/package.json b/package.json new file mode 100644 index 0000000..49f60d7 --- /dev/null +++ b/package.json @@ -0,0 +1,48 @@ +{ + "name": "yunkong2.sql", + "description": "Log state sql in a two-stages process (first to Redis, then to CouchDB)", + "version": "1.9.2", + "author": "bluefox ", + "contributors": [ + "bluefox ", + "Apollon77" + ], + "homepage": "https://git.spacen.net/yunkong2/yunkong2.sql", + "repository": { + "type": "git", + "url": "https://git.spacen.net/yunkong2/yunkong2.sql" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://git.spacen.net/yunkong2/yunkong2.sql/blob/master/LICENSE" + } + ], + "keywords": [ + "yunkong2", + "log data", + "home automation" + ], + "optionalDependencies": { + "mysql": "^2.15.0", + "pg": "^6.1.5", + "sqlite3": "^4.0.1", + "mssql": "^3.3.0" + }, + "dependencies": { + "sql-client": "0.7.0" + }, + "devDependencies": { + "gulp": "^3.9.1", + "mocha": "^4.1.0", + "chai": "^4.1.2" + }, + "bugs": { + "url": "https://git.spacen.net/yunkong2/yunkong2.sql/issues" + }, + "main": "main.js", + "scripts": { + "test": "node node_modules/mocha/bin/mocha --exit" + }, + "license": "MIT" +} diff --git a/tasks/jscs.js b/tasks/jscs.js new file mode 100644 index 0000000..588b6f2 --- /dev/null +++ b/tasks/jscs.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + all: { + src: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ], + options: require('./jscsRules.js') + } +}; \ No newline at end of file diff --git a/tasks/jscsRules.js b/tasks/jscsRules.js new file mode 100644 index 0000000..ded301d --- /dev/null +++ b/tasks/jscsRules.js @@ -0,0 +1,36 @@ +module.exports = { + force: true, + "requireCurlyBraces": ["else", "for", "while", "do", "try", "catch"], /*"if",*/ + "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], + "requireSpaceBeforeBlockStatements": true, + "requireParenthesesAroundIIFE": true, + "disallowSpacesInFunctionDeclaration": {"beforeOpeningRoundBrace": true}, + "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true}, + "requireSpacesInFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInAnonymousFunctionExpression": {"beforeOpeningRoundBrace": true, "beforeOpeningCurlyBrace": true}, + "requireSpacesInNamedFunctionExpression": {"beforeOpeningCurlyBrace": true}, + "requireSpacesInFunctionDeclaration": {"beforeOpeningCurlyBrace": true}, + "disallowMultipleVarDecl": true, + "requireBlocksOnNewline": true, + "disallowEmptyBlocks": true, + "disallowSpacesInsideObjectBrackets": true, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpaceAfterObjectKeys": true, + "disallowSpacesInsideParentheses": true, + "requireCommaBeforeLineBreak": true, + //"requireAlignedObjectValues": "all", + "requireOperatorBeforeLineBreak": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "disallowLeftStickedOperators": ["?", "+", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], +// "requireRightStickedOperators": ["!"], +// "requireSpaceAfterBinaryOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], + //"disallowSpaceAfterBinaryOperators": [","], + "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], + "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], + "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + "requireSpaceAfterBinaryOperators": ["?", ">", ",", ">=", "<=", "<", "+", "-", "/", "*", "=", "==", "===", "!=", "!=="], + //"validateIndentation": 4, + //"validateQuoteMarks": { "mark": "\"", "escape": true }, + "disallowMixedSpacesAndTabs": true, + "disallowKeywordsOnNewLine": ["else", "catch"] + +}; diff --git a/tasks/jshint.js b/tasks/jshint.js new file mode 100644 index 0000000..f823ebc --- /dev/null +++ b/tasks/jshint.js @@ -0,0 +1,17 @@ +var srcDir = __dirname + "/../"; + +module.exports = { + options: { + force: true + }, + all: [ + srcDir + "*.js", + srcDir + "lib/*.js", + srcDir + "adapter/example/*.js", + srcDir + "tasks/**/*.js", + srcDir + "www/**/*.js", + '!' + srcDir + "www/lib/**/*.js", + '!' + srcDir + 'node_modules/**/*.js', + '!' + srcDir + 'adapter/*/node_modules/**/*.js' + ] +}; \ No newline at end of file diff --git a/test/lib/setup.js b/test/lib/setup.js new file mode 100644 index 0000000..e2a1680 --- /dev/null +++ b/test/lib/setup.js @@ -0,0 +1,728 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +// check if tmp directory exists +var fs = require('fs'); +var path = require('path'); +var child_process = require('child_process'); +var rootDir = path.normalize(__dirname + '/../../'); +var pkg = require(rootDir + 'package.json'); +var debug = typeof v8debug === 'object'; +pkg.main = pkg.main || 'main.js'; + +var adapterName = path.normalize(rootDir).replace(/\\/g, '/').split('/'); +adapterName = adapterName[adapterName.length - 2]; +var adapterStarted = false; + +function getAppName() { + var parts = __dirname.replace(/\\/g, '/').split('/'); + return parts[parts.length - 3].split('.')[0]; +} + +var appName = getAppName().toLowerCase(); + +var objects; +var states; + +var pid = null; + +function copyFileSync(source, target) { + + var targetFile = target; + + //if target is a directory a new file with the same name will be created + if (fs.existsSync(target)) { + if ( fs.lstatSync( target ).isDirectory() ) { + targetFile = path.join(target, path.basename(source)); + } + } + + try { + fs.writeFileSync(targetFile, fs.readFileSync(source)); + } + catch (err) { + console.log("file copy error: " +source +" -> " + targetFile + " (error ignored)"); + } +} + +function copyFolderRecursiveSync(source, target, ignore) { + var files = []; + + var base = path.basename(source); + if (base === adapterName) { + base = pkg.name; + } + //check if folder needs to be created or integrated + var targetFolder = path.join(target, base); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder); + } + + //copy + if (fs.lstatSync(source).isDirectory()) { + files = fs.readdirSync(source); + files.forEach(function (file) { + if (ignore && ignore.indexOf(file) !== -1) { + return; + } + + var curSource = path.join(source, file); + var curTarget = path.join(targetFolder, file); + if (fs.lstatSync(curSource).isDirectory()) { + // ignore grunt files + if (file.indexOf('grunt') !== -1) return; + if (file === 'chai') return; + if (file === 'mocha') return; + copyFolderRecursiveSync(curSource, targetFolder, ignore); + } else { + copyFileSync(curSource, curTarget); + } + }); + } +} + +if (!fs.existsSync(rootDir + 'tmp')) { + fs.mkdirSync(rootDir + 'tmp'); +} + +function storeOriginalFiles() { + console.log('Store original files...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0'] && objects['system.adapter.admin.0'].common) { + objects['system.adapter.admin.0'].common.enabled = false; + } + if (objects['system.adapter.admin.1'] && objects['system.adapter.admin.1'].common) { + objects['system.adapter.admin.1'].common.enabled = false; + } + + fs.writeFileSync(dataDir + 'objects.json.original', JSON.stringify(objects)); + try { + f = fs.readFileSync(dataDir + 'states.json'); + fs.writeFileSync(dataDir + 'states.json.original', f); + } + catch (err) { + console.log('no states.json found - ignore'); + } +} + +function restoreOriginalFiles() { + console.log('restoreOriginalFiles...'); + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var f = fs.readFileSync(dataDir + 'objects.json.original'); + fs.writeFileSync(dataDir + 'objects.json', f); + try { + f = fs.readFileSync(dataDir + 'states.json.original'); + fs.writeFileSync(dataDir + 'states.json', f); + } + catch (err) { + console.log('no states.json.original found - ignore'); + } + +} + +function checkIsAdapterInstalled(cb, counter, customName) { + customName = customName || pkg.name.split('.').pop(); + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + console.log('checkIsAdapterInstalled...'); + + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.' + customName + '.0']) { + console.log('checkIsAdapterInstalled: ready!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } else { + console.warn('checkIsAdapterInstalled: still not ready'); + } + } catch (err) { + + } + + if (counter > 20) { + console.error('checkIsAdapterInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsAdapterInstalled: wait...'); + setTimeout(function() { + checkIsAdapterInstalled(cb, counter + 1); + }, 1000); + } +} + +function checkIsControllerInstalled(cb, counter) { + counter = counter || 0; + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + console.log('checkIsControllerInstalled...'); + try { + var f = fs.readFileSync(dataDir + 'objects.json'); + var objects = JSON.parse(f.toString()); + if (objects['system.adapter.admin.0']) { + console.log('checkIsControllerInstalled: installed!'); + setTimeout(function () { + if (cb) cb(); + }, 100); + return; + } + } catch (err) { + + } + + if (counter > 20) { + console.log('checkIsControllerInstalled: Cannot install!'); + if (cb) cb('Cannot install'); + } else { + console.log('checkIsControllerInstalled: wait...'); + setTimeout(function() { + checkIsControllerInstalled(cb, counter + 1); + }, 1000); + } +} + +function installAdapter(customName, cb) { + if (typeof customName === 'function') { + cb = customName; + customName = null; + } + customName = customName || pkg.name.split('.').pop(); + console.log('Install adapter...'); + var startFile = 'node_modules/' + appName + '.js-controller/' + appName + '.js'; + // make first install + if (debug) { + child_process.execSync('node ' + startFile + ' add ' + customName + ' --enabled false', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + } else { + // add controller + var _pid = child_process.fork(startFile, ['add', customName, '--enabled', 'false'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + + waitForEnd(_pid, function () { + checkIsAdapterInstalled(function (error) { + if (error) console.error(error); + console.log('Adapter installed.'); + if (cb) cb(); + }); + }); + } +} + +function waitForEnd(_pid, cb) { + if (!_pid) { + cb(-1, -1); + return; + } + _pid.on('exit', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); + _pid.on('close', function (code, signal) { + if (_pid) { + _pid = null; + cb(code, signal); + } + }); +} + +function installJsController(cb) { + console.log('installJsController...'); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller') || + !fs.existsSync(rootDir + 'tmp/' + appName + '-data')) { + // try to detect appName.js-controller in node_modules/appName.js-controller + // travis CI installs js-controller into node_modules + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => copy it from "' + rootDir + 'node_modules/' + appName + '.js-controller"'); + // copy all + // stop controller + console.log('Stop controller if running...'); + var _pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js stop', { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // copy all files into + if (!fs.existsSync(rootDir + 'tmp')) fs.mkdirSync(rootDir + 'tmp'); + if (!fs.existsSync(rootDir + 'tmp/node_modules')) fs.mkdirSync(rootDir + 'tmp/node_modules'); + + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')){ + console.log('Copy js-controller...'); + copyFolderRecursiveSync(rootDir + 'node_modules/' + appName + '.js-controller', rootDir + 'tmp/node_modules/'); + } + + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + _pid = child_process.exec('node ' + appName + '.js setup first --console', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + __pid = child_process.fork(appName + '.js', ['setup', 'first', '--console'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + waitForEnd(__pid, function () { + checkIsControllerInstalled(function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + console.log('Setup finished.'); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }); + } else { + // check if port 9000 is free, else admin adapter will be added to running instance + var client = new require('net').Socket(); + client.connect(9000, '127.0.0.1', function() { + console.error('Cannot initiate fisrt run of test, because one instance of application is running on this PC. Stop it and repeat.'); + process.exit(0); + }); + + setTimeout(function () { + client.destroy(); + if (!fs.existsSync(rootDir + 'tmp/node_modules/' + appName + '.js-controller')) { + console.log('installJsController: no js-controller => install from git'); + + child_process.execSync('npm install https://git.spacen.net/' + appName + '/' + appName + '.js-controller/tarball/master --prefix ./ --production', { + cwd: rootDir + 'tmp/', + stdio: [0, 1, 2] + }); + } else { + console.log('Setup js-controller...'); + var __pid; + if (debug) { + // start controller + child_process.exec('node ' + appName + '.js setup first', { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2] + }); + } else { + child_process.fork(appName + '.js', ['setup', 'first'], { + cwd: rootDir + 'tmp/node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + } + + // let npm install admin and run setup + checkIsControllerInstalled(function () { + var _pid; + + if (fs.existsSync(rootDir + 'node_modules/' + appName + '.js-controller/' + appName + '.js')) { + _pid = child_process.fork(appName + '.js', ['stop'], { + cwd: rootDir + 'node_modules/' + appName + '.js-controller', + stdio: [0, 1, 2, 'ipc'] + }); + } + + waitForEnd(_pid, function () { + // change ports for object and state DBs + var config = require(rootDir + 'tmp/' + appName + '-data/' + appName + '.json'); + config.objects.port = 19001; + config.states.port = 19000; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/' + appName + '.json', JSON.stringify(config, null, 2)); + + copyAdapterToController(); + + installAdapter(function () { + storeOriginalFiles(); + if (cb) cb(true); + }); + }); + }); + }, 1000); + } + } else { + setTimeout(function () { + console.log('installJsController: js-controller installed'); + if (cb) cb(false); + }, 0); + } +} + +function copyAdapterToController() { + console.log('Copy adapter...'); + // Copy adapter to tmp/node_modules/appName.adapter + copyFolderRecursiveSync(rootDir, rootDir + 'tmp/node_modules/', ['.idea', 'test', 'tmp', '.git', appName + '.js-controller']); + console.log('Adapter copied.'); +} + +function clearControllerLog() { + var dirPath = rootDir + 'tmp/log'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear controller log...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Controller log cleared'); + } catch (err) { + console.error('cannot clear log: ' + err); + } + } +} + +function clearDB() { + var dirPath = rootDir + 'tmp/yunkong2-data/sqlite'; + var files; + try { + if (fs.existsSync(dirPath)) { + console.log('Clear sqlite DB...'); + files = fs.readdirSync(dirPath); + } else { + console.log('Create controller log directory...'); + files = []; + fs.mkdirSync(dirPath); + } + } catch(e) { + console.error('Cannot read "' + dirPath + '"'); + return; + } + if (files.length > 0) { + try { + for (var i = 0; i < files.length; i++) { + var filePath = dirPath + '/' + files[i]; + fs.unlinkSync(filePath); + } + console.log('Clear sqlite DB'); + } catch (err) { + console.error('cannot clear DB: ' + err); + } + } +} + +function setupController(cb) { + installJsController(function (isInited) { + clearControllerLog(); + clearDB(); + + if (!isInited) { + restoreOriginalFiles(); + copyAdapterToController(); + } + // read system.config object + var dataDir = rootDir + 'tmp/' + appName + '-data/'; + + var objs; + try { + objs = fs.readFileSync(dataDir + 'objects.json'); + objs = JSON.parse(objs); + } + catch (e) { + console.log('ERROR reading/parsing system configuration. Ignore'); + objs = {'system.config': {}}; + } + if (!objs || !objs['system.config']) { + objs = {'system.config': {}}; + } + + if (cb) cb(objs['system.config']); + }); +} + +function startAdapter(objects, states, callback) { + if (adapterStarted) { + console.log('Adapter already started ...'); + if (callback) callback(objects, states); + return; + } + adapterStarted = true; + console.log('startAdapter...'); + if (fs.existsSync(rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main)) { + try { + if (debug) { + // start controller + pid = child_process.exec('node node_modules/' + pkg.name + '/' + pkg.main + ' --console silly', { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2] + }); + } else { + // start controller + pid = child_process.fork('node_modules/' + pkg.name + '/' + pkg.main, ['--console', 'silly'], { + cwd: rootDir + 'tmp', + stdio: [0, 1, 2, 'ipc'] + }); + } + } catch (error) { + console.error(JSON.stringify(error)); + } + } else { + console.error('Cannot find: ' + rootDir + 'tmp/node_modules/' + pkg.name + '/' + pkg.main); + } + if (callback) callback(objects, states); +} + +function startController(isStartAdapter, onObjectChange, onStateChange, callback) { + if (typeof isStartAdapter === 'function') { + callback = onStateChange; + onStateChange = onObjectChange; + onObjectChange = isStartAdapter; + isStartAdapter = true; + } + + if (onStateChange === undefined) { + callback = onObjectChange; + onObjectChange = undefined; + } + + if (pid) { + console.error('Controller is already started!'); + } else { + console.log('startController...'); + adapterStarted = false; + var isObjectConnected; + var isStatesConnected; + + var Objects = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/objects/objectsInMemServer'); + objects = new Objects({ + connection: { + "type" : "file", + "host" : "127.0.0.1", + "port" : 19001, + "user" : "", + "pass" : "", + "noFileCache": false, + "connectTimeout": 2000 + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.warn(msg); + }, + error: function (msg) { + console.error(msg); + } + }, + connected: function () { + isObjectConnected = true; + if (isStatesConnected) { + console.log('startController: started!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onObjectChange + }); + + // Just open in memory DB itself + var States = require(rootDir + 'tmp/node_modules/' + appName + '.js-controller/lib/states/statesInMemServer'); + states = new States({ + connection: { + type: 'file', + host: '127.0.0.1', + port: 19000, + options: { + auth_pass: null, + retry_max_delay: 15000 + } + }, + logger: { + silly: function (msg) { + console.log(msg); + }, + debug: function (msg) { + console.log(msg); + }, + info: function (msg) { + console.log(msg); + }, + warn: function (msg) { + console.log(msg); + }, + error: function (msg) { + console.log(msg); + } + }, + connected: function () { + isStatesConnected = true; + if (isObjectConnected) { + console.log('startController: started!!'); + if (isStartAdapter) { + startAdapter(objects, states, callback); + } else { + if (callback) { + callback(objects, states); + callback = null; + } + } + } + }, + change: onStateChange + }); + } +} + +function stopAdapter(cb) { + if (!pid) { + console.error('Controller is not running!'); + if (cb) { + setTimeout(function () { + cb(false); + }, 0); + } + } else { + adapterStarted = false; + pid.on('exit', function (code, signal) { + if (pid) { + console.log('child process terminated due to receipt of signal ' + signal); + if (cb) cb(); + pid = null; + } + }); + + pid.on('close', function (code, signal) { + if (pid) { + if (cb) cb(); + pid = null; + } + }); + + pid.kill('SIGTERM'); + } +} + +function _stopController() { + if (objects) { + objects.destroy(); + objects = null; + } + if (states) { + states.destroy(); + states = null; + } +} + +function stopController(cb) { + var timeout; + if (objects) { + console.log('Set system.adapter.' + pkg.name + '.0'); + objects.setObject('system.adapter.' + pkg.name + '.0', { + common:{ + enabled: false + } + }); + } + + stopAdapter(function () { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + + _stopController(); + + if (cb) { + cb(true); + cb = null; + } + }); + + timeout = setTimeout(function () { + timeout = null; + console.log('child process NOT terminated'); + + _stopController(); + + if (cb) { + cb(false); + cb = null; + } + pid = null; + }, 5000); +} + +// Setup the adapter +function setAdapterConfig(common, native, instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + if (common) objects[id].common = common; + if (native) objects[id].native = native; + fs.writeFileSync(rootDir + 'tmp/' + appName + '-data/objects.json', JSON.stringify(objects)); +} + +// Read config of the adapter +function getAdapterConfig(instance) { + var objects = JSON.parse(fs.readFileSync(rootDir + 'tmp/' + appName + '-data/objects.json').toString()); + var id = 'system.adapter.' + adapterName.split('.').pop() + '.' + (instance || 0); + return objects[id]; +} + +if (typeof module !== undefined && module.parent) { + module.exports.getAdapterConfig = getAdapterConfig; + module.exports.setAdapterConfig = setAdapterConfig; + module.exports.startController = startController; + module.exports.stopController = stopController; + module.exports.setupController = setupController; + module.exports.stopAdapter = stopAdapter; + module.exports.startAdapter = startAdapter; + module.exports.installAdapter = installAdapter; + module.exports.appName = appName; + module.exports.adapterName = adapterName; + module.exports.adapterStarted = adapterStarted; +} diff --git a/test/testMSSQL.js b/test/testMSSQL.js new file mode 100644 index 0000000..e226faf --- /dev/null +++ b/test/testMSSQL.js @@ -0,0 +1,397 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('MSSQL: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('MSSQL: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test MSSQL', function() { + before('Test MSSQL: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + console.log('Started in TRAVIS: ' + (process.env.TRAVIS && process.env.TRAVIS==='true')); + console.log('Started in APPVEYOR: ' + (process.env.APPVEYOR && process.env.APPVEYOR==='True')); + + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + console.log('MSSQL testing only available in Appveyor on Windows, ignore test run (APPVEYOR:' + JSON.stringify(process.env.APPVEYOR) + ', TRAVIS:' + JSON.stringify(process.env.TRAVIS) + ')'); + _done(); + return; + } + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'mssql'; + config.native.user = 'sa'; + config.native.password = 'Password12!'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + objects.setObject('sql.0.memRss', { + common: { + type: 'number', + role: 'state', + custom: { + "sql.0": { + enabled: true, + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5 + } + } + }, + type: 'state' + }, _done); + }); + }); + }); + + it('Test MSSQL: Check if adapter started', function (done) { + this.timeout(60000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function () { + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.memRss', + options: { + changesOnly: true, + debounce: 0, + retention: 31536000, + changesMinDelta: 0.5, + storageType: 'Number' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'String' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Boolean' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 20000); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test MSSQL: Write values into DB', function (done) { + this.timeout(10000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test MSSQL: Read values from DB using query', function (done) { + this.timeout(10000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'query', "SELECT id FROM yunkong2.dbo.datapoints WHERE name='sql.0.memRss'", function (result) { + sendTo('sql.0', 'query', 'SELECT * FROM yunkong2.dbo.ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('MSSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test MSSQL: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('MSSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('MSSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'query', "SELECT name, type FROM yunkong2.dbo.datapoints", function (result) { + console.log('MSSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(1); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(2); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(2); + done(); + }); + }); + + after('Test MSSQL: Stop js-controller', function (done) { + this.timeout(6000); + if (!(process.env.APPVEYOR && process.env.APPVEYOR==='True')) { + done(); + return; + } + + setup.stopController(function (normalTerminated) { + console.log('MSSQL: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testMySQL.js b/test/testMySQL.js new file mode 100644 index 0000000..ad7684f --- /dev/null +++ b/test/testMySQL.js @@ -0,0 +1,374 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('MySQL: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('MySQL: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test MySQL', function() { + before('Test MySQL: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'mysql'; + config.native.user = 'root'; + if (process.env.APPVEYOR && process.env.APPVEYOR==='True') { + config.native.password = 'Password12!'; + } + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + objects.setObject('sql.0.memRss', { + common: { + type: 'number', + role: 'state', + custom: { + "sql.0": { + enabled: true, + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5 + } + } + }, + type: 'state' + }, _done); + }); + }); + }); + + it('Test MySQL: Check if adapter started', function (done) { + this.timeout(90000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function () { + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'String' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.alive', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Boolean' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 70000); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(4); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test MySQL: Write values into DB', function (done) { + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.3, ts: now - 3500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test MySQL: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', 'SELECT id FROM yunkong2.datapoints WHERE name="sql.0.memRss"', function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + sendTo('sql.0', 'query', 'SELECT * FROM yunkong2.ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + var found22 = false; + var found23 = false; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + if (result.result[i].val === 2.2) found22 = true; + if (result.result[i].val === 2.3) found23 = true; + } + expect(found).to.be.equal(6); + expect(found22).to.be.false; + expect(found23).to.be.true; + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test MySQL: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'query', "SELECT id, name, type FROM yunkong2.datapoints", function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + var uptime_id = null; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(1); + } + else if (result.result[i].name === 'system.adapter.sql.0.alive') { + expect(result.result[i].type).to.be.equal(2); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(0); + uptime_id = result.result[i].id; + expect(uptime_id).to.be.not.null; + } + } + + sendTo('sql.0', 'query', "UPDATE yunkong2.datapoints SET type=NULL WHERE id=" + uptime_id, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.affectedRows).to.be.equal(1); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + done(); + }); + }); + + after('Test MySQL: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('MySQL: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testMySQLDash.js b/test/testMySQLDash.js new file mode 100644 index 0000000..a748d34 --- /dev/null +++ b/test/testMySQLDash.js @@ -0,0 +1,482 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('MySQL-with-dash: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('MySQL-with-dash: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test MySQL-with-dash', function() { + before('Test MySQL-with-dash: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'mysql'; + config.native.user = 'root'; + config.native.dbname = 'io-broker'; + if (process.env.APPVEYOR && process.env.APPVEYOR==='True') { + config.native.password = 'Password12!'; + } + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + objects.setObject('sql.0.memRss', { + common: { + type: 'number', + role: 'state', + custom: { + "sql.0": { + enabled: true, + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5 + } + } + }, + type: 'state' + }, _done); + }); + }); + }); + + it('Test MySQL-with-dash: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function () { + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.alive', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + objects.setObject('sql.0.testValue2', { + common: { + type: 'number', + role: 'state' + }, + type: 'state' + }, + function () { + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.testValue2', + options: { + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5, + aliasId: 'sql.0.testValue2-alias' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 2000); + }); + }); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(5); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test MySQL-with-dash: Write values into DB', function (done) { + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.testValue2', {val: 1, ts: now - 2000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.testValue2', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test MySQL-with-dash: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', 'SELECT id FROM `io-broker`.datapoints WHERE name="sql.0.memRss"', function (result) { + console.log('MySQL-with-dash: ' + JSON.stringify(result.result, null, 2)); + sendTo('sql.0', 'query', 'SELECT * FROM `io-broker`.ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('MySQL-with-dash: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test MySQL-with-dash: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL-with-dash: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL-with-dash: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'query', "SELECT name, type FROM `io-broker`.datapoints", function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.alive') { + expect(result.result[i].type).to.be.equal(2); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(0); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }); + + it('Test ' + adapterShortName + ': Read values from DB using GetHistory for aliased testValue2', function (done) { + this.timeout(25000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.testValue2', + options: { + start: now - 5000, + end: now, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log(JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.testValue2-alias', + options: { + start: now - 5000, + end: now, + count: 50, + aggregate: 'none' + } + }, function (result2) { + console.log(JSON.stringify(result2.result, null, 2)); + expect(result2.result.length).to.be.equal(2); + for (var i = 0; i < result2.result.length; i++) { + expect(result2.result[i].val).to.be.equal(result.result[i].val); + } + + done(); + }); + }); + }); + + it('Test ' + adapterShortName + ': Remove Alias-ID', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.testValue2', + options: { + aliasId: '' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 2000); + }); + }); + it('Test ' + adapterShortName + ': Add Alias-ID again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.testValue2', + options: { + aliasId: 'this.is.a.test-value' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 2000); + }); + }); + it('Test ' + adapterShortName + ': Change Alias-ID', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.testValue2', + options: { + aliasId: 'this.is.another.test-value' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 2000); + }); + }); + + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(4); + done(); + }); + }); + + after('Test MySQL-with-dash: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('MySQL-with-dash: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testMySQLExisting.js b/test/testMySQLExisting.js new file mode 100644 index 0000000..f398367 --- /dev/null +++ b/test/testMySQLExisting.js @@ -0,0 +1,368 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); +var now2; +if (!now2) now2 = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('MySQL: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('MySQL: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test MySQL Existing', function() { + before('Test MySQL Existing: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'mysql'; + config.native.user = 'root'; + if (process.env.APPVEYOR && process.env.APPVEYOR==='True') { + config.native.password = 'Password12!'; + } + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + + it('Test MySQL Existing: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function() { + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.memRss', + options: { + changesOnly: true, + debounce: 0, + retention: 31536000, + changesMinDelta: 0.5, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Number' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.alive', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 20000); + }); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(4); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test MySQL Existing: Write values into DB', function (done) { + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.3, ts: now - 3500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test MySQL Existing: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', 'SELECT id FROM yunkong2.datapoints WHERE name="sql.0.memRss"', function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + sendTo('sql.0', 'query', 'SELECT * FROM yunkong2.ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + var found22 = false; + var found23 = false; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + if (result.result[i].val === 2.2) found22 = true; + if (result.result[i].val === 2.3) found23 = true; + } + expect(found).to.be.equal(12); + expect(found22).to.be.false; + expect(found23).to.be.true; + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test MySQL Existing: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now2 - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(12); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now2 - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'query', "SELECT name, type FROM yunkong2.datapoints", function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.alive') { + expect(result.result[i].type).to.be.equal(2); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(0); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + done(); + }); + }); + + after('Test MySQL Existing: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('MySQL: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testMySQLExistingNoNulls.js b/test/testMySQLExistingNoNulls.js new file mode 100644 index 0000000..3ccfb7d --- /dev/null +++ b/test/testMySQLExistingNoNulls.js @@ -0,0 +1,369 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); +var now2; +if (!now2) now2 = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('MySQL: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('MySQL: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test MySQL Existing No Nulls', function() { + before('Test MySQL Existing No Nulls: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.writeNulls = false; + config.native.dbtype = 'mysql'; + config.native.user = 'root'; + if (process.env.APPVEYOR && process.env.APPVEYOR==='True') { + config.native.password = 'Password12!'; + } + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + _done(); + }); + }); + }); + + it('Test MySQL Existing No Nulls: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function() { + sendTo('sql.0', 'enableHistory', { + id: 'sql.0.memRss', + options: { + changesOnly: true, + debounce: 0, + retention: 31536000, + changesMinDelta: 0.5, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Number' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.alive', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 20000); + }); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(4); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test MySQL Existing No Nulls: Write values into DB', function (done) { + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.3, ts: now - 3500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test MySQL Existing No Nulls: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', 'SELECT id FROM yunkong2.datapoints WHERE name="sql.0.memRss"', function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + sendTo('sql.0', 'query', 'SELECT * FROM yunkong2.ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + var found22 = false; + var found23 = false; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + if (result.result[i].val === 2.2) found22 = true; + if (result.result[i].val === 2.3) found23 = true; + } + expect(found).to.be.equal(18); + expect(found22).to.be.false; + expect(found23).to.be.true; + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test MySQL Existing No Nulls: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now2 - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(18); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now2 - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'query', "SELECT name, type FROM yunkong2.datapoints", function (result) { + console.log('MySQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.alive') { + expect(result.result[i].type).to.be.equal(2); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(0); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + done(); + }); + }); + + after('Test MySQL Existing No Nulls: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('MySQL: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testPackageFiles.js b/test/testPackageFiles.js new file mode 100644 index 0000000..d0759c0 --- /dev/null +++ b/test/testPackageFiles.js @@ -0,0 +1,91 @@ +/* jshint -W097 */ +/* jshint strict:false */ +/* jslint node: true */ +/* jshint expr: true */ +var expect = require('chai').expect; +var fs = require('fs'); + +describe('Test package.json and io-package.json', function() { + it('Test package files', function (done) { + console.log(); + + var fileContentIOPackage = fs.readFileSync(__dirname + '/../io-package.json', 'utf8'); + var ioPackage = JSON.parse(fileContentIOPackage); + + var fileContentNPMPackage = fs.readFileSync(__dirname + '/../package.json', 'utf8'); + var npmPackage = JSON.parse(fileContentNPMPackage); + + expect(ioPackage).to.be.an('object'); + expect(npmPackage).to.be.an('object'); + + expect(ioPackage.common.version, 'ERROR: Version number in io-package.json needs to exist').to.exist; + expect(npmPackage.version, 'ERROR: Version number in package.json needs to exist').to.exist; + + expect(ioPackage.common.version, 'ERROR: Version numbers in package.json and io-package.json needs to match').to.be.equal(npmPackage.version); + + if (!ioPackage.common.news || !ioPackage.common.news[ioPackage.common.version]) { + console.log('WARNING: No news entry for current version exists in io-package.json, no rollback in Admin possible!'); + console.log(); + } + + expect(npmPackage.author, 'ERROR: Author in package.json needs to exist').to.exist; + expect(ioPackage.common.authors, 'ERROR: Authors in io-package.json needs to exist').to.exist; + + if (ioPackage.common.name.indexOf('template') !== 0) { + if (Array.isArray(ioPackage.common.authors)) { + expect(ioPackage.common.authors.length, 'ERROR: Author in io-package.json needs to be set').to.not.be.equal(0); + if (ioPackage.common.authors.length === 1) { + expect(ioPackage.common.authors[0], 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + expect(ioPackage.common.authors, 'ERROR: Author in io-package.json needs to be a real name').to.not.be.equal('my Name '); + } + } + else { + console.log('WARNING: Testing for set authors field in io-package skipped because template adapter'); + console.log(); + } + expect(fs.existsSync(__dirname + '/../README.md'), 'ERROR: README.md needs to exist! Please create one with description, detail information and changelog. English is mandatory.').to.be.true; + if (!ioPackage.common.titleLang || typeof ioPackage.common.titleLang !== 'object') { + console.log('WARNING: titleLang is not existing in io-package.json. Please add'); + console.log(); + } + if ( + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('yunkong2') !== -1 || + ioPackage.common.title.indexOf('adapter') !== -1 || + ioPackage.common.title.indexOf('Adapter') !== -1 + ) { + console.log('WARNING: title contains Adapter or yunkong2. It is clear anyway, that it is adapter for yunkong2.'); + console.log(); + } + + if (ioPackage.common.name.indexOf('vis-') !== 0) { + if (!ioPackage.common.materialize || !fs.existsSync(__dirname + '/../admin/index_m.html') || !fs.existsSync(__dirname + '/../gulpfile.js')) { + console.log('WARNING: Admin3 support is missing! Please add it'); + console.log(); + } + if (ioPackage.common.materialize) { + expect(fs.existsSync(__dirname + '/../admin/index_m.html'), 'Admin3 support is enabled in io-package.json, but index_m.html is missing!').to.be.true; + } + } + + var licenseFileExists = fs.existsSync(__dirname + '/../LICENSE'); + var fileContentReadme = fs.readFileSync(__dirname + '/../README.md', 'utf8'); + if (fileContentReadme.indexOf('## Changelog') === -1) { + console.log('Warning: The README.md should have a section ## Changelog'); + console.log(); + } + expect((licenseFileExists || fileContentReadme.indexOf('## License') !== -1), 'A LICENSE must exist as LICENSE file or as part of the README.md').to.be.true; + if (!licenseFileExists) { + console.log('Warning: The License should also exist as LICENSE file'); + console.log(); + } + if (fileContentReadme.indexOf('## License') === -1) { + console.log('Warning: The README.md should also have a section ## License to be shown in Admin3'); + console.log(); + } + done(); + }); +}); diff --git a/test/testPostgreSQL.js b/test/testPostgreSQL.js new file mode 100644 index 0000000..b082ba3 --- /dev/null +++ b/test/testPostgreSQL.js @@ -0,0 +1,342 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('PostgreSQL: ' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('PostgreSQL: ' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test PostgreSQL', function() { + before('Test PostgreSQL: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'postgresql'; + config.native.user = 'postgres'; + if (process.env.APPVEYOR && process.env.APPVEYOR==='True') { + config.native.password = 'Password12!'; + } + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + objects.setObject('sql.0.memRss', { + common: { + type: 'number', + role: 'state', + custom: { + "sql.0": { + enabled: true, + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5 + } + } + }, + type: 'state' + }, _done); + }); + }); + }); + + it('Test PostgreSQL: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function () { + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'String' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Boolean' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 20000); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test PostgreSQL: Write values into DB', function (done) { + this.timeout(10000); + + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test PostgreSQL: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', "SELECT id FROM datapoints WHERE name='sql.0.memRss'", function (result) { + sendTo('sql.0', 'query', 'SELECT * FROM ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('PostgreSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test PostgreSQL: Read values from DB using GetHistory', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('PostgreSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('PostgreSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'query', "SELECT name, type FROM datapoints", function (result) { + console.log('PostgreSQL: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(1); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(2); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(2); + done(); + }); + }); + + after('Test PostgreSQL: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('PostgreSQL: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +}); diff --git a/test/testSQLite.js b/test/testSQLite.js new file mode 100644 index 0000000..e22c1b4 --- /dev/null +++ b/test/testSQLite.js @@ -0,0 +1,355 @@ +/* jshint -W097 */// jshint strict:false +/*jslint node: true */ +/*jshint expr: true*/ +var expect = require('chai').expect; +var setup = require(__dirname + '/lib/setup'); + +var objects = null; +var states = null; +var onStateChanged = null; +var onObjectChanged = null; +var sendToID = 1; + +var adapterShortName = setup.adapterName.substring(setup.adapterName.indexOf('.')+1); + +var now = new Date().getTime(); + +function checkConnectionOfAdapter(cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check connection'); + return; + } + + states.getState('system.adapter.' + adapterShortName + '.0.alive', function (err, state) { + if (err) console.error('SQLite:' + err); + if (state && state.val) { + cb && cb(); + } else { + setTimeout(function () { + checkConnectionOfAdapter(cb, counter + 1); + }, 1000); + } + }); +} + +function checkValueOfState(id, value, cb, counter) { + counter = counter || 0; + if (counter > 20) { + cb && cb('Cannot check value Of State ' + id); + return; + } + + states.getState(id, function (err, state) { + if (err) console.error('SQLite:' + err); + if (value === null && !state) { + cb && cb(); + } else + if (state && (value === undefined || state.val === value)) { + cb && cb(); + } else { + setTimeout(function () { + checkValueOfState(id, value, cb, counter + 1); + }, 500); + } + }); +} + +function sendTo(target, command, message, callback) { + onStateChanged = function (id, state) { + if (id === 'messagebox.system.adapter.test.0') { + callback(state.message); + } + }; + + states.pushMessage('system.adapter.' + target, { + command: command, + message: message, + from: 'system.adapter.test.0', + callback: { + message: message, + id: sendToID++, + ack: false, + time: (new Date()).getTime() + } + }); +} + +describe('Test SQLite', function() { + before('Test SQLite: Start js-controller', function (_done) { + this.timeout(600000); // because of first install from npm + setup.adapterStarted = false; + + setup.setupController(function () { + var config = setup.getAdapterConfig(); + // enable adapter + config.common.enabled = true; + config.common.loglevel = 'debug'; + + config.native.dbtype = 'sqlite'; + + setup.setAdapterConfig(config.common, config.native); + + setup.startController(true, function(id, obj) {}, function (id, state) { + if (onStateChanged) onStateChanged(id, state); + }, + function (_objects, _states) { + objects = _objects; + states = _states; + objects.setObject('sql.0.memRss', { + common: { + type: 'number', + role: 'state', + custom: { + "sql.0": { + enabled: true, + changesOnly: true, + debounce: 0, + retention: 31536000, + maxLength: 3, + changesMinDelta: 0.5 + } + } + }, + type: 'state' + }, _done); + }); + }); + }); + + it('Test SQLite: Check if adapter started', function (done) { + this.timeout(60000); + checkConnectionOfAdapter(function () { + now = new Date().getTime(); + objects.setObject('system.adapter.test.0', { + common: { + + }, + type: 'instance' + }, + function () { + states.subscribeMessage('system.adapter.test.0'); + setTimeout(function () { + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.memHeapTotal', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'String' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.alive', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: 'Boolean' + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + sendTo('sql.0', 'enableHistory', { + id: 'system.adapter.sql.0.uptime', + options: { + changesOnly: false, + debounce: 0, + retention: 31536000, + storageType: false + } + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + // wait till adapter receives the new settings + setTimeout(function () { + done(); + }, 20000); + }); + }); + }); + }, 10000); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Enable', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(4); + expect(result['sql.0.memRss'].enabled).to.be.true; + setTimeout(function () { + done(); + }, 15000); + }); + }); + it('Test SQLite: Write values into DB', function (done) { + this.timeout(10000); + + this.timeout(10000); + + states.setState('sql.0.memRss', {val: 2, ts: now - 20000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: true, ts: now - 10000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2, ts: now - 5000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 2.2, ts: now - 4000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: '2.5', ts: now - 3000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 3, ts: now - 1000}, function (err) { + if (err) { + console.log(err); + } + setTimeout(function () { + states.setState('sql.0.memRss', {val: 'Test', ts: now - 500}, function (err) { + if (err) { + console.log(err); + } + setTimeout(done, 5000); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }, 100); + }); + }); + it('Test SQLite: Read values from DB using query', function (done) { + this.timeout(10000); + + sendTo('sql.0', 'query', 'SELECT id FROM datapoints WHERE name="sql.0.memRss"', function (result) { + sendTo('sql.0', 'query', 'SELECT * FROM ts_number WHERE id=' + result.result[0].id, function (result) { + console.log('SQLite:' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + setTimeout(function () { + done(); + }, 3000); + }); + }); + }); + it('Test SQLite: Read values from DB using GetHistory', function (done) { + this.timeout(20000); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 30000, + limit: 50, + count: 50, + aggregate: 'none' + } + }, function (result) { + console.log('SQLite:' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.at.least(5); + var found = 0; + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].val >= 1 && result.result[i].val <= 3) found ++; + } + expect(found).to.be.equal(6); + + sendTo('sql.0', 'getHistory', { + id: 'sql.0.memRss', + options: { + start: now - 15000, + end: now, + limit: 2, + count: 2, + aggregate: 'none' + } + }, function (result) { + console.log('SQLite:' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.be.equal(2); + done(); + }); + }); + }); + it('Test ' + adapterShortName + ': Check Datapoint Types', function (done) { + this.timeout(125000); + + setTimeout(function() { + sendTo('sql.0', 'query', "SELECT name, type FROM datapoints", function (result) { + console.log('SQLite: ' + JSON.stringify(result.result, null, 2)); + expect(result.result.length).to.least(3); + for (var i = 0; i < result.result.length; i++) { + if (result.result[i].name === 'sql.0.memRss') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.memHeapTotal') { + expect(result.result[i].type).to.be.equal(1); + } + else if (result.result[i].name === 'system.adapter.sql.0.uptime') { + expect(result.result[i].type).to.be.equal(0); + } + else if (result.result[i].name === 'system.adapter.sql.0.alive') { + expect(result.result[i].type).to.be.equal(2); + } + } + + setTimeout(function () { + done(); + }, 3000); + }); + }, 121000); + }); + it('Test ' + adapterShortName + ': Disable Datapoint again', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'disableHistory', { + id: 'sql.0.memRss', + }, function (result) { + expect(result.error).to.be.undefined; + expect(result.success).to.be.true; + setTimeout(done, 2000); + }); + }); + it('Test ' + adapterShortName + ': Check Enabled Points after Disable', function (done) { + this.timeout(5000); + + sendTo('sql.0', 'getEnabledDPs', {}, function (result) { + console.log(JSON.stringify(result)); + expect(Object.keys(result).length).to.be.equal(3); + done(); + }); + }); + + after('Test SQLite: Stop js-controller', function (done) { + this.timeout(6000); + + setup.stopController(function (normalTerminated) { + console.log('SQLite: Adapter normal terminated: ' + normalTerminated); + done(); + }); + }); +});