Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The changes we talked about in issue #23 #24

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
FROM node:slim
LABEL maintainer="Holger Imbery <[email protected]>" \
version="1.1a" \
description="HM2MQTT (hm2mqtt.js) dockerized version of https://github.com/hobbyquaker/hm2mqtt.js"

RUN npm config set unsafe-perm true && npm install -g hm2mqtt
COPY . /node

RUN cd /node && \
npm install

EXPOSE 2126
EXPOSE 2127
ENTRYPOINT ["hm2mqtt"]

ENTRYPOINT [ "node", "/node/index.js" ]
14 changes: 7 additions & 7 deletions Dockerfile.armhf
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
FROM hypriot/rpi-node:slim
LABEL maintainer="Holger Imbery <[email protected]>" \
version="1.1a" \
description="HM2MQTT (hm2mqtt.js) dockerized version of https://github.com/hobbyquaker/hm2mqtt.js"
FROM arm32v7/node:slim

RUN npm config set unsafe-perm true
RUN npm install -g hm2mqtt
COPY . /node

RUN cd /node && \
npm install

EXPOSE 2126
EXPOSE 2127
ENTRYPOINT ["hm2mqtt"]

ENTRYPOINT [ "node", "/node/index.js" ]
135 changes: 18 additions & 117 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,132 +1,33 @@
# hm2mqtt.js

[![mqtt-smarthome](https://img.shields.io/badge/mqtt-smarthome-blue.svg)](https://github.com/mqtt-smarthome/mqtt-smarthome)
[![NPM version](https://badge.fury.io/js/hm2mqtt.svg)](http://badge.fury.io/js/hm2mqtt)
[![dependencies Status](https://david-dm.org/hobbyquaker/hm2mqtt.js/status.svg)](https://david-dm.org/hobbyquaker/hm2mqtt.js)
[![Build Status](https://travis-ci.org/hobbyquaker/hm2mqtt.js.svg?branch=master)](https://travis-ci.org/hobbyquaker/hm2mqtt.js)
[![Coverage Status](https://coveralls.io/repos/github/hobbyquaker/hm2mqtt.js/badge.svg?branch=master)](https://coveralls.io/github/hobbyquaker/hm2mqtt.js?branch=master)
[![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
[![License][mit-badge]][mit-url]
#### History of this project

> Node.js based Interface between Homematic and MQTT
It started off with owagner's Java-based [hm2mqtt](https://github.com/owagner/hm2mqtt). Hobbyquaker then created [hm2mqtt.js](https://github.com/hobbyquaker/hm2mqtt.js) (JavaScript) providing the same functionality as hm2mqtt. He gave up the development in favor his new projects [node-red-contrib-ccu](https://github.com/hobbyquaker/node-red-contrib-ccu) and [RedMatic](https://github.com/hobbyquaker/RedMatic).

# UNMAINTAINED - I gave up working on this project in favor of [node-red-contrib-ccu](https://github.com/hobbyquaker/node-red-contrib-ccu) respectively the CCU3/RaspberryMatic Addon [RedMatic](https://github.com/hobbyquaker/RedMatic) that includes node-red-contrib-ccu. The functionality of hm2mqtt.js will be implemented in node-red-contrib-ccu so this can act as a 1:1 drop-in-replacement.
I forked the project some time ago and added a few features to it. This project however got a bit too inflated for my taste, thus I'm also working on a very simple approach, implementing only rfd. No rega, no paramset database, just sending and receiving messages: [simple homematic rfd](https://github.com/dersimn/simplehomematicrfd2mqtt).

Because [hm2mqtt](https://github.com/owagner/hm2mqtt) isn't developed anymore and I don't really like Java I decided to
re-implement this with Node.js

It's kind of the same like the original hm2mqtt, but it supports BINRPC and XMLRPC (hm2mqtt only supports BINRPC), so it
can be used with Homematic IP also. Furthermore it supports Rega variables and programs.
## Usage with Docker

Run

### Installation
docker run dersimn/hm2mqtt.js --help

Prerequisites: [Node.js](https://nodejs.org) 6.0 or higher.
for a full list of options. A full command could look like this:

`npm install -g hm2mqtt`

I suggest to use [pm2](http://pm2.keymetrics.io/) to manage the hm2mqtt process (start on system boot, manage log files,
...)


### Command Line Options

Use `hm2mqtt --help` to get a list of available options. All options can also be set per environment variable (e.g.
setting `HM2MQTT_VERBOSITY=debug` has the same effect as using `--verbosity debug` as commandline parameter).

### MQTT URL

You can add Username/Password for the connection to the MQTT broker to the MQTT URL param like e.g.
`mqtt://user:pass@broker`. For a secure connection via TLS use `mqtts://` as URL scheme.


### Topics

* Events are published on `<name>/status/<channelName>/<datapoint>` (JSON payload, follows
[mqtt-smarthome payload format](https://github.com/mqtt-smarthome/mqtt-smarthome/blob/master/Architecture.md))
* Values can be set via `<name>/set/<channelAddress_or_channelName>/<datapoint>` (can be plain or JSON payload). Example:
`hmip/set/Light_Garage/STATE`,
* Single values from arbitrary Paramsets can be set via
`<name>/param/<channelAddress_or_channelName>/<paramset>/<datapoint>`. Example topic for setting the Mode of an 1st gen
Thermostat HM-CC-TC: `hm/param/Temperatur Hobbyraum Soll/MASTER/MODE_TEMPERATUR_REGULATOR`
* Multiple values at once in arbitrary Paramsets can be set via `
``<name>/param/<channelAddress_or_channelName>/<paramset>`. The payload has to be a JSON object like e.g.
`{"MODE_TEMPERATURE_REGULATOR":2,"TEMPERATUR_COMFORT_VALUE":24}`.
* Arbitrary RPC methods can be called via `<name>/rpc/<iface>/<command>/<callId>` and respond to `<name>/response/<callId>`
(JSON encoded Array as payload). The callId can be an arbitrary string, its purpose is just to collate the response
to the command. iface can be one of `hmip`, `rfd` or `hs485d`.


### Device and Channel Names

Device and Channel names are queried from ReGa, this can be disabled by setting the `--disable-rega` option. To trigger
a re-read after changes on the ReGa you can publish a message to `<name>/command/regasync` or just restart hm2mqtt.
As an alternative to using the names from ReGa you can also supply a json file with the `--json-name-table` option
containing address to name mappings, created by e.g.
[homematic-manager](https://github.com/hobbyquaker/homematic-manager). This file should look like:
```javascript
{
"EEQ1234567": "Device Name",
"EEQ1234567:1": "Channel Name",
...
}
```


### ReGa (Homematic variables and programs)

To receive changes from ReGa you have to set `--rega-poll-interval` and/or `--rega-poll-trigger`.
`--rega-poll-trigger` can be set to e.g. `BidCoS-RF:50.PRESS_SHORT`, then a polling is done whenever this virtual button
is pressed. This is meant to create a "pseudo push mechanism" where a program on the ccu reacts on variable changes and
presses this virtual button.

Variables and Programs are published to `<name>/status/<variableOrProgramName>` and can be set by sending a message to
`<name>/rega/<variableOrProgramName>`. Publishing `true` or `false` to a program activates/deactivates the program. To
start a program publish the string `start`.


### _NOTWORKING datapoints

hm2mqtt sends virtual datapoints named `LEVEL_NOTWORKING` respectively `STATE_NOTWORKING` for actuators that have a
`WORKING` and/or `DIRECTION` datapoint. The `*_NOTWORKING` datapoints are only updated when `WORKING` is `false` - this
is useful for e.g. sliders in a UI to prevent jumping sliders when a Blind or Keymatic is moving or a Dimmer is dimming.


## docker image for hm2mqtt.js

#### Usage (architecture: amd64)
- pull the image to your machine, or if you are on a swarm to each node
```
docker pull mqttsmarthome/hm2mqtt:latest
```
- start the container with (e.g)
```
docker run -d -p 2126:2126 -p 2127:2127 --name hm2mqtt -e HM2MQTT_MQTT-URL="mqtt://xxx.xxx.xxx.xxx" -e HM2MQTT_MQTT-USERNAME="mqtt-user-name" -e HM2MQTT_MQTT-PASSWORD="mqtt-user-password" -e HM2MQTT_CCU-ADDRESS="xxx.xxx.xxx.xxx" -e HM2MQTT_INIT-ADDRESS="xxx.xxx.xxx.xxx" -e HM2MQTT_VERBOSITY="debug" mqttsmarthome/hm2mqtt
```
- or the service in your swarm with (e.g)
```
docker service create --name hm2mqtt \
--network ingress \
--publish 2126:2126 \
--publish 2127:2127 \
--env HM2MQTT_MQTT-URL="mqtt://xxx.xxx.xxx.xxx" \
--env HM2MQTT_CCU-ADDRESS="xxx.xxx.xxx.xxx" \
--env HM2MQTT_INIT-ADDRESS="xxx.xxx.xxx.xxx" \
--env HM2MQTT_VERBOSITY="debug" \
mqttsmarthome/hm2mqtt
```

#### Usage (architecture: armhf)
- pull the image to your machine, or if you are on a swarm to each node
```
docker pull mqttsmarthome/hm2mqtt:armhf
```
- follow the description above (architecture: amd64), but leave out the pull sequence mentioned there.
docker run -d --restart=always --name=hm \
-p 2126:2126 -p 2127:2127 \
dersimn/hm2mqtt.js \
--mqtt-url mqtt://10.1.1.50 \
--ccu-address 10.1.1.112 \
--disable-rega \
--init-address 10.1.1.50 \
--protocol-prefer-strings \
--protocol-disable-value-checking \
--protocol-publish-enum-as-string \
--protocol-prefer-xmlrpc-for-rfd


## License

MIT (c) 2017 [Sebastian Raff](https://github.com/hobbyquaker)

[mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat
[mit-url]: LICENSE
10 changes: 9 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ module.exports = require('yargs')
.describe('publish-metadata', '')
.describe('publish-counters', '')
.describe('mqtt-retain', 'enable/disable retain flag for mqtt messages')
.describe('protocol-replace-colons', 'Replace colons (:) in topic name with underscores (_). Useful for OpenHAB compatibility.').boolean('protocol-replace-colons')
.describe('protocol-prefer-strings', 'Disables the use of explicitDouble converstion and transmitts float values as string. Might put more load on the CCU.').boolean('protocol-prefer-strings')
.describe('protocol-disable-value-checking', 'Disables the checking for unallowed values for e.g. in ENUM datapoints or the range of integer values. If set to true (disabled), all values will be sent to the CCU, the CCU then handles filtering/throwing error messages etc.').boolean('protocol-disable-value-checking')
.describe('protocol-publish-enum-as-string', 'Publishes ENUM datapoints with verbal string value instead of index, for e.g. thermostats "val":"MANU-MODE" instead of "val":1 (1 is the index of MANU-MODE in ENUM).').boolean('protocol-publish-enum-as-string')
.describe('protocol-publish-enum-list', 'Publishes complete VALUE_LIST for ENUM datapoints with every message. Might cause more load on MQTT broker.').boolean('protocol-publish-enum-list')
.describe('protocol-prefer-xmlrpc-for-rfd', 'Prefer xmlrpc instead of binrpc for rfd communications').boolean('protocol-prefer-xmlrpc-for-rfd')
.describe('protocol-publish-val-distinct', 'Publish just the value, no JSON object').boolean('protocol-publish-val-distinct')
.describe('insecure', 'allow tls connections with invalid certificates')
.boolean('insecure')
.alias({
Expand All @@ -43,7 +50,8 @@ module.exports = require('yargs')
'mqtt-url': 'mqtt://127.0.0.1',
name: 'hm',
verbosity: 'info',
'listen-address': require('./firstip.js'),
'listen-address': '0.0.0.0',
'init-address': require('./firstip.js'),
'listen-port': 2126,
'binrpc-listen-port': 2127,
'ping-interval': 30,
Expand Down
74 changes: 55 additions & 19 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,18 +133,21 @@ mqtt.on('message', (topic, payload) => {
const parts = topic.split('/');
if (parts.length >= 4 && parts[1] === 'set') {
// Topic <name>/set/<channel>/<datapoint>
const channel = parts.slice(2, parts.length - 1).join('/');
var channel = parts.slice(2, parts.length - 1).join('/');
if ( config.protocolReplaceColons ) channel = channel.replace("_",":");
const datapoint = parts[parts.length - 1];
rpcSet(channel, 'VALUES', datapoint, payload);
} else if (parts.length >= 5 && parts[1] === 'param') {
// Topic <name>/param/<channel>/<paramset>/<datapoint>
const channel = parts.slice(2, parts.length - 2).join('/');
var channel = parts.slice(2, parts.length - 2).join('/');
if ( config.protocolReplaceColons ) channel = channel.replace("_",":");
const paramset = parts[parts.length - 2];
const datapoint = parts[parts.length - 1];
rpcPutParam(channel, paramset, datapoint, payload);
} else if (parts.length >= 4 && parts[1] === 'paramset') {
// Topic <name>/paramset/<channel>/<paramset>
const channel = parts.slice(2, parts.length - 1).join('/');
var channel = parts.slice(2, parts.length - 1).join('/');
if ( config.protocolReplaceColons ) channel = channel.replace("_",":");
const paramset = parts[parts.length - 1];
rpcPutParamset(channel, paramset, payload);
} else if (parts.length === 5 && parts[1] === 'rpc') {
Expand Down Expand Up @@ -304,26 +307,46 @@ function rpcType(payload, paramset) {
break;
case 'FLOAT':
val = parseFloat(val);
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
if ( !config.protocolDisableValueChecking ) {
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
}
}
if ( config.protocolPreferStrings ) {
/* JavaScript doesn't seperate integer and float/double types, so in JavaScript there's only "Number".
* However, the XML-RPC library needs to determine whether to wrap the value in <i4>22</i$> or <double>22</double>,
* see [list of allowed datatypes](https://ws.apache.org/xmlrpc/types.html).
*
* node-xmlrpc does this with an `if ( value % 1 == 0)`, see [serializer.js:188](https://github.com/baalexander/node-xmlrpc/blob/d9c88c4185e16637ed5a22c1b91c80e958e8d69e/lib/serializer.js#L188)
*
* homematic-xmlrpc introduced an object like `{explicitDouble: val}`.
*
* The CCU2 seems to accept <string>22</string> messages and does a string to int/float conversion itself - currently in testing.
*/
val = String(val);
} else {
val = {explicitDouble: val};
}
val = {explicitDouble: val};
break;
case 'ENUM':
if (typeof val === 'string') {
if (typeof val === 'string' && !config.protocolPreferStrings ) {
if (paramset.ENUM && (paramset.ENUM.indexOf(val) !== -1)) {
val = paramset.ENUM.indexOf(val);
}
} else {
// When message is already an integer, check if it's an allowed index
}
// eslint-disable-line no-fallthrough
case 'INTEGER':
val = parseInt(val, 10);
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
if ( !config.protocolDisableValueChecking ) {
if (val < paramset.MIN) {
val = paramset.MIN;
} else if (val > paramset.MAX) {
val = paramset.MAX;
}
}
break;
case 'STRING':
Expand Down Expand Up @@ -630,7 +653,7 @@ function getPrograms(cb) {
log.debug('discover interfaces');
discover(config.ccuAddress, {
// Todo... cuxd: {port: 8701, protocol: 'binrpc'},
rfd: {port: 2001, protocol: 'binrpc'},
rfd: {port: 2001, protocol: (config.protocolPreferXmlrpcForRfd ? 'xmlrpc' : 'binrpc') },
hs485d: {port: 2000, protocol: 'binrpc'},
hmip: {port: 2010, protocol: 'xmlrpc'}
}, interfaces => {
Expand Down Expand Up @@ -729,9 +752,9 @@ process.on('SIGTERM', stop);
function initIface(name, protocol) {
let url;
if (protocol === 'binrpc') {
url = 'xmlrpc_bin://' + (config.initAddress || config.listenAddress) + ':' + config.binrpcListenPort;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why remove this? listenAdress is needed if hm2mqtt runs in a vm with nat networking or in a container that exposes the listenPort to the hosts interface

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually the other way around. In a Docker environment you can and will have multiple network interfaces. It's therefore a good idea to bind the socket to 0.0.0.0 a.k.a. "listen on all interfaces". This happens here in your script.

The line from this comment creates the url-string that is sent to the CCU. If you don't specify the --init-address it will default to the information form your firstip.js script, otherwise (for e.g. NAT-ed Docker) the user has to provide the real external IP and take care of proper port forwarding.

You can leave the line in here but combined with the change from line 44, this makes no semantical sense anymore.

url = 'xmlrpc_bin://' + (config.initAddress) + ':' + config.binrpcListenPort;
} else {
url = 'http://' + (config.initAddress || config.listenAddress) + ':' + config.listenPort;
url = 'http://' + (config.initAddress) + ':' + config.listenPort;
}
const params = [url, 'hm2mqtt_' + name];
log.info('rpc', name, '> init', params);
Expand Down Expand Up @@ -930,7 +953,9 @@ const rpcMethods = {
}
ps = (ps && ps.VALUES && ps.VALUES[params[2]]) || {};

const topic = config.name + '/status/' + (names[params[1]] || params[1]) + '/' + params[2];
var channel = (names[params[1]] || params[1]);
if ( config.protocolReplaceColons ) channel = channel.replace(":","_");
const topic = config.name + '/status/' + channel + '/' + params[2];

let payload = {val: params[3], ts, lc: changes[key], hm: {ADDRESS: params[1]}};
if (ps.UNIT && ps.UNIT !== '""') {
Expand All @@ -941,9 +966,20 @@ const rpcMethods = {
}
}
if (ps.TYPE === 'ENUM') {
payload.hm.ENUM = ps.VALUE_LIST[params[3]];
if ( config.protocolPublishEnumAsString ) {
payload.val = ps.VALUE_LIST[params[3]];
} else {
payload.hm.ENUM = ps.VALUE_LIST[params[3]];
}
if ( config.protocolPublishEnumList ) {
payload.hm.VALUE_LIST = ps.VALUE_LIST;
}
}
if (config.protocolPublishValDistinct) {
payload = String(payload.val);
} else {
payload = JSON.stringify(payload);
}
payload = JSON.stringify(payload);

const retain = (config.mqttRetain) && (ps.TYPE !== 'ACTION');

Expand Down