hm2mqtt 2.5.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Lists (and with --yes clears) retained messages matching topic patterns — for the cutover
5
+ * (ROADMAP §9): the Node-RED flow left retained topics for unnamed channels (`hm/status//X`)
6
+ * and for devices and names that no longer exist.
7
+ *
8
+ * node scripts/clean-retained.js mqtt://broker 'hm/status//#' # list only
9
+ * node scripts/clean-retained.js mqtt://broker 'hm/status//#' 'hm3/#' --yes
10
+ * node scripts/clean-retained.js mqtts://broker:8883 'hm/#' --ca ca.pem --grep 'Alte Lampe'
11
+ */
12
+
13
+ import fs from 'node:fs';
14
+ import mqtt from 'mqtt';
15
+
16
+ const args = process.argv.slice(2);
17
+ const take = (flag) => {
18
+ const i = args.indexOf(flag);
19
+ return i === -1 ? undefined : args.splice(i, 2)[1];
20
+ };
21
+ const ca = take('--ca');
22
+ const grep = take('--grep');
23
+ const seconds = Number(take('--seconds') || 5);
24
+ const yes = args.includes('--yes');
25
+ const patterns = args.filter((a) => !a.startsWith('--') && a !== args[0]);
26
+ const url = args[0];
27
+ if (!url || patterns.length === 0) {
28
+ console.error(
29
+ 'usage: clean-retained.js <mqtt-url> <pattern>... [--grep <substring>] [--seconds 5] [--ca file] [--yes]',
30
+ );
31
+ process.exit(1);
32
+ }
33
+
34
+ const found = new Map();
35
+ const client = mqtt.connect(url, {ca: ca ? fs.readFileSync(ca) : undefined, rejectUnauthorized: Boolean(ca)});
36
+ client.on('connect', () => {
37
+ client.subscribe(patterns);
38
+ console.error(`collecting retained messages for ${patterns.join(', ')} (${seconds} s) …`);
39
+ });
40
+ client.on('message', (topic, payload, packet) => {
41
+ if (packet.retain && payload.length > 0 && (!grep || topic.includes(grep))) {
42
+ found.set(topic, payload.toString().slice(0, 80));
43
+ }
44
+ });
45
+ setTimeout(() => {
46
+ const topics = [...found.keys()].sort();
47
+ for (const t of topics) {
48
+ console.log(`${t} ${found.get(t)}`);
49
+ }
50
+ console.error(`${topics.length} retained topics`);
51
+ if (!yes) {
52
+ console.error('dry run — add --yes to clear them');
53
+ client.end(true);
54
+ return;
55
+ }
56
+ let n = 0;
57
+ for (const t of topics) {
58
+ client.publish(t, '', {retain: true}, () => {
59
+ n += 1;
60
+ if (n === topics.length) {
61
+ console.error(`cleared ${n}`);
62
+ client.end(true);
63
+ }
64
+ });
65
+ }
66
+ if (topics.length === 0) {
67
+ client.end(true);
68
+ }
69
+ }, seconds * 1000);
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Parallel-run check before the cutover (ROADMAP §9): subscribes to two status trees on the same
5
+ * broker, collects the retained state plus everything published while running, and prints
6
+ * what differs. Ignores the fields that legitimately differ (timestamps, cache flags).
7
+ *
8
+ * node scripts/compare-trees.js mqtt://broker hm hm3 [seconds]
9
+ * node scripts/compare-trees.js mqtts://user:pass@broker:8883 hm hm3 3600 --ca ca.pem
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import mqtt from 'mqtt';
14
+ import {compareTrees, formatReport} from '../lib/compare.js';
15
+
16
+ const args = process.argv.slice(2);
17
+ const caIndex = args.indexOf('--ca');
18
+ const ca = caIndex === -1 ? undefined : fs.readFileSync(args.splice(caIndex, 2)[1]);
19
+ const [url, leftName, rightName, seconds = '60'] = args;
20
+ if (!url || !leftName || !rightName) {
21
+ console.error('usage: compare-trees.js <mqtt-url> <left-prefix> <right-prefix> [seconds] [--ca file]');
22
+ process.exit(1);
23
+ }
24
+
25
+ const left = new Map();
26
+ const right = new Map();
27
+ const client = mqtt.connect(url, {ca, rejectUnauthorized: Boolean(ca)});
28
+ client.on('connect', () => {
29
+ client.subscribe([`${leftName}/status/#`, `${rightName}/status/#`]);
30
+ console.error(`collecting ${leftName}/status/# and ${rightName}/status/# for ${seconds} s …`);
31
+ });
32
+ client.on('message', (topic, payload) => {
33
+ const raw = payload.toString();
34
+ if (topic.startsWith(`${leftName}/status/`)) {
35
+ left.set(topic.slice(leftName.length + 8), raw);
36
+ } else if (topic.startsWith(`${rightName}/status/`)) {
37
+ right.set(topic.slice(rightName.length + 8), raw);
38
+ }
39
+ });
40
+ setTimeout(
41
+ () => {
42
+ client.end(true);
43
+ console.log(formatReport(compareTrees(left, right), {leftName, rightName}));
44
+ },
45
+ Number(seconds) * 1000,
46
+ );
package/.npmignore DELETED
@@ -1,4 +0,0 @@
1
- node_modules
2
- .DS_Store
3
- .idea
4
- .nyc_output
package/.travis.yml DELETED
@@ -1,40 +0,0 @@
1
- sudo: true
2
-
3
- before_install:
4
- - sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa -y
5
- - sudo apt-get update -y
6
- - sudo apt-get install mosquitto -y
7
-
8
- services:
9
- - docker
10
-
11
- before_script:
12
- - sudo apt-get --yes --no-install-recommends install binfmt-support qemu-user-static
13
- - echo ':arm:M::\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x28\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-arm-static:' | sudo tee -a /proc/sys/fs/binfmt_misc/register
14
-
15
- jobs:
16
- include:
17
- - stage: test
18
- script:
19
- - nvm install 6
20
- - npm install
21
- - npm test
22
- after_success: skip
23
- - stage: build docker image
24
- script:
25
- - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
26
- - docker build -f Dockerfile.armhf -t hm2mqtt_armhf .
27
- - docker images
28
- - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo $(npm view hm2mqtt version); else echo -$TRAVIS_BRANCH ; fi`
29
- - docker tag hm2mqtt_armhf mqttsmarthome/hm2mqtt_armhf:$TAG
30
- - docker tag hm2mqtt_armhf mqttsmarthome/hm2mqtt_armhf:latest
31
- - stage: test docker image
32
- script:
33
- - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo $(npm view hm2mqtt version); else echo -$TRAVIS_BRANCH ; fi`
34
- - docker run -d mqttsmarthome/hm2mqtt_armhf:$TAG
35
- - docker ps | grep armhf
36
- after_success:
37
- - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo $(npm view hm2mqtt version); else echo -$TRAVIS_BRANCH ; fi`
38
- - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
39
- - docker push mqttsmarthome/hm2mqtt_armhf:$TAG
40
- - docker push mqttsmarthome/hm2mqtt_armhf:latest
package/Dockerfile DELETED
@@ -1,10 +0,0 @@
1
- FROM node:slim
2
- LABEL maintainer="Holger Imbery <contact@connectedobjects.cloud>" \
3
- version="1.1a" \
4
- description="HM2MQTT (hm2mqtt.js) dockerized version of https://github.com/hobbyquaker/hm2mqtt.js"
5
-
6
- RUN npm config set unsafe-perm true && npm install -g hm2mqtt
7
-
8
- EXPOSE 2126
9
- EXPOSE 2127
10
- ENTRYPOINT ["hm2mqtt"]
package/Dockerfile.armhf DELETED
@@ -1,11 +0,0 @@
1
- FROM hypriot/rpi-node:slim
2
- LABEL maintainer="Holger Imbery <contact@connectedobjects.cloud>" \
3
- version="1.1a" \
4
- description="HM2MQTT (hm2mqtt.js) dockerized version of https://github.com/hobbyquaker/hm2mqtt.js"
5
-
6
- RUN npm config set unsafe-perm true
7
- RUN npm install -g hm2mqtt
8
-
9
- EXPOSE 2126
10
- EXPOSE 2127
11
- ENTRYPOINT ["hm2mqtt"]
package/discover.js DELETED
@@ -1,24 +0,0 @@
1
- const net = require('net');
2
-
3
- module.exports = (host, interfaces, callback) => {
4
- const sum = Object.keys(interfaces).length;
5
- const connections = {};
6
- let count = 0;
7
-
8
- Object.keys(interfaces).forEach(service => {
9
- connections[service] = net.connect({host, port: interfaces[service].port}, () => {
10
- connections[service].end();
11
- checkDone();
12
- });
13
- connections[service].on('error', () => {
14
- delete interfaces[service];
15
- checkDone();
16
- });
17
- });
18
-
19
- function checkDone() {
20
- if (++count >= sum) {
21
- callback(interfaces);
22
- }
23
- }
24
- };
package/firstip.js DELETED
@@ -1,16 +0,0 @@
1
- const os = require('os');
2
-
3
- const interfaces = os.networkInterfaces();
4
- const addresses = [];
5
- Object.keys(interfaces).forEach(i => {
6
- Object.keys(interfaces[i]).forEach(a => {
7
- const address = interfaces[i][a];
8
- if (address.family === 'IPv4' && !address.internal) {
9
- addresses.push(address.address);
10
- }
11
- });
12
- });
13
-
14
- const [firstAddress] = addresses;
15
-
16
- module.exports = firstAddress;
@@ -1,43 +0,0 @@
1
- !# devices.fn hm2mqtt
2
- !#
3
- !# Dieses Homematic-Script gibt eine Liste aller Geraete/Kanaele im JSON Format aus
4
- !#
5
- !# 3'2013-4'2017 hobbyquaker https://github.com/hobbyquaker
6
- !#
7
-
8
- string sDevId;
9
- string sChnId;
10
-
11
- Write('{');
12
-
13
- boolean dFirst = true;
14
-
15
- foreach (sDevId, root.Devices().EnumUsedIDs()) {
16
-
17
- object oDevice = dom.GetObject(sDevId);
18
- boolean bDevReady = oDevice.ReadyConfig();
19
-
20
- if (bDevReady) {
21
-
22
- if (dFirst) {
23
- dFirst = false;
24
- } else {
25
- WriteLine(',');
26
- }
27
-
28
- Write('"' # oDevice.Address() # '":"');
29
- WriteURL(oDevice.Name());
30
- Write('"');
31
-
32
- foreach(sChnId, oDevice.Channels()) {
33
- object oChannel = dom.GetObject(sChnId);
34
- WriteLine(',');
35
- Write('"' # oChannel.Address() # '":"');
36
- WriteURL(oChannel.Name());
37
- Write('"');
38
- }
39
-
40
- }
41
- }
42
-
43
- Write('}');
@@ -1,27 +0,0 @@
1
- !# programs.fn hm2mqtt
2
- !# Dieses Script gibt eine Liste der Programme im JSON Format aus
3
- !#
4
- !# 3'2013-4'2017 hobbyquaker https://github.com/hobbyquaker
5
- !#
6
-
7
- string oPrgID;
8
- object oProgram;
9
- boolean first = true;
10
- Write('{');
11
- foreach (oPrgID, dom.GetObject(ID_PROGRAMS).EnumUsedIDs()) {
12
- oProgram = dom.GetObject(oPrgID);
13
- if(oProgram != null) {
14
- if (first == false) {
15
- WriteLine(',');
16
- } else {
17
- first = false;
18
- }
19
- Write('"' # oPrgID # '":{"name":"');
20
- WriteURL(oProgram.Name());
21
- Write('", "active":' # oProgram.Active());
22
- Write(',"ts":"' # oProgram.ProgramLastExecuteTime());
23
- Write('"}');
24
-
25
- }
26
- }
27
- Write('}');
@@ -1,78 +0,0 @@
1
- !# variables.fn hm2mqtt
2
- !#
3
- !# Dieses Script gibt die Systemvariablen im JSON Format aus
4
- !#
5
- !# 3'2013-4'2017 hobbyquaker https://github.com/hobbyquaker
6
- !#
7
-
8
- object oSysVar;
9
- string sSysVarId;
10
- string sValueType;
11
- string sValue;
12
- boolean bFirst = true;
13
-
14
- Write('{');
15
-
16
- WriteLine('"40":{"name":"Alarmmeldungen","val":' # dom.GetObject(40).Value() # ',"min":0,"max":65000,"unit":"","type":16,"enum":""},');
17
- Write('"41":{"name":"Servicemeldungen","val":' # dom.GetObject(41).Value() # ',"min":0,"max":65000,"unit":"","type":16,"enum":""}');
18
-
19
- foreach (sSysVarId, dom.GetObject(ID_SYSTEM_VARIABLES).EnumUsedIDs()) {
20
-
21
- WriteLine(',');
22
-
23
- oSysVar = dom.GetObject(sSysVarId);
24
- sValueType = oSysVar.ValueType();
25
-
26
- Write('"' # sSysVarId # '":{"name":"');
27
- WriteURL(oSysVar.Name());
28
- Write('","val":');
29
- if (sValueType == 20) {
30
- Write('"');
31
- WriteURL(oSysVar.Value());
32
- Write('"');
33
- } else {
34
- sValue = oSysVar.Value();
35
- if (sValueType == 2) {
36
- if (sValue) {
37
- Write("true");
38
- } else {
39
- Write("false");
40
- }
41
- } else {
42
- if (sValue == "") {
43
- Write("0");
44
- } else {
45
- Write(sValue);
46
- }
47
- }
48
- }
49
- string sValueMin = oSysVar.ValueMin();
50
-
51
- if (sValueMin == '') {
52
- sValueMin = 'null';
53
- }
54
-
55
- string sValueMax = oSysVar.ValueMax();
56
-
57
- if (sValueMax == '') {
58
- sValueMax = 'null';
59
- }
60
-
61
- Write(',"ts":"' # oSysVar.Timestamp());
62
- Write('","min":' # sValueMin # ',"max":' # sValueMax # ',"unit":"');
63
- WriteURL(oSysVar.ValueUnit());
64
- Write('","type":' # sValueType);
65
-
66
- if (sValueType == 2) {
67
- Write(',"enum":"');
68
- WriteURL(oSysVar.ValueName0() # ';' # oSysVar.ValueName1());
69
- } else {
70
- Write(',"enum":"');
71
- WriteURL(oSysVar.ValueList());
72
- }
73
-
74
- Write('"}');
75
-
76
- }
77
-
78
- Write('}');
package/test.js DELETED
@@ -1,201 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- require('should');
4
-
5
- const cp = require('child_process');
6
- const request = require('request');
7
- const path = require('path');
8
- const streamSplitter = require('stream-splitter');
9
- const Mqtt = require('mqtt');
10
- mqtt = Mqtt.connect('mqtt://127.0.0.1');
11
-
12
- const simCmd = path.join(__dirname, '/node_modules/.bin/hm-simulator');
13
- const simArgs = [];
14
- let sim;
15
- let simPipeOut;
16
- let simPipeErr;
17
- const simSubscriptions = {};
18
- const simBuffer = [];
19
-
20
- const hmCmd = path.join(__dirname, '/index.js');
21
- const hmArgs = ['--mqtt-url', 'mqtt://127.0.0.1:1883', '--ccu-address', '127.0.0.1', '-v', 'debug'];
22
- let hm;
23
- let hmPipeOut;
24
- let hmPipeErr;
25
- const hmSubscriptions = {};
26
- const hmBuffer = [];
27
-
28
- let subIndex = 0;
29
-
30
- function subscribe(type, rx, cb) {
31
- subIndex += 1;
32
- if (type === 'sim') {
33
- simSubscriptions[subIndex] = {rx, cb};
34
- } else if (type === 'hm') {
35
- hmSubscriptions[subIndex] = {rx, cb};
36
- }
37
- matchSubscriptions(type);
38
- return subIndex;
39
- }
40
-
41
- function unsubscribe(type, subIndex) {
42
- if (type === 'sim') {
43
- delete simSubscriptions[subIndex];
44
- } else if (type === 'hm') {
45
- delete hmSubscriptions[subIndex];
46
- }
47
- }
48
-
49
- function matchSubscriptions(type, data) {
50
- let subs;
51
- let buf;
52
- if (type === 'sim') {
53
- subs = simSubscriptions;
54
- buf = simBuffer;
55
- } else if (type === 'hm') {
56
- subs = hmSubscriptions;
57
- buf = hmBuffer;
58
- }
59
- if (data) {
60
- buf.push(data);
61
- }
62
- buf.forEach((line, index) => {
63
- Object.keys(subs).forEach(key => {
64
- const sub = subs[key];
65
- if (line.match(sub.rx)) {
66
- sub.cb(line);
67
- delete subs[key];
68
- buf.splice(index, 1);
69
- }
70
- });
71
- });
72
- }
73
-
74
- function startHm() {
75
- hm = cp.spawn(hmCmd, hmArgs);
76
- hmPipeOut = hm.stdout.pipe(streamSplitter('\n'));
77
- hmPipeErr = hm.stderr.pipe(streamSplitter('\n'));
78
- hmPipeOut.on('token', data => {
79
- console.log('hm', data.toString());
80
- matchSubscriptions('hm', data.toString());
81
- });
82
- hmPipeErr.on('token', data => {
83
- console.log('hm', data.toString());
84
- matchSubscriptions('hm', data.toString());
85
- });
86
- }
87
-
88
- function startSim() {
89
- sim = cp.spawn(simCmd, simArgs);
90
- simPipeOut = sim.stdout.pipe(streamSplitter('\n'));
91
- simPipeErr = sim.stderr.pipe(streamSplitter('\n'));
92
- simPipeOut.on('token', data => {
93
- console.log('sim', data.toString());
94
- matchSubscriptions('sim', data.toString());
95
- });
96
- simPipeErr.on('token', data => {
97
- console.log('sim', data.toString());
98
- matchSubscriptions('sim', data.toString());
99
- });
100
- }
101
-
102
- function end(code) {
103
- if (hm.kill) {
104
- hm.kill();
105
- }
106
- if (sim.kill) {
107
- sim.kill();
108
- }
109
- if (typeof code !== 'undefined') {
110
- process.exit(code);
111
- }
112
- }
113
-
114
- process.on('SIGINT', () => {
115
- end(1);
116
- });
117
-
118
- process.on('exit', () => {
119
- end();
120
- });
121
-
122
- describe('start daemons', () => {
123
- it('hm-simulator should start without error', function (done) {
124
- this.timeout(20000);
125
- subscribe('sim', /binrpc server listening/, data => {
126
- done();
127
- });
128
- startSim();
129
-
130
- });
131
- it('hm2mqtt should start without error', function (done) {
132
- this.timeout(20000);
133
- subscribe('hm', /hm2mqtt [0-9.]+ starting/, data => {
134
- done();
135
- });
136
- startHm();
137
- });
138
- });
139
-
140
- describe('hm2mqtt - mqtt connection', () => {
141
- it('hm2mqtt should connect to the mqtt broker', function (done) {
142
- this.timeout(12000);
143
- subscribe('hm', /mqtt connected/, data => {
144
- done();
145
- });
146
- });
147
- });
148
-
149
- describe('hm2mqtt - hm-simulator connection', () => {
150
- it('hm2mqtt should have rfd devices', function (done) {
151
- this.timeout(12000);
152
- subscribe('hm', /rfd got [0-9]+ devices and channels/, data => {
153
- done();
154
- });
155
- });
156
- it('hm2mqtt should have hmip devices', function (done) {
157
- this.timeout(12000);
158
- subscribe('hm', /hmip got [0-9]+ devices and channels/, data => {
159
- done();
160
- });
161
- });
162
- it('hm2mqtt should save paramsetDescriptions (1)', function (done) {
163
- this.timeout(180000);
164
- subscribe('hm', /saving paramsetDescriptions/, data => {
165
- done();
166
- });
167
- });
168
- it('hm2mqtt should save paramsetDescriptions (2)', function (done) {
169
- this.timeout(180000);
170
- subscribe('hm', /saving paramsetDescriptions/, data => {
171
- done();
172
- });
173
- });
174
- });
175
-
176
- describe('hm2mqtt - hm-simulator - mqtt', () => {
177
- it('hm2mqtt should receive a BidCoS-RF:1 PRESS_SHORT event', function (done) {
178
- this.timeout(12000);
179
- subscribe('hm', /rpc < event \["hm2mqtt_rfd","BidCoS-RF:1","PRESS_SHORT",true\]/, data => {
180
- done();
181
- });
182
- });
183
- it('hm2mqtt should publish a mqtt message on hm/status/BidCoS-RF:1/PRESS_SHORT', function (done) {
184
- this.timeout(12000);
185
- subscribe('hm', /mqtt > hm\/status\/BidCoS-RF:1\/PRESS_SHORT/, data => {
186
- done();
187
- });
188
- });
189
- it('hm2mqtt should publish a BidCoS-RF:2 PRESS_SHORT event', function (done) {
190
- this.timeout(12000);
191
- subscribe('hm', /mqtt > hm\/status\/BidCoS-RF:2\/PRESS_SHORT/, data => {
192
- done();
193
- });
194
- mqtt.publish('hm/set/BidCoS-RF:2/PRESS_SHORT', 'true');
195
- });
196
- });
197
-
198
- setTimeout(() => {
199
- hm2mqtt.kill();
200
- process.exit(1);
201
- }, 30000);