diff --git a/Dockerfile b/Dockerfile
index 8179132..2143f07 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -28,6 +28,12 @@ WORKDIR /opt/OpenHaus/backend
COPY --from=builder node_modules node_modules
RUN apk --no-cache add openssl
+ARG version=unknown
+LABEL version=$version
+
+ARG buildDate=unknown
+LABEL buildDate=$buildDate
+
COPY ./build/ ./
#COPY ./package.json ./
diff --git a/Gruntfile.js b/Gruntfile.js
index 9c309d6..6cbaf4b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -88,10 +88,22 @@ module.exports = function (grunt) {
grunt.registerTask("build:docker", () => {
- cp.execSync(`docker build . -t openhaus/${pkg.name}:latest --build-arg version=${pkg.version}`, {
+
+ let buildArgs = [
+ `--build-arg version=${pkg.version}`,
+ `--build-arg buildDate=${Date.now()}`,
+ ].join(" ");
+
+ cp.execSync(`docker build . -t openhaus/${pkg.name}:${pkg.version} ${buildArgs}`, {
env: process.env,
stdio: "inherit"
});
+
+ cp.execSync(`docker build . -t openhaus/${pkg.name}:latest ${buildArgs}`, {
+ env: process.env,
+ stdio: "inherit"
+ });
+
});
@@ -137,4 +149,18 @@ module.exports = function (grunt) {
});
+ grunt.registerTask("publish", () => {
+ [
+ `docker push openhaus/${pkg.name}:${pkg.version}`,
+ `docker push openhaus/${pkg.name}:latest`
+ ].forEach((cmd) => {
+ cp.execSync(cmd, {
+ env: process.env,
+ stdio: "inherit"
+ });
+ });
+
+ });
+
+
};
\ No newline at end of file
diff --git a/README.md b/README.md
index f691935..5d3640c 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,18 @@ The [docs](docs) folder is soon be removed.
For the HTTP API we provide a [postman collection](postman.json).
+## Demo
+There exsits a public demo: http://demo.open-haus.io
+It is deployed with docker and runs the frontend & backend container.
+The instance is rested to its default values every 10 Minutes.
+No authentication required, full API support.
+
+
+## HTTP API
+We provide a [postmann collection](./postman.json) that you can import.
+It containes documentation about every URL endpoint and its meaning.
+Get postman on https://www.postman.com/ its great tool for HTTP testing & documentation.
+
## Contribution
If you have questions, want to contribute or just wanna have a talk, open a new issue.
@@ -25,16 +37,6 @@ Fork this repository, apply the changes you want to make, and create a pull requ
__*Note*__: If you want to contribute, please take a look on the "documentation" repository, section "[How to document the source code](https://github.com/OpenHausIO/documentation#how-to-document-the-source-code)".
-## Docker quick start:
-Build the docker image:
-```sh
-npm run build:docker-image
-```
-
-Start a container:
-```sh
-docker run --rm -it --net host --name backend --env=UUID=c04a9aa6-7261-11ed-8578-cb6ee612422e --env=VAULT_MASTER_PASSWORD=Pa$$w0rd --env=USERS_JWT_SECRET=Pa$$w0rd --env=DATABASE_HOST=172.17.0.1 --env=API_AUTH_ENABLED=false openhaus/backend
-```
## License
Im currently not sure, under what license i publish this work.
diff --git a/adapter/base64.js b/adapter/base64.js
deleted file mode 100644
index 358419a..0000000
--- a/adapter/base64.js
+++ /dev/null
@@ -1,41 +0,0 @@
-const { Transform } = require("stream");
-const logger = require("../system/logger").create("adapter/base64");
-
-module.exports = (options) => {
-
- const encode = new Transform({
- transform(chunk, encoding, cb) {
-
- let data = chunk.toString("base64");
-
- logger.verbose("[encode]: %j", data);
-
- this.push(data);
-
- cb();
-
- },
- ...options
- });
-
- const decode = new Transform({
- transform(chunk, encoding, cb) {
-
- let data = Buffer.from(chunk.toString(), "base64").toString("ascii");
-
- logger.verbose("[decode]: %j", data);
-
- this.push(data);
-
- cb();
-
- },
- ...options
- });
-
- return {
- encode,
- decode
- };
-
-};
\ No newline at end of file
diff --git a/adapter/json.js b/adapter/json.js
deleted file mode 100644
index 7e80e09..0000000
--- a/adapter/json.js
+++ /dev/null
@@ -1,42 +0,0 @@
-const { Transform } = require("stream");
-const logger = require("../system/logger").create("adapter/json");
-
-module.exports = (options) => {
-
- const encode = new Transform({
- transform(chunk, encoding, cb) {
-
- let data = JSON.stringify(chunk);
-
- logger.verbose("[encode] %d", data);
-
- this.push(data);
-
- cb();
-
- },
- ...options
- });
-
- const decode = new Transform({
- transform(chunk, encoding, cb) {
-
- let data = JSON.parse(chunk);
-
- logger.verbose("[decode] %d", data);
-
- this.push(data);
-
- cb();
-
- },
- ...options
-
- });
-
- return {
- encode,
- decode
- };
-
-};
\ No newline at end of file
diff --git a/backend.service b/backend.service
new file mode 100644
index 0000000..6d163d2
--- /dev/null
+++ b/backend.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=OpenHaus Backend
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+ExecStart=/usr/bin/node /opt/OpenHaus/backend/index.js
+WorkingDirectory=/opt/OpenHaus/backend
+Restart=always
+RestartSec=10
+Environment=NODE_ENV=production
+Environment=VAULT_MASTER_PASSWORD=Pa$$w0rd
+Environment=USERS_JWT_SECRET=Pa$$w0rd
+Environment=UUID=00000000-0000-0000-0000-000000000000
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/components/devices/class.interface.js b/components/devices/class.interface.js
index 985ad3b..aff2909 100644
--- a/components/devices/class.interface.js
+++ b/components/devices/class.interface.js
@@ -87,7 +87,7 @@ module.exports = class Interface {
is: "SERIAL",
then: SERIAL
}),
- adapter: Joi.array().items("base64", "eiscp", "json", "raw").default(["raw"]),
+ adapter: Joi.array().items("eiscp", "raw").default(["raw"]),
description: Joi.string().allow(null).default(null)
});
@@ -108,7 +108,7 @@ module.exports = class Interface {
options = Object.assign({
keepAlive: true,
- maxSockets: 1,
+ //maxSockets: 1,
keepAliveMsecs: 3000, // use this as websocket ping/pong value to detect broken connections?
}, options);
diff --git a/components/mdns/class.mdns.js b/components/mdns/class.mdns.js
new file mode 100644
index 0000000..19efe27
--- /dev/null
+++ b/components/mdns/class.mdns.js
@@ -0,0 +1,23 @@
+class MDNS {
+
+ constructor(obj) {
+
+ Object.assign(this, obj);
+ this._id = String(obj._id);
+
+ Object.defineProperty(this, "_matches", {
+ value: [],
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ }
+
+ match(cb) {
+ this._matches.push(cb);
+ }
+
+}
+
+module.exports = MDNS;
\ No newline at end of file
diff --git a/components/mdns/index.js b/components/mdns/index.js
new file mode 100644
index 0000000..5435dd4
--- /dev/null
+++ b/components/mdns/index.js
@@ -0,0 +1,90 @@
+const mongodb = require("mongodb");
+const Joi = require("joi");
+
+//const logger = require("../../system/logger").create("rooms");
+//const COMMON_COMPONENT = require("../../system/component/common.js");
+const COMPONENT = require("../../system/component/class.component.js");
+
+const MDNS = require("./class.mdns.js");
+
+const messageHandler = require("./message-handler.js");
+
+/**
+ * @description
+ * Listen for mdns message and sends query requests.
+ * This requires the "connector".
+ *
+ * The emitted message events is the parsed data received on the underlaying udp socket.
+ *
+ * @class C_MDNS
+ * @extends COMPONENT system/component/class.component.js
+ *
+ * @emits message Received message on udp socket; Arguments: [0]=parsed dns packet, [1]=raw udp message
+ *
+ * @link router.api.mdns.js routes/router.api.mdns.js
+ * @see https://en.wikipedia.org/wiki/Multicast_DNS
+ * @see https://www.npmjs.com/package/dns-packet
+ */
+class C_MDNS extends COMPONENT {
+
+ constructor() {
+
+ // inject logger, collection and schema object
+ super("mdns", {
+ _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => {
+ return String(new mongodb.ObjectId());
+ }),
+ name: Joi.string().required(),
+ type: Joi.string().valid("SRV", "PTR", "A", "AAAA").default("A"),
+ timestamps: {
+ announced: Joi.number().allow(null).default(null)
+ }
+ }, module);
+
+ this.hooks.post("add", (data, next) => {
+ next(null, new MDNS(data));
+ });
+
+ this.collection.createIndex({
+ name: 1,
+ type: 1
+ }, {
+ unique: true
+ });
+
+ // handle incoming messages
+ // triggers registerd callback for mdns items
+ messageHandler(this);
+
+ }
+
+}
+
+
+// create component instance
+const instance = module.exports = new C_MDNS();
+
+
+// init component
+// set items/build cache
+instance.init((scope, ready) => {
+ scope.collection.find({}).toArray((err, data) => {
+ if (err) {
+
+ // shit...
+ ready(err);
+
+ } else {
+
+ data = data.map((obj) => {
+ return new MDNS(obj);
+ });
+
+ scope.items.push(...data);
+
+ // init done
+ ready(null);
+
+ }
+ });
+});
\ No newline at end of file
diff --git a/components/mdns/message-handler.js b/components/mdns/message-handler.js
new file mode 100644
index 0000000..20d3dcc
--- /dev/null
+++ b/components/mdns/message-handler.js
@@ -0,0 +1,101 @@
+const { encode, RECURSION_DESIRED } = require("dns-packet");
+
+module.exports = (scope) => {
+ scope._ready(({ logger }) => {
+
+ // proceed mdns item instance
+ // deconstruct properties
+ let matchCallbacks = scope.items.map(({ name, type, _matches }) => {
+ return {
+ name,
+ type,
+ _matches
+ };
+ });
+
+
+ // listen for newly added items
+ scope.events.on("added", ({ name, type, _matches }) => {
+ matchCallbacks.push({
+ name,
+ type,
+ _matches
+ });
+ });
+
+
+ scope.events.on("connected", (ws) => {
+
+ let questions = scope.items.map(({ type, name }) => {
+ return {
+ type,
+ name
+ };
+ });
+
+ let query = encode({
+ type: "query",
+ id: 1,
+ flags: RECURSION_DESIRED,
+ questions
+ });
+
+ logger.debug("Connected, send query", query, questions);
+
+ ws.send(query);
+
+ });
+
+
+ scope.events.on("message", (packet, message) => {
+
+ // feedback
+ logger.trace("Message received on udp socket, record:", packet, "message: ", message);
+
+ if (packet.type === "response") {
+ packet.answers.forEach((record) => {
+ matchCallbacks.forEach(async ({ name, type, _matches }, i) => {
+
+ // create regex from db data
+ // allow wildcards writen as * in items
+ name = name.replace(/\./, "\\.");
+ name = name.replace("*", ".*");
+ let rexp = new RegExp(name);
+
+ if (type === record.type && rexp.test(record.name)) {
+
+ logger.verbose("Matching recourd found", record, name, type);
+
+ let { timestamps, _id } = scope.items[i];
+ timestamps.announced = Date.now();
+
+ // update mdns item timestamps
+ await scope.update(_id, {
+ timestamps: {
+ ...timestamps
+ }
+ });
+
+ _matches.forEach((cb) => {
+ cb(record);
+ });
+
+ } else {
+
+ // Do nothing if nothing matches
+ //console.log(">> NON << MAchting record type");
+
+ }
+
+ });
+ });
+ } else {
+
+ logger.trace("Other packet type then response received", packet.type);
+
+ }
+
+ });
+
+ });
+};
\ No newline at end of file
diff --git a/components/mqtt/class.mqtt.js b/components/mqtt/class.mqtt.js
new file mode 100644
index 0000000..ecafe25
--- /dev/null
+++ b/components/mqtt/class.mqtt.js
@@ -0,0 +1,54 @@
+/**
+ * @description
+ * Represents a mqtt topic item
+ *
+ * @class MQTT
+ *
+ * @param {Object} obj Object that matches the item schema. See properties below:
+ *
+ * @property {String} _id MongoDB Object is as string
+ * @property {String} topic MQTT topic e.g. `air-sensor/sensor/particulate_matter_25m_concentration/state`
+ * @property {String} description Description for Admins/Topic
+ */
+class MQTT {
+
+ constructor(obj) {
+
+ Object.assign(this, obj);
+ this._id = String(obj._id);
+
+ Object.defineProperty(this, "_subscriber", {
+ value: [],
+ writable: false,
+ configurable: false,
+ enumerable: false
+ });
+
+ Object.defineProperty(this, "_publisher", {
+ value: () => { },
+ writable: true,
+ configurable: true,
+ enumerable: false
+ });
+
+ }
+
+ /**
+ * Subscribe to this topic
+ * @param {Function} cb Callback
+ */
+ subscribe(cb) {
+ this._subscriber.push(cb);
+ }
+
+ /**
+ * Publish data on this topic
+ * @param {*} data Payload
+ */
+ publish(data) {
+ this._publisher(data);
+ }
+
+}
+
+module.exports = MQTT;
\ No newline at end of file
diff --git a/components/mqtt/exit-codes.js b/components/mqtt/exit-codes.js
new file mode 100644
index 0000000..4637797
--- /dev/null
+++ b/components/mqtt/exit-codes.js
@@ -0,0 +1,61 @@
+// https://mosquitto.org/man/mosquitto_passwd-1.html#idm102
+
+module.exports = (version = 3) => {
+ if (version >= 5) {
+
+ return {
+ 0: "Success",
+ 128: "Unspecified error",
+ 129: "Malformed packet",
+ 130: "Protocol error",
+ 131: "Implementation specific error",
+ 132: "Unsupported protocol version",
+ 133: "Client ID not valid",
+ 134: "Bad username or password",
+ 135: "Not authorized",
+ 136: "Server unavailable",
+ 137: "Server busy",
+ 138: "Banned",
+ 139: "Server shutting down",
+ 140: "Bad authentication method",
+ 141: "Keep alive timeout",
+ 142: "Session taken over",
+ 143: "Topic filter invalid",
+ 144: "Topic name invalid",
+ 145: "", // unused
+ 146: "", // unused
+ 147: "Receive maximum exceeded",
+ 148: "Topic alias invalid",
+ 149: "Packet too large",
+ 150: "", // unused
+ 151: "Quota exceeded",
+ 152: "Administrative action",
+ 153: "Payload format invalid",
+ 154: "Retain not supported",
+ 155: "QoS not supported",
+ 156: "Use another server",
+ 157: "Server moved",
+ 158: "Shared subscriptions not supported",
+ 159: "Connection rate exceeded",
+ 160: "Maximum connect time",
+ 161: "Subscription IDs not supported",
+ 162: "Wildcard subscriptions not supported",
+ };
+
+ } else if (version >= 3) {
+
+ return {
+ 0: "Success",
+ 1: "Connection refused: Bad protocol version",
+ 2: "Connection refused: Identifier rejected",
+ 3: "Connection refused: Identifier rejected",
+ 4: "Connection refused: Bad username/password",
+ 5: "Connection refused: Not authorized"
+ };
+
+ } else {
+
+ throw new Error(`Unsupported protocol version "${version}"`);
+
+ }
+};
\ No newline at end of file
diff --git a/components/mqtt/index.js b/components/mqtt/index.js
new file mode 100644
index 0000000..10deecd
--- /dev/null
+++ b/components/mqtt/index.js
@@ -0,0 +1,106 @@
+const mongodb = require("mongodb");
+const Joi = require("joi");
+
+//const logger = require("../../system/logger").create("rooms");
+//const COMMON_COMPONENT = require("../../system/component/common.js");
+const COMPONENT = require("../../system/component/class.component.js");
+
+const MQTT = require("./class.mqtt.js");
+
+const messageHandler = require("./message-handler.js");
+//const packetHandler = require("./packet-handler.js");
+
+/**
+ * @description
+ * Receives MQTT messages from broker.
+ * It can publish and subscribe to topics.
+ * The emitted events are a mix from mqtt & custom ones.
+ *
+ * NOTE: Currenlty no authentication is possible.
+ *
+ * @class C_MQTT
+ * @extends COMPONENT system/component/class.component.js
+ *
+ * @emits message Received message over websocket/tcp connection; Arguments: [0] = tcp message
+ * @emits connect
+ * @emits connack
+ * @emits subscribe
+ * @emits suback
+ * @emits unsubscribe
+ * @emits unsuback
+ * @emits publish Something was published; Arguments: [0] = payload (buffer); [1] = parsed packet
+ * @emits puback
+ * @emits pubrec
+ * @emits pubrel
+ * @emits pubcomp
+ * @emits pingreq Ping request
+ * @emits pingresp Ping response
+ * @emits disconnect
+ * @emits auth
+ * @emits connected When websocket connected; Arguments: [0] = WebSocket client object "ws"
+ * @emits disconnected When websocket disconnected; Arguments: [0] = WebSocket client object "ws"
+ *
+ * @link router.api.mqtt.js routes/router.api.mqtt.js
+ * @see https://en.wikipedia.org/wiki/MQTT
+ * @see https://www.npmjs.com/package/mqtt-packet
+ */
+class C_MQTT extends COMPONENT {
+
+ constructor() {
+
+ // inject logger, collection and schema object
+ super("mqtt", {
+ _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => {
+ return String(new mongodb.ObjectId());
+ }),
+ topic: Joi.string().required(),
+ description: Joi.string().allow(null).default(null)
+ }, module);
+
+ this.hooks.post("add", (data, next) => {
+ next(null, new MQTT(data));
+ });
+
+ this.collection.createIndex({
+ topic: 1
+ }, {
+ unique: true
+ });
+
+ // handle incoming messages
+ // triggers registerd callback for mdns items
+ messageHandler(this);
+ //packetHandler(this);
+
+ }
+
+}
+
+
+// create component instance
+const instance = module.exports = new C_MQTT();
+
+
+// init component
+// set items/build cache
+instance.init((scope, ready) => {
+ scope.collection.find({}).toArray((err, data) => {
+ if (err) {
+
+ // shit...
+ ready(err);
+
+ } else {
+
+ data = data.map((obj) => {
+ return new MQTT(obj);
+ });
+
+ scope.items.push(...data);
+
+ // init done
+ ready(null);
+
+ }
+ });
+});
\ No newline at end of file
diff --git a/components/mqtt/message-handler.js b/components/mqtt/message-handler.js
new file mode 100644
index 0000000..7800041
--- /dev/null
+++ b/components/mqtt/message-handler.js
@@ -0,0 +1,151 @@
+const crypto = require("crypto");
+const mqtt = require("mqtt-packet");
+
+const VERSION = Number(process.env.MQTT_BROKER_VERSION);
+
+const parser = mqtt.parser({
+ protocolVersion: VERSION
+});
+
+const exitCodes = require("./exit-codes.js")(VERSION);
+
+module.exports = (scope) => {
+ scope._ready(({ logger, events }) => {
+
+ // ping timer
+ let interval = null;
+
+ events.on("publish", (packet) => {
+ scope.items.forEach(({ topic, _subscriber }) => {
+
+ if (String(packet.topic).startsWith(topic) || packet.topic === topic) {
+ _subscriber.forEach((cb) => {
+ cb(packet.payload, packet);
+ });
+ }
+
+ });
+ });
+
+
+ events.on("connected", (ws) => {
+
+ logger.debug("TCP socket connected to broker");
+
+ events.once("disconnected", () => {
+ clearInterval(interval);
+ logger.trace("Ping interval cleared");
+ });
+
+ // TODO make this object configurable
+ let data = mqtt.generate({
+ cmd: "connect",
+ protocolId: "MQTT", // Or "MQIsdp" in MQTT 3.1 and 5.0
+ protocolVersion: VERSION, // Or 3 in MQTT 3.1, or 5 in MQTT 5.0
+ clean: true, // Can also be false
+ clientId: process.env.MQTT_CLIENT_ID,
+ keepalive: 10, // Seconds which can be any positive number, with 0 as the default setting
+ /*
+ will: {
+ topic: "mydevice/test",
+ payload: Buffer.from("2134f"), // Payloads are buffers
+
+ }
+ */
+ });
+
+ ws.send(data);
+
+ events.once("connack", (packet) => {
+ if (packet.returnCode === 0) {
+
+ events.once("suback", () => {
+
+ logger.debug("Subscribed to topic #");
+
+ let ping = mqtt.generate({
+ cmd: "pingreq"
+ });
+
+ interval = setInterval(() => {
+ ws.send(ping);
+ }, Number(process.env.MQTT_PING_INTERVAL));
+
+ // monkey patch publisher function
+ scope.items.forEach((item) => {
+ item._publisher = (payload) => {
+
+ scope.logger.verbose(`Publish on topic ${item.topic}`, payload);
+
+ let pub = mqtt.generate({
+ cmd: "publish",
+ messageId: crypto.randomInt(0, 65535),
+ qos: 0,
+ dup: false,
+ topic: item.topic,
+ payload: Buffer.from(`${payload}`),
+ retain: false
+ });
+
+ ws.send(pub);
+
+ };
+ });
+
+ });
+
+ let sub = mqtt.generate({
+ cmd: "subscribe",
+ messageId: crypto.randomInt(0, 65535),
+ /*
+ properties: { // MQTT 5.0 properties
+ subscriptionIdentifier: 145,
+ userProperties: {
+ test: "shellies"
+ }
+ },
+ */
+ subscriptions: [{
+ topic: "#",
+ qos: 0,
+ nl: false, // no Local MQTT 5.0 flag
+ rap: true, // Retain as Published MQTT 5.0 flag
+ rh: 1 // Retain Handling MQTT 5.0
+ }]
+ });
+
+ ws.send(sub);
+
+ }
+ });
+
+ });
+
+
+ parser.on("packet", (packet) => {
+
+ logger.verbose("Packet received", packet);
+
+ if (packet.cmd === "connack") {
+ if (packet.returnCode == 0) {
+
+ logger.debug("Connected to broker");
+
+ } else {
+
+ logger.warn(`Could not connecto to broker: "${exitCodes[packet.returnCode]}"`);
+
+ }
+ }
+
+ events.emit(packet.cmd, packet);
+
+ });
+
+
+ events.on("message", (message) => {
+ parser.parse(message);
+ });
+
+ });
+};
\ No newline at end of file
diff --git a/components/plugins/class.plugin.js b/components/plugins/class.plugin.js
index 335413b..1b2129e 100644
--- a/components/plugins/class.plugin.js
+++ b/components/plugins/class.plugin.js
@@ -29,10 +29,10 @@ class Plugin {
}
/**
- * @function boot
- * Start/boot installed plugin
+ * @function start
+ * Start installed plugin
*/
- boot() {
+ start() {
if (this.enabled) {
let plugin = path.resolve(process.cwd(), "plugins", this.uuid);
@@ -116,6 +116,13 @@ class Plugin {
}
}
+ /*
+ stop(){
+ // TODO: Implement
+ // When plugins run in seperate worker process
+ }
+ */
+
}
module.exports = Plugin;
\ No newline at end of file
diff --git a/components/plugins/index.js b/components/plugins/index.js
index bbc0538..77153bf 100644
--- a/components/plugins/index.js
+++ b/components/plugins/index.js
@@ -47,7 +47,7 @@ class C_PLUGINS extends COMPONENT {
//runlevel: Joi.number().min(0).max(2).default(0),
autostart: Joi.boolean().default(true),
enabled: Joi.boolean().default(true),
- intents: Joi.array().items("devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", "vault").required()
+ intents: Joi.array().items("devices", "endpoints", "plugins", "rooms", "ssdp", "store", "users", "vault", "mqtt", "mdns", "webhooks").required()
}, module);
this.hooks.post("add", (data, next) => {
diff --git a/components/webhooks/class.webhook.js b/components/webhooks/class.webhook.js
new file mode 100644
index 0000000..331c156
--- /dev/null
+++ b/components/webhooks/class.webhook.js
@@ -0,0 +1,45 @@
+/**
+ * @description
+ * Represents a webhook item
+ *
+ * @class Webhook
+ *
+ * @param {Object} obj Object that matches the item schema. See properties below:
+ *
+ * @property {String} _id MongoDB Object is as string
+ * @property {String} name Webhook name
+ */
+module.exports = class Webhook {
+
+ constructor(obj) {
+
+ Object.assign(this, obj);
+ this._id = String(obj._id);
+
+ Object.defineProperty(this, "_handler", {
+ value: [],
+ configurable: false,
+ enumerable: false,
+ writable: false
+ });
+
+ Object.defineProperty(this, "_trigger", {
+ value: (body, query) => {
+
+ this._handler.forEach((cb) => {
+ cb(body, query);
+ });
+
+ },
+ enumerable: false,
+ configurable: false,
+ writable: false
+ });
+
+ }
+
+ handle(cb) {
+ this._handler.push(cb);
+ }
+
+};
\ No newline at end of file
diff --git a/components/webhooks/index.js b/components/webhooks/index.js
new file mode 100644
index 0000000..b5f6b10
--- /dev/null
+++ b/components/webhooks/index.js
@@ -0,0 +1,62 @@
+const mongodb = require("mongodb");
+const Joi = require("joi");
+
+//const logger = require("../../system/logger").create("rooms");
+//const COMMON_COMPONENT = require("../../system/component/common.js");
+const COMPONENT = require("../../system/component/class.component.js");
+
+const Webhook = require("./class.webhook.js");
+
+/**
+ * @description
+ * Implement webhook functionality
+ *
+ * @class C_WEBHOOKS
+ * @extends COMPONENT system/component/class.component.js
+ */
+class C_WEBHOOKS extends COMPONENT {
+ constructor() {
+
+ // inject logger, collection and schema object
+ super("webhooks", {
+ _id: Joi.string().pattern(/^[0-9a-fA-F]{24}$/).default(() => {
+ return String(new mongodb.ObjectId());
+ }),
+ name: Joi.string().required()
+ }, module);
+
+ this.hooks.post("add", (data, next) => {
+ next(null, new Webhook(data));
+ });
+
+ }
+}
+
+
+// create component instance
+const instance = module.exports = new C_WEBHOOKS();
+
+
+// init component
+// set items/build cache
+instance.init((scope, ready) => {
+ scope.collection.find({}).toArray((err, data) => {
+ if (err) {
+
+ // shit...
+ ready(err);
+
+ } else {
+
+ data = data.map((obj) => {
+ return new Webhook(obj);
+ });
+
+ scope.items.push(...data);
+
+ // init done
+ ready(null);
+
+ }
+ });
+});
\ No newline at end of file
diff --git a/helper/merge.js b/helper/merge.js
index f816857..bfe03dd 100644
--- a/helper/merge.js
+++ b/helper/merge.js
@@ -18,9 +18,16 @@ function merge(dst, src) {
let sourceArray = dst[key] || [];
res[key] = cur.map((valObj, index) => {
+
+ // fix #295
+ if (!(typeof valObj === "object")) {
+ return valObj;
+ }
+
// this is simply merging on index, but if you wanted a "smarter" merge, you could look up
// the dst by a specific key with sourceArray.find(...)
return merge(sourceArray[index] || {}, valObj);
+
});
} else if (typeof cur === "object" && cur !== null) {
diff --git a/index.js b/index.js
index 8109e2e..4a53e58 100644
--- a/index.js
+++ b/index.js
@@ -56,7 +56,10 @@ process.env = Object.assign({
VAULT_SALT_BYTE_LEN: "16",
USERS_BCRYPT_SALT_ROUNDS: "12",
USERS_JWT_SECRET: "",
- USERS_JWT_ALGORITHM: "HS384"
+ USERS_JWT_ALGORITHM: "HS384",
+ MQTT_BROKER_VERSION: "3",
+ MQTT_CLIENT_ID: "OpenHaus",
+ MQTT_PING_INTERVAL: "5000"
}, env.parsed, process.env);
@@ -201,7 +204,10 @@ const init_components = () => {
"ssdp",
"store",
"users",
- "vault"
+ "vault",
+ "webhooks",
+ "mqtt",
+ "mdns"
].sort(() => {
// pseudo randomize start/init of components
@@ -461,7 +467,7 @@ const starter = new Promise((resolve) => {
logger.verbose(`Start plugin "${plugin.name}" (${plugin.uuid})`);
- plugin.boot();
+ plugin.start();
started += 1;
@@ -480,4 +486,4 @@ const starter = new Promise((resolve) => {
logger.info("Startup complete");
-});
\ No newline at end of file
+});
diff --git a/package-lock.json b/package-lock.json
index 5b1e434..7c675f4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "backend",
- "version": "1.0.0",
+ "version": "2.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "backend",
- "version": "1.0.0",
+ "version": "2.0.0",
"hasInstallScript": true,
"license": "ISC",
"dependencies": {
@@ -14,11 +14,13 @@
"body-parser": "^1.20.1",
"colors": "^1.4.0",
"dateformat": "^4.6.3",
+ "dns-packet": "^5.4.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"joi": "^17.6.4",
"jsonwebtoken": "^9.0.0",
"mongodb": "^4.11.0",
+ "mqtt-packet": "^8.1.2",
"uuid": "^9.0.0",
"ws": "^8.10.0"
},
@@ -1160,6 +1162,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
+ "node_modules/@leichtgewicht/ip-codec": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
+ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
+ },
"node_modules/@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
@@ -1249,9 +1256,9 @@
}
},
"node_modules/@sideway/formula": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
- "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"node_modules/@sideway/pinpoint": {
"version": "2.0.0",
@@ -1691,6 +1698,39 @@
"node": ">=8"
}
},
+ "node_modules/bl": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "dependencies": {
+ "buffer": "^6.0.3",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ }
+ },
+ "node_modules/bl/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
"node_modules/bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
@@ -2259,6 +2299,17 @@
"node": ">=8"
}
},
+ "node_modules/dns-packet": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz",
+ "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==",
+ "dependencies": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -4683,6 +4734,37 @@
"node": ">=12"
}
},
+ "node_modules/mqtt-packet": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz",
+ "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==",
+ "dependencies": {
+ "bl": "^5.0.0",
+ "debug": "^4.1.1",
+ "process-nextick-args": "^2.0.1"
+ }
+ },
+ "node_modules/mqtt-packet/node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mqtt-packet/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -5588,8 +5670,7 @@
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
@@ -7726,6 +7807,11 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true
},
+ "@leichtgewicht/ip-codec": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz",
+ "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A=="
+ },
"@mapbox/node-pre-gyp": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz",
@@ -7797,9 +7883,9 @@
}
},
"@sideway/formula": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
- "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg=="
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
},
"@sideway/pinpoint": {
"version": "2.0.0",
@@ -8156,6 +8242,27 @@
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
"dev": true
},
+ "bl": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz",
+ "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==",
+ "requires": {
+ "buffer": "^6.0.3",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.4.0"
+ },
+ "dependencies": {
+ "buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "requires": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ }
+ }
+ },
"bluebird": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-2.11.0.tgz",
@@ -8574,6 +8681,14 @@
"path-type": "^4.0.0"
}
},
+ "dns-packet": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz",
+ "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==",
+ "requires": {
+ "@leichtgewicht/ip-codec": "^2.0.1"
+ }
+ },
"doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -10447,6 +10562,31 @@
}
}
},
+ "mqtt-packet": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz",
+ "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==",
+ "requires": {
+ "bl": "^5.0.0",
+ "debug": "^4.1.1",
+ "process-nextick-args": "^2.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "requires": {
+ "ms": "2.1.2"
+ }
+ },
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@@ -11139,8 +11279,7 @@
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
- "dev": true
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"proxy-addr": {
"version": "2.0.7",
diff --git a/package.json b/package.json
index d769021..8834647 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "backend",
"private": true,
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "",
"main": "index.js",
"scripts": {
@@ -18,7 +18,8 @@
"release": "grunt release",
"db:export": "mongodump --db OpenHaus --archive=./demo-database.gz",
"db:import": "mongorestore --db OpenHaus --archive=./demo-database.gz",
- "postinstall": "scripts/post-install.sh"
+ "postinstall": "scripts/post-install.sh",
+ "publish": "grunt publish"
},
"engines": {
"node": ">=0.16.0"
@@ -30,11 +31,13 @@
"body-parser": "^1.20.1",
"colors": "^1.4.0",
"dateformat": "^4.6.3",
+ "dns-packet": "^5.4.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"joi": "^17.6.4",
"jsonwebtoken": "^9.0.0",
"mongodb": "^4.11.0",
+ "mqtt-packet": "^8.1.2",
"uuid": "^9.0.0",
"ws": "^8.10.0"
},
diff --git a/postman.json b/postman.json
index 327ead9..76e6e11 100644
--- a/postman.json
+++ b/postman.json
@@ -1,6 +1,6 @@
{
"info": {
- "_postman_id": "7c990139-b42a-4603-9c76-841a6cda01aa",
+ "_postman_id": "87196fff-8d06-44e2-a0ce-626fa3a54a1b",
"name": "OpenHaus",
"description": "SmartHome/IoT application",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
@@ -328,6 +328,978 @@
}
]
},
+ {
+ "name": "Webhooks",
+ "item": [
+ {
+ "name": "Create new webhook",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test(\"Check room name: input = output\", () => {",
+ "",
+ " let res = pm.response.json();",
+ " let req = JSON.parse(pm.request.body);",
+ "",
+ " pm.expect(res.name).to.eql(req.name);",
+ "",
+ "});",
+ "",
+ "pm.test(\"Check properties\", () => {",
+ "",
+ " let res = pm.response.json();",
+ "",
+ " let props = [",
+ " \"name\", \"timestamps\", \"_id\",",
+ " \"number\", \"floor\", \"icon\"",
+ " ];",
+ "",
+ " Object.keys(res).forEach((key) => {",
+ " pm.expect(props.includes(key)).to.be.true;",
+ " });",
+ "",
+ " props.forEach((item) => {",
+ " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;",
+ " });",
+ "",
+ "})"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"Triggerd from Shelly i3\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all webhooks",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"The response has all properties\", () => {",
+ " let json = pm.response.json();",
+ " pm.expect(json).to.have.lengthOf(json.length);",
+ "});",
+ "",
+ "pm.test(\"Status code is 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "console.log(\"Fooo\")"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "x-auth-token",
+ "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e",
+ "type": "text",
+ "disabled": true
+ }
+ ],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get sinlge webhook",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks",
+ "63e9fe420d172b3b39313175"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update existing webhook",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "const HOST = pm.collectionVariables.get(\"HOST\");",
+ "const PORT = pm.collectionVariables.get(\"PORT\");",
+ "",
+ "//console.log(pm.request.url.toString())",
+ "",
+ "pm.sendRequest({",
+ " url: `http://${HOST}:${PORT}/api/rooms/`,",
+ " method: 'GET',",
+ "}, function (err, res) {",
+ " if(err){",
+ "",
+ " consle.error(err);",
+ "",
+ " }else {",
+ "",
+ " let data = res.json();",
+ " let key = Math.floor(Math.random()*data.length);",
+ " let item = data[key];",
+ "",
+ " pm.variables.set(\"_id\", item._id);",
+ "",
+ " }",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"Garage\",\n \"icon\": \"fa-solid fa-warehouse\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks",
+ "63e9fe420d172b3b39313175"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete exisiting room",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));"
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));",
+ "",
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks",
+ "63e9fe420d172b3b39313175"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Trigger webhook",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"and\": \"also\",\n \"json\": \"data\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/webhooks/63e9fe420d172b3b39313175/trigger?optional=query¶meter=can&be=given",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "webhooks",
+ "63e9fe420d172b3b39313175",
+ "trigger"
+ ],
+ "query": [
+ {
+ "key": "optional",
+ "value": "query"
+ },
+ {
+ "key": "parameter",
+ "value": "can"
+ },
+ {
+ "key": "be",
+ "value": "given"
+ }
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "MDNS",
+ "item": [
+ {
+ "name": "Create mdns entry",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test(\"Check room name: input = output\", () => {",
+ "",
+ " let res = pm.response.json();",
+ " let req = JSON.parse(pm.request.body);",
+ "",
+ " pm.expect(res.name).to.eql(req.name);",
+ "",
+ "});",
+ "",
+ "pm.test(\"Check properties\", () => {",
+ "",
+ " let res = pm.response.json();",
+ "",
+ " let props = [",
+ " \"name\", \"timestamps\", \"_id\",",
+ " \"number\", \"floor\", \"icon\"",
+ " ];",
+ "",
+ " Object.keys(res).forEach((key) => {",
+ " pm.expect(props.includes(key)).to.be.true;",
+ " });",
+ "",
+ " props.forEach((item) => {",
+ " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;",
+ " });",
+ "",
+ "})"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"name\": \"shelly*.local\",\n \"type\": \"A\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mdns",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mdns"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all mdns targets",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"The response has all properties\", () => {",
+ " let json = pm.response.json();",
+ " pm.expect(json).to.have.lengthOf(json.length);",
+ "});",
+ "",
+ "pm.test(\"Status code is 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "console.log(\"Fooo\")"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "x-auth-token",
+ "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e",
+ "type": "text",
+ "disabled": true
+ }
+ ],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mdns",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mdns"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get sinlge mdns target",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mdns",
+ "63e7f7ba26b161df7f3af1d6"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update mdns target",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "const HOST = pm.collectionVariables.get(\"HOST\");",
+ "const PORT = pm.collectionVariables.get(\"PORT\");",
+ "",
+ "//console.log(pm.request.url.toString())",
+ "",
+ "pm.sendRequest({",
+ " url: `http://${HOST}:${PORT}/api/rooms/`,",
+ " method: 'GET',",
+ "}, function (err, res) {",
+ " if(err){",
+ "",
+ " consle.error(err);",
+ "",
+ " }else {",
+ "",
+ " let data = res.json();",
+ " let key = Math.floor(Math.random()*data.length);",
+ " let item = data[key];",
+ "",
+ " pm.variables.set(\"_id\", item._id);",
+ "",
+ " }",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"labels\": [\n \"manufacturer=shelly\"\n ]\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mdns",
+ "63e7f7ba26b161df7f3af1d6"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete mdns target",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));"
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));",
+ "",
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mdns/63e7f7ba26b161df7f3af1d6",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mdns",
+ "63e7f7ba26b161df7f3af1d6"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+ },
+ {
+ "name": "MQTT",
+ "item": [
+ {
+ "name": "Create mqtt topic",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "pm.test(\"Check room name: input = output\", () => {",
+ "",
+ " let res = pm.response.json();",
+ " let req = JSON.parse(pm.request.body);",
+ "",
+ " pm.expect(res.name).to.eql(req.name);",
+ "",
+ "});",
+ "",
+ "pm.test(\"Check properties\", () => {",
+ "",
+ " let res = pm.response.json();",
+ "",
+ " let props = [",
+ " \"name\", \"timestamps\", \"_id\",",
+ " \"number\", \"floor\", \"icon\"",
+ " ];",
+ "",
+ " Object.keys(res).forEach((key) => {",
+ " pm.expect(props.includes(key)).to.be.true;",
+ " });",
+ "",
+ " props.forEach((item) => {",
+ " pm.expect(Object.prototype.hasOwnProperty.call(res, item)).to.be.true;",
+ " });",
+ "",
+ "})"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"topic\": \"air-sensor/sensor/particulate_matter_25m_concentration\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mqtt",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mqtt"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get all mqtt topics",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "pm.test(\"The response has all properties\", () => {",
+ " let json = pm.response.json();",
+ " pm.expect(json).to.have.lengthOf(json.length);",
+ "});",
+ "",
+ "pm.test(\"Status code is 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});",
+ "",
+ "console.log(\"Fooo\")"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [
+ {
+ "key": "x-auth-token",
+ "value": "eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImhhbnMuaHViZXJ0QGV4YW1wbGUuY29tIiwidXVpZCI6ImM3N2E3NjJkLWM4ODYtNGQ2My1iNGM1LWU0MDJhZGNmYTdiZSIsImlhdCI6MTY1NDI2ODI4NX0.w4mkvTuJ-OXzTcmvWhwIT84oOmo2399hSEfWGbA-9SUWndMWUiHvly1A7-kSV93e",
+ "type": "text",
+ "disabled": true
+ }
+ ],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mqtt",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mqtt"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Get sinlge mqtt topic",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ ""
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mqtt",
+ "63e8f7d2ab413a9760e9b08c"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Update mqtt topic",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "const HOST = pm.collectionVariables.get(\"HOST\");",
+ "const PORT = pm.collectionVariables.get(\"PORT\");",
+ "",
+ "//console.log(pm.request.url.toString())",
+ "",
+ "pm.sendRequest({",
+ " url: `http://${HOST}:${PORT}/api/rooms/`,",
+ " method: 'GET',",
+ "}, function (err, res) {",
+ " if(err){",
+ "",
+ " consle.error(err);",
+ "",
+ " }else {",
+ "",
+ " let data = res.json();",
+ " let key = Math.floor(Math.random()*data.length);",
+ " let item = data[key];",
+ "",
+ " pm.variables.set(\"_id\", item._id);",
+ "",
+ " }",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "PATCH",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"description\": \"Ikea VINDRIKTNING MQTT modd\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mqtt",
+ "63e8f7d2ab413a9760e9b08c"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "Delete mqtt topic",
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));"
+ ],
+ "type": "text/javascript"
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "exec": [
+ "console.log(\"_id varaible\", pm.variables.get(\"_id\"));",
+ "",
+ "pm.test(\"status code: 200\", () => {",
+ " pm.response.to.have.status(200);",
+ "});"
+ ],
+ "type": "text/javascript"
+ }
+ }
+ ],
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://{{HOST}}:{{PORT}}/api/mqtt/63e8f7d2ab413a9760e9b08c",
+ "protocol": "http",
+ "host": [
+ "{{HOST}}"
+ ],
+ "port": "{{PORT}}",
+ "path": [
+ "api",
+ "mqtt",
+ "63e8f7d2ab413a9760e9b08c"
+ ]
+ }
+ },
+ "response": []
+ }
+ ],
+ "event": [
+ {
+ "listen": "prerequest",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ },
+ {
+ "listen": "test",
+ "script": {
+ "type": "text/javascript",
+ "exec": [
+ ""
+ ]
+ }
+ }
+ ]
+ },
{
"name": "SSDP",
"item": [
@@ -1309,7 +2281,7 @@
"header": [],
"body": {
"mode": "raw",
- "raw": "{\n \"name\": \"AV - Receiver\"\n}",
+ "raw": " {\n \"_id\": \"63a5a4c2bd5fe7cb165960d0\",\n \"name\": \"Fernseher\",\n \"device\": \"63a5a4c2bd5fe7cb165960cd\",\n \"commands\": [\n {\n \"payload\": \"KEY_0\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_0\",\n \"name\": \"KEY_0\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_1\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_1\",\n \"name\": \"KEY_1\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_2\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_2\",\n \"name\": \"KEY_2\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_3\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_3\",\n \"name\": \"KEY_3\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_4\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_4\",\n \"name\": \"KEY_4\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_5\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_5\",\n \"name\": \"KEY_5\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_6\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_6\",\n \"name\": \"KEY_6\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_7\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_7\",\n \"name\": \"KEY_7\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_8\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_8\",\n \"name\": \"KEY_8\",\n \"_id\": \"63a5a4c2bd5fe7cb165960d9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_9\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_9\",\n \"name\": \"KEY_9\",\n \"_id\": \"63a5a4c2bd5fe7cb165960da\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_10\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_10\",\n \"name\": \"KEY_10\",\n \"_id\": \"63a5a4c2bd5fe7cb165960db\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_11\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_11\",\n \"name\": \"KEY_11\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dc\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_12\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_12\",\n \"name\": \"KEY_12\",\n \"_id\": \"63a5a4c2bd5fe7cb165960dd\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHDOWN\",\n \"name\": \"KEY_CHDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960de\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_CHUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_CHUP\",\n \"name\": \"KEY_CHUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960df\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLDOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLDOWN\",\n \"name\": \"KEY_VOLDOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e0\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_VOLUP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_VOLUP\",\n \"name\": \"KEY_VOLUP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e1\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MUTE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MUTE\",\n \"name\": \"KEY_MUTE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e2\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_SOURCE\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_SOURCE\",\n \"name\": \"KEY_SOURCE\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e3\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_LEFT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_LEFT\",\n \"name\": \"KEY_LEFT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e4\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_RIGHT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_RIGHT\",\n \"name\": \"KEY_RIGHT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e5\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_UP\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_UP\",\n \"name\": \"KEY_UP\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e6\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_DOWN\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_DOWN\",\n \"name\": \"KEY_DOWN\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e7\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_ENTER\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_ENTER\",\n \"name\": \"KEY_ENTER\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e8\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_MENU\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_MENU\",\n \"name\": \"KEY_MENU\",\n \"_id\": \"63a5a4c2bd5fe7cb165960e9\",\n \"identifier\": null,\n \"description\": null\n },\n {\n \"payload\": \"KEY_EXIT\",\n \"interface\": \"63a5a4c2bd5fe7cb165960ce\",\n \"alias\": \"KEY_EXIT\",\n \"name\": \"KEY_EXIT\",\n \"_id\": \"63a5a4c2bd5fe7cb165960ea\",\n \"identifier\": null,\n \"description\": null\n }\n ],\n \"timestamps\": {\n \"created\": 1671800002321,\n \"updated\": null\n },\n \"enabled\": true,\n \"room\": \"62a4bbf0d9256b5e8d69889c\",\n \"states\": [],\n \"identifier\": null,\n \"icon\": \"fa-solid fa-tv\"\n }",
"options": {
"raw": {
"language": "json"
@@ -1317,7 +2289,7 @@
}
},
"url": {
- "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/6266c441e207ba2a3e3c9222",
+ "raw": "http://{{HOST}}:{{PORT}}/api/endpoints/63a5a4c2bd5fe7cb165960d0",
"protocol": "http",
"host": [
"{{HOST}}"
@@ -1326,7 +2298,7 @@
"path": [
"api",
"endpoints",
- "6266c441e207ba2a3e3c9222"
+ "63a5a4c2bd5fe7cb165960d0"
]
}
},
@@ -2001,7 +2973,9 @@
"request": {
"method": "GET",
"header": [],
- "url": null
+ "url": {
+ "raw": ""
+ }
},
"response": []
}
diff --git a/routes/index.js b/routes/index.js
index 33129a5..cde8e32 100644
--- a/routes/index.js
+++ b/routes/index.js
@@ -10,6 +10,9 @@ const C_VAULT = require("../components/vault");
const C_SSDP = require("../components/ssdp");
const C_STORE = require("../components/store");
const C_USERS = require("../components/users");
+const C_WEBHOOKS = require("../components/webhooks");
+const C_MQTT = require("../components/mqtt");
+const C_MDNS = require("../components/mdns");
// Remove due to issue #273
//const { encode } = require("../helper/sanitize");
@@ -127,6 +130,9 @@ module.exports = (server) => {
const ssdpRouter = express.Router();
const storeRouter = express.Router();
const usersRouter = express.Router();
+ const webhooksRouter = express.Router();
+ const mqttRouter = express.Router();
+ const mdnsRouter = express.Router();
// http://127.0.0.1/api/plugins
api.use("/plugins", pluginsRouter);
@@ -176,6 +182,21 @@ module.exports = (server) => {
require("./rest-handler.js")(C_USERS, usersRouter);
//require("./router.api.users.js")(app, vaultRouter);
+ // http://127.0.0.1/api/webhooks
+ api.use("/webhooks", webhooksRouter);
+ require("./router.api.webhooks.js")(app, webhooksRouter);
+ require("./rest-handler.js")(C_WEBHOOKS, webhooksRouter);
+
+ // http://127.0.0.1/api/mqtt
+ api.use("/mqtt", mqttRouter);
+ require("./router.api.mqtt.js")(app, mqttRouter);
+ require("./rest-handler.js")(C_MQTT, mqttRouter);
+
+ // http://127.0.0.1/api/mdns
+ api.use("/mdns", mdnsRouter);
+ require("./router.api.mdns.js")(app, mdnsRouter);
+ require("./rest-handler.js")(C_MDNS, mdnsRouter);
+
// NOTE: Drop this?!
api.use((req, res) => {
res.status(404).end();
diff --git a/routes/router.api.mdns.js b/routes/router.api.mdns.js
new file mode 100644
index 0000000..a5895d3
--- /dev/null
+++ b/routes/router.api.mdns.js
@@ -0,0 +1,107 @@
+const { decode } = require("dns-packet");
+
+const C_MDNS = require("../components/mdns");
+
+// external modules
+const WebSocket = require("ws");
+
+module.exports = (app, router) => {
+
+ // websocket server
+ let wss = new WebSocket.Server({
+ noServer: true
+ });
+
+ // detect broken connections
+ let interval = setInterval(() => {
+ wss.clients.forEach((ws) => {
+
+ if (!ws.isAlive) {
+ ws.terminate();
+ return;
+ }
+
+ ws.isAlive = false;
+ ws.ping();
+
+ });
+ }, Number(process.env.API_WEBSOCKET_TIMEOUT));
+
+
+ // if the server closes
+ // clear the interval
+ wss.on("close", () => {
+ clearInterval(interval);
+ });
+
+
+ // http route handler
+ router.get("/", (req, res, next) => {
+
+ console.log("Request to /api/mdns");
+
+ // check if connection is a simple get request or ws client
+ if ((!req.headers["upgrade"] || !req.headers["connection"])) {
+ //return res.status(403).end();
+ next(); // let the rest-handler.js do its job
+ return;
+ }
+
+ // listen for websockt clients
+ // keep sending new log entrys to client
+ wss.once("connection", (ws) => {
+
+ console.log("Clien connected to mdns");
+
+ C_MDNS.events.emit("connected", ws);
+
+ ws.on("message", (msg) => {
+ C_MDNS.events.emit("message", decode(msg), msg);
+ });
+
+ ws.on("close", () => {
+ console.log("Client disconnected disolaskjdflaskjfdasdf");
+ });
+
+
+ // QUERY LOCAL DNS
+ /*
+ setInterval(() => {
+
+ console.log("Query for HTTP Server");
+
+ let msg = encode({
+ type: "query",
+ id: 1,
+ flags: RECURSION_DESIRED,
+ questions: [{
+ type: "A",
+ //name: '_http._tcp.local'
+ name: "*"
+ }]
+ });
+
+ ws.send(msg);
+
+ }, 30_000);
+ */
+
+ });
+
+ // handle request as websocket
+ // perform websocket handshake
+ wss.handleUpgrade(req, req.socket, req.headers, (ws) => {
+
+ ws.isAlive = true;
+
+ ws.on("pong", () => {
+ ws.isAlive = true;
+ });
+
+ wss.emit("connection", ws, req);
+
+ });
+
+ });
+
+};
\ No newline at end of file
diff --git a/routes/router.api.mqtt.js b/routes/router.api.mqtt.js
new file mode 100644
index 0000000..20a46a6
--- /dev/null
+++ b/routes/router.api.mqtt.js
@@ -0,0 +1,81 @@
+const C_MQTT = require("../components/mqtt");
+
+// external modules
+const WebSocket = require("ws");
+
+module.exports = (app, router) => {
+
+ // websocket server
+ let wss = new WebSocket.Server({
+ noServer: true
+ });
+
+ // detect broken connections
+ let interval = setInterval(() => {
+ wss.clients.forEach((ws) => {
+
+ if (!ws.isAlive) {
+ ws.terminate();
+ return;
+ }
+
+ ws.isAlive = false;
+ ws.ping();
+
+ });
+ }, Number(process.env.API_WEBSOCKET_TIMEOUT));
+
+
+ // if the server closes
+ // clear the interval
+ wss.on("close", () => {
+ clearInterval(interval);
+ });
+
+
+ // http route handler
+ router.get("/", (req, res, next) => {
+
+ console.log("Request to /ai/mqtt");
+
+ // check if connection is a simple get request or ws client
+ if ((!req.headers["upgrade"] || !req.headers["connection"])) {
+ //return res.status(403).end();
+ next(); // let the rest-handler.js do its job
+ return;
+ }
+
+ // listen for websockt clients
+ // keep sending new log entrys to client
+ wss.once("connection", (ws) => {
+
+ C_MQTT.events.emit("connected", ws);
+
+ ws.on("message", (msg) => {
+ C_MQTT.events.emit("message", msg);
+ });
+
+ ws.on("close", () => {
+ console.log("MQTT Client disconnected disolaskjdflaskjfdasdf");
+ C_MQTT.events.emit("disconnected", ws);
+ });
+
+ });
+
+ // handle request as websocket
+ // perform websocket handshake
+ wss.handleUpgrade(req, req.socket, req.headers, (ws) => {
+
+ ws.isAlive = true;
+
+ ws.on("pong", () => {
+ ws.isAlive = true;
+ });
+
+ wss.emit("connection", ws, req);
+
+ });
+
+ });
+
+};
\ No newline at end of file
diff --git a/routes/router.api.plugins.js b/routes/router.api.plugins.js
index 4609b1d..2b5c653 100644
--- a/routes/router.api.plugins.js
+++ b/routes/router.api.plugins.js
@@ -59,4 +59,32 @@ module.exports = (app, router) => {
}
});
+ router.post("/:_id/start", (req, res) => {
+ try {
+
+ req.item.start();
+ res.json(req.item);
+
+ } catch (err) {
+
+ res.status(500).end(err);
+
+ }
+ });
+
+ /*
+ router.post("/:_id/stop", (req, res) => {
+ try {
+
+ req.item.stop();
+ res.json(req.item);
+
+ } catch (err) {
+
+ res.status(500).end(err);
+
+ }
+ });
+ */
+
};
\ No newline at end of file
diff --git a/routes/router.api.webhooks.js b/routes/router.api.webhooks.js
new file mode 100644
index 0000000..a60171a
--- /dev/null
+++ b/routes/router.api.webhooks.js
@@ -0,0 +1,38 @@
+const C_WEBHOOKS = require("../components/webhooks");
+
+
+module.exports = (app, router) => {
+
+ router.param("_id", (req, res, next, _id) => {
+ C_WEBHOOKS.get(_id, (err, obj) => {
+ if (err) {
+
+ res.status(400).json({
+ error: err
+ });
+
+ } else {
+
+ if (!obj) {
+ return res.status(404).end();
+ }
+
+ req.item = obj;
+
+ next();
+
+ }
+ });
+ });
+
+ router.all("/:_id/trigger", (req, res) => {
+
+ //res.end(`Hello from webhook: ${req.method}, ${JSON.stringify(req.item)}`);
+
+ req.item._trigger(req.body, req.query);
+
+ res.status(202).end();
+
+ });
+
+};
\ No newline at end of file
diff --git a/system/component/class.component.js b/system/component/class.component.js
index 95f9f20..5abc169 100644
--- a/system/component/class.component.js
+++ b/system/component/class.component.js
@@ -17,6 +17,10 @@ const PENDING_CHANGE_EVENTS = new Set();
* - remove
* - find
*
+ * Methods that are not hookable:
+ * - found
+ * - labels
+ *
* @class COMPONENT
*
* @extends COMMON system/component/class.common.js
@@ -24,6 +28,7 @@ const PENDING_CHANGE_EVENTS = new Set();
* @property {Array} items Store where instance of items are keept
* @property {Object} collection MongoDB collection instance
* @property {Object} schema Joi Object schema which is extend by a timestamp object:
+ * @property {Array} schema.labels Array that allow custom labels for identification
* @property {Object} schema.timestamps Timestamps
* @property {Number} schema.timestamps.created Set to `Date.now()` when a item is created/added
* @property {Number} schema.timestamps.updated Set to `Date.now()` when a item is updated
@@ -92,6 +97,9 @@ module.exports = class COMPONENT extends COMMON {
this.schema = Joi.object({
...schema,
+ //labels: Joi.array().items(Joi.string().regex(/^[a-zA-Z0-9]+=[a-zA-Z0-9]+$/)).default([])
+ //labels: Joi.array().items(Joi.string().regex(/^[a-z0-9\.]+=[a-z0-9]+$/)).default([]),
+ labels: Joi.array().items(Joi.string().regex(/^[a-z0-9]+=[a-z0-9]+$/)).default([]),
timestamps: Joi.object({
...schema?.timestamps,
created: Joi.number().allow(null).default(null),
@@ -262,10 +270,17 @@ module.exports = class COMPONENT extends COMMON {
*/
this._defineMethod("add", (final) => {
+ let duplicate = false;
+
final((item) => {
- //this.items.push(item);
- items.push(item);
+
+ // Fix #294
+ if (!duplicate) {
+ items.push(item);
+ }
+
return Promise.resolve();
+
});
return (data) => {
@@ -316,6 +331,8 @@ module.exports = class COMPONENT extends COMMON {
*/
});
+ duplicate = !!item;
+
// remove id when error occurs
PENDING_CHANGE_EVENTS.delete(result.value._id);
@@ -555,12 +572,13 @@ module.exports = class COMPONENT extends COMMON {
*/
this._defineMethod("find", () => {
return (query) => {
- return new Promise((resolve, reject) => {
+ return new Promise((resolve) => {
// https://javascript.plainenglish.io/4-ways-to-compare-objects-in-javascript-97fe9b2a949c
// https://stackoverflow.com/a/1068883/5781499
// https://dmitripavlutin.com/how-to-compare-objects-in-javascript/
+ /*
let item = this.items.find((item) => {
// for (let key of query) { ?!
for (let key in Object.keys(query)) {
@@ -579,6 +597,28 @@ module.exports = class COMPONENT extends COMMON {
}
resolve([item]);
+ */
+
+ // fix #306
+ let item = this.items.find((item) => {
+
+ let loop = (target, filter) => {
+ return Object.keys(filter).every((key) => {
+
+ if (target[key] instanceof Object) {
+ return loop(target[key], filter[key]);
+ } else {
+ return target[key] === filter[key];
+ }
+
+ });
+ };
+
+ return loop(item, query);
+
+ });
+
+ resolve([item || null]);
});
};
@@ -600,6 +640,8 @@ module.exports = class COMPONENT extends COMMON {
* @param {Object} filter Object that matches the component schema
* @param {Function} cb Callback function
* @param {Function} [nothing] Function that is called when not matching item in array is found. Usefull to add then something, when its not found.
+ *
+ * @returns {Function} Cleanup. If you dont to get pdates/called again, call the "cleanup" function
*/
found(filter, cb, nothing) {
@@ -643,6 +685,32 @@ module.exports = class COMPONENT extends COMMON {
};
+
+ /*
+ // potential fix for #307
+ // the problem is not the method, but the wildcard in labels
+ let handler = (filter, item) => {
+
+ let loop = (target, filter) => {
+ return Object.keys(filter).every((key) => {
+
+ if (target[key] instanceof Object) {
+ return loop(target[key], filter[key]);
+ } else {
+ return target[key] === filter[key];
+ }
+
+ });
+ };
+
+ if (loop(item, filter)) {
+ matched = true;
+ cb(item);
+ }
+
+ };
+ */
+
this.items.forEach((item) => {
handler(filter, item);
});
@@ -653,7 +721,6 @@ module.exports = class COMPONENT extends COMMON {
});
}
- // NOTE: Why is a array from the add eventlistener returned?!
let ev = (item) => {
handler(filter, item);
};
@@ -676,6 +743,60 @@ module.exports = class COMPONENT extends COMMON {
}
+ /**
+ * @function _labels
+ * Checks if filter array contains matching labels
+ *
+ * @param {Array} arr Array with item labels to check with filter
+ * @param {Array} filter Filter array
+ *
+ * @returns {Boolean} true/false if filter matches label array
+ */
+ _labels(arr, filter) {
+ return filter.every((filter) => {
+ if (arr.includes(filter)) {
+
+ return true;
+
+ } else {
+
+ let [key, value] = filter.split("=");
+
+ return arr.some((label) => {
+
+ let [k, v] = label.split("=");
+
+ if (value === "*") {
+ return key === k;
+ }
+
+ if (key === "*") {
+ return value === v;
+ }
+
+ return false;
+
+ });
+
+ }
+ });
+ }
+
+
+ /**
+ * @function labels
+ * Retun all items that matches the givel label filter
+ *
+ * @param {Array} filter Filter for labels array
+ *
+ * @returns {Array} With all items that have matching labels
+ */
+ labels(filter) {
+ return this.items.filter(({ labels }) => {
+ return this._labels(labels || [], filter);
+ });
+ }
+
//
/*
_exportItemMethod(name, prop) {
diff --git a/system/logger/index.js b/system/logger/index.js
index bdb17cb..fedf82b 100644
--- a/system/logger/index.js
+++ b/system/logger/index.js
@@ -18,7 +18,9 @@ Object.defineProperty(logger, "create", {
value: function create(name) {
let file = path.resolve(process.env.LOG_PATH, `${name}.log`);
- let stream = createWriteStream(file);
+ let stream = createWriteStream(file, {
+ flags: "a"
+ });
stream.on("error", (err) => {
console.error(err);
@@ -51,8 +53,12 @@ Object.defineProperty(logger, "create", {
* ```
*/
-const system = createWriteStream(path.resolve(process.env.LOG_PATH, "system.log"));
-const combined = createWriteStream(path.resolve(process.env.LOG_PATH, "combined.log"));
+const system = createWriteStream(path.resolve(process.env.LOG_PATH, "system.log"), {
+ flags: "a"
+});
+const combined = createWriteStream(path.resolve(process.env.LOG_PATH, "combined.log"), {
+ flags: "a"
+});
[system, combined].forEach((stream) => {
stream.on("error", (err) => {
@@ -106,7 +112,9 @@ Object.defineProperty(logger, "create", {
});
}
- let stream = createWriteStream(file);
+ let stream = createWriteStream(file, {
+ flags: "a"
+ });
stream.on("error", (err) => {
console.error(err);
diff --git a/tests/components/index.js b/tests/components/index.js
index 1578c94..492d3ae 100644
--- a/tests/components/index.js
+++ b/tests/components/index.js
@@ -7,7 +7,7 @@ describe("Components", () => {
[
"devices", "endpoints", "plugins",
"rooms", "ssdp", "store", "users",
- "vault"
+ "vault", "webhooks", "mdns", "mqtt"
].forEach((name) => {
describe(name, () => {
diff --git a/tests/components/mdns.js b/tests/components/mdns.js
new file mode 100644
index 0000000..54dcf9f
--- /dev/null
+++ b/tests/components/mdns.js
@@ -0,0 +1,132 @@
+const assert = require("assert");
+const mongodb = require("mongodb");
+
+try {
+
+ const C_COMPONENT = require(`../../components/mdns/index.js`);
+ const MDNS = require("../../components/mdns/class.mdns.js");
+
+ const workflow = require("./test.workflow.js");
+
+ let _id = String(new mongodb.ObjectId());
+
+
+ workflow(C_COMPONENT, "add", (done, { event }) => {
+ C_COMPONENT.add({
+ _id,
+ type: "A",
+ name: "shelly*.local"
+ }, (err, item) => {
+ try {
+
+ // check event arguments
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof MDNS, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MDNS, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "get", (done) => {
+ C_COMPONENT.get(_id, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MDNS, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", (done) => {
+ C_COMPONENT.update(_id, {
+ type: "SRV"
+ }, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MDNS, true);
+ assert.equal(item.name, "shelly*.local");
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => {
+ Promise.all([
+
+ // update call 1
+ C_COMPONENT.update(_id, {
+ name: `shelly*_tcp.local`
+ }),
+
+ // update call 2
+ C_COMPONENT.update(_id, {
+ name: `shelly*_tcp.local`,
+ type: "A"
+ }),
+
+ ]).then(() => {
+
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof MDNS, true);
+ });
+
+ done();
+
+ }).catch(done);
+ });
+
+
+ workflow(C_COMPONENT, "remove", (done, { post }) => {
+ C_COMPONENT.remove(_id, (err, item) => {
+ try {
+
+ // check post arguments item instance
+ post.args.forEach((args) => {
+ assert.equal(args[0] instanceof MDNS, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MDNS, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+} catch (err) {
+ console.error(err);
+ process.exit(100);
+}
\ No newline at end of file
diff --git a/tests/components/mqtt.js b/tests/components/mqtt.js
new file mode 100644
index 0000000..84a6ef2
--- /dev/null
+++ b/tests/components/mqtt.js
@@ -0,0 +1,139 @@
+const assert = require("assert");
+const mongodb = require("mongodb");
+
+try {
+
+ const C_COMPONENT = require(`../../components/mqtt/index.js`);
+ const MQTT = require("../../components/mqtt/class.mqtt.js");
+
+ const workflow = require("./test.workflow.js");
+
+ let _id = String(new mongodb.ObjectId());
+
+
+ workflow(C_COMPONENT, "add", (done, { event }) => {
+ C_COMPONENT.add({
+ _id,
+ topic: "air-sensor/",
+ }, (err, item) => {
+ try {
+
+ // check event arguments
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof MQTT, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MQTT, true);
+ assert.ok(item._publisher);
+ assert.ok(item._subscriber);
+ assert.equal(item.subscribe instanceof Function, true);
+ assert.equal(item.publish instanceof Function, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "get", (done) => {
+ C_COMPONENT.get(_id, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MQTT, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", (done) => {
+ C_COMPONENT.update(_id, {
+ topic: "air-sensor/sensor/particulate_matter_25m_concentration/state"
+ }, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MQTT, true);
+ assert.equal(item.topic, "air-sensor/sensor/particulate_matter_25m_concentration/state");
+ assert.ok(item._publisher);
+ assert.ok(item._subscriber);
+ assert.equal(item.subscribe instanceof Function, true);
+ assert.equal(item.publish instanceof Function, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => {
+ Promise.all([
+
+ // update call 1
+ C_COMPONENT.update(_id, {
+ topic: `air-sensor/status`
+ }),
+
+ // update call 2
+ C_COMPONENT.update(_id, {
+ topic: `air-sensor/`,
+ description: "Ikea VINDRIKTNING Air sensor MQTT topic"
+ }),
+
+ ]).then(() => {
+
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof MQTT, true);
+ });
+
+ done();
+
+ }).catch(done);
+ });
+
+
+ workflow(C_COMPONENT, "remove", (done, { post }) => {
+ C_COMPONENT.remove(_id, (err, item) => {
+ try {
+
+ // check post arguments item instance
+ post.args.forEach((args) => {
+ assert.equal(args[0] instanceof MQTT, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof MQTT, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+} catch (err) {
+ console.error(err);
+ process.exit(100);
+}
\ No newline at end of file
diff --git a/tests/components/webhooks.js b/tests/components/webhooks.js
new file mode 100644
index 0000000..d9173b1
--- /dev/null
+++ b/tests/components/webhooks.js
@@ -0,0 +1,134 @@
+const assert = require("assert");
+const mongodb = require("mongodb");
+
+try {
+
+ const C_COMPONENT = require(`../../components/webhooks/index.js`);
+ const Webhook = require("../../components/webhooks/class.webhook.js");
+
+ const workflow = require("./test.workflow.js");
+
+ let _id = String(new mongodb.ObjectId());
+
+
+ workflow(C_COMPONENT, "add", (done, { event }) => {
+ C_COMPONENT.add({
+ _id,
+ name: "Webhook #1",
+ }, (err, item) => {
+ try {
+
+ // check event arguments
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof Webhook, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof Webhook, true);
+ assert.equal(item._handler instanceof Array, true);
+ assert.equal(item._trigger instanceof Function, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "get", (done) => {
+ C_COMPONENT.get(_id, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof Webhook, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", (done) => {
+ C_COMPONENT.update(_id, {
+ name: "Webhook #1 - updated",
+ }, (err, item) => {
+ try {
+
+ assert.ok(err === null);
+ assert.equal(item instanceof Webhook, true);
+ assert.equal(item._handler instanceof Array, true);
+ assert.equal(item._trigger instanceof Function, true);
+ assert.equal(item.name, "Webhook #1 - updated");
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+ workflow(C_COMPONENT, "update", "Double update result / event arguments check", (done, { event }) => {
+ Promise.all([
+
+ // update call 1
+ C_COMPONENT.update(_id, {
+ name: "New name",
+ }),
+
+ // update call 2
+ C_COMPONENT.update(_id, {
+ name: "New name - updated",
+ })
+
+ ]).then(() => {
+
+ event.args.forEach((args) => {
+ assert.equal(args[0] instanceof Webhook, true);
+ });
+
+ done();
+
+ }).catch(done);
+ });
+
+
+ workflow(C_COMPONENT, "remove", (done, { post }) => {
+ C_COMPONENT.remove(_id, (err, item) => {
+ try {
+
+ // check post arguments item instance
+ post.args.forEach((args) => {
+ assert.equal(args[0] instanceof Webhook, true);
+ });
+
+ assert.ok(err === null);
+ assert.equal(item instanceof Webhook, true);
+
+ done(err);
+
+ } catch (err) {
+
+ done(err);
+
+ }
+ });
+ });
+
+
+} catch (err) {
+ console.error(err);
+ process.exit(100);
+}
\ No newline at end of file
diff --git a/tests/index.js b/tests/index.js
index aee3a46..616cee4 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -35,7 +35,8 @@ process.env = Object.assign({
VAULT_SALT_BYTE_LEN: "16",
USERS_BCRYPT_SALT_ROUNDS: "12",
USERS_JWT_SECRET: "Pa$$w0rd",
- USERS_JWT_ALGORITHM: "HS384"
+ USERS_JWT_ALGORITHM: "HS384",
+ MQTT_BROKER_VERSION: "3"
}, env.parsed, process.env);
diff --git a/tests/system/component/test.component.js b/tests/system/component/test.component.js
index 7dd3fc5..9193426 100644
--- a/tests/system/component/test.component.js
+++ b/tests/system/component/test.component.js
@@ -67,7 +67,7 @@ describe("component", function () {
"_defineMethod", "_mapMethod", "_ready", // common methods
"items", "collection", "schema", // common properties
"get", "add", "update", "remove", "find", // component methods
- "found"
+ "found", "_labels", "labels"
];
for (let item of getItmes(instance)) {