hm-simulator 1.0.0 → 1.1.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.
- package/README.md +17 -2
- package/lib/method-help.js +4 -0
- package/lib/sim.js +180 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,11 +37,12 @@ messages. rfd and hmipserver start by default, the others when their port is con
|
|
|
37
37
|
| devices | `listDevices`, `getDeviceDescription`, `deleteDevice`, `replaceDevice`, `setInstallMode`, `getInstallMode` |
|
|
38
38
|
| paramsets | `getParamsetDescription`, `getParamset`, `putParamset`, `getValue`, `setValue`, `determineParameter`, `reportValueUsage` |
|
|
39
39
|
| links | `getLinks`, `getLinkPeers`, `getLinkInfo`, `setLinkInfo`, `addLink`, `removeLink`, `activateLinkParamset` |
|
|
40
|
-
| interface | `rssiInfo`, `listBidcosInterfaces`, `setBidcosInterface`, `getServiceMessages`
|
|
40
|
+
| interface | `rssiInfo`, `listBidcosInterfaces`, `setBidcosInterface`, `getServiceMessages`, `setTempKey` (BidCos only) |
|
|
41
|
+
| teams | `listTeams`, `setTeam` (BidCos only): the smoke detector teams as rfd keeps them, pseudo devices `*<serial>` |
|
|
41
42
|
| maintenance | `clearConfigCache`, `restoreConfigToDevice`, `updateFirmware`, `installFirmware` (the last two are stubs that record the call) |
|
|
42
43
|
|
|
43
44
|
**Outgoing RPC calls** to every registered logic layer: `listDevices`, `newDevices`,
|
|
44
|
-
`deleteDevices`, `event`, `system.multicall`.
|
|
45
|
+
`deleteDevices`, `updateDevice` (after `setTeam`), `event`, `system.multicall`.
|
|
45
46
|
|
|
46
47
|
**ReGa** (`rega.exe` on port 8181): the scripts of the
|
|
47
48
|
[homematic-rega](https://github.com/hobbyquaker/homematic-rega) client - `getChannels`,
|
|
@@ -300,6 +301,20 @@ hm-simulator [options]
|
|
|
300
301
|
|
|
301
302
|
## Changelog
|
|
302
303
|
|
|
304
|
+
### 1.1.0
|
|
305
|
+
|
|
306
|
+
- `setTempKey` on the BidCos interfaces: the passphrase a device is taught in with, applied to
|
|
307
|
+
the pairings that follow; `sim.getTempKey(iface)` reads back what was set, an empty string
|
|
308
|
+
clears it. No cryptography. hmipserver has no such method, so it faults with unknown-method
|
|
309
|
+
there (Homematic Manager issue #20).
|
|
310
|
+
- Smoke detector teams on the BidCos interfaces: `listTeams` and `setTeam`. A channel with a
|
|
311
|
+
`TEAM_TAG` is in the team its `TEAM` names, a pseudo device `*<serial>` whose team channel lists
|
|
312
|
+
the members in `TEAM_CHANNELS`; `setTeam(channel, team)` moves it and deletes a team nobody is
|
|
313
|
+
left in, `setTeam(channel, '')` puts it back into a team of its own, created like the one it
|
|
314
|
+
left (`newDevices`, `deleteDevices`, `updateDevice` to the logic layers). The shape is rfd's
|
|
315
|
+
as Homematic Manager task 58 read it from a CCU3; when rfd deletes and creates the team devices
|
|
316
|
+
is the simulator's model, not a measurement.
|
|
317
|
+
|
|
303
318
|
### 1.0.0
|
|
304
319
|
|
|
305
320
|
- Node >= 20.19, `binrpc` ^4.2, `homematic-xmlrpc` ^2.0; `express`, `body-parser`, `request`,
|
package/lib/method-help.js
CHANGED
|
@@ -36,6 +36,10 @@ module.exports = {
|
|
|
36
36
|
getServiceMessages: 'array getServiceMessages() - the pending service messages',
|
|
37
37
|
setInstallMode: 'void setInstallMode(boolean on, int time, int mode) - switch the install mode',
|
|
38
38
|
getInstallMode: 'int getInstallMode() - remaining seconds of the install mode',
|
|
39
|
+
setTempKey: 'void setTempKey(string passphrase) - the temporary key the next BidCos pairings use; BidCos only',
|
|
40
|
+
listTeams: 'array listTeams() - the team pseudo devices (smoke detector teams) with their channels; BidCos only',
|
|
41
|
+
setTeam:
|
|
42
|
+
'void setTeam(string channelAddress, string teamAddress) - put a channel into a team, an empty team into its own; BidCos only',
|
|
39
43
|
deleteDevice: 'void deleteDevice(string address, int flags) - remove a device',
|
|
40
44
|
replaceDevice:
|
|
41
45
|
'void replaceDevice(string oldDeviceAddress, string newDeviceAddress) - take over the configuration of a device',
|
package/lib/sim.js
CHANGED
|
@@ -37,6 +37,9 @@ const PARAMSET_PREFIX = {
|
|
|
37
37
|
cuxd: 'CUxD',
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
+
/** the interfaces that speak the BidCos protocol, and therefore have setTempKey */
|
|
41
|
+
const BIDCOS_INTERFACES = new Set(['rfd', 'wired']);
|
|
42
|
+
|
|
40
43
|
/** paramset keys that are not the address of a linked peer channel */
|
|
41
44
|
const STANDARD_PARAMSETS = new Set(['MASTER', 'VALUES', 'SERVICE']);
|
|
42
45
|
|
|
@@ -181,6 +184,8 @@ class HmSim {
|
|
|
181
184
|
this.serviceMessages = {};
|
|
182
185
|
/** end of the install mode per interface, as an epoch timestamp */
|
|
183
186
|
this.installMode = {};
|
|
187
|
+
/** the temporary AES key of the BidCos interfaces, see setTempKey */
|
|
188
|
+
this.tempKey = {};
|
|
184
189
|
/** devices that appear when the install mode is switched on, see scriptNewDevices() */
|
|
185
190
|
this.newDevicesScript = {};
|
|
186
191
|
/** every reportValueUsage the simulator was told about */
|
|
@@ -978,6 +983,172 @@ class HmSim {
|
|
|
978
983
|
return '';
|
|
979
984
|
}
|
|
980
985
|
|
|
986
|
+
/**
|
|
987
|
+
* `setTempKey(passphrase)` - the temporary key a BidCos device is taught in with.
|
|
988
|
+
*
|
|
989
|
+
* A HomeMatic device can be paired with a passphrase instead of the interface's own key, and a
|
|
990
|
+
* device that was taught in that way can only be paired again when the same passphrase is
|
|
991
|
+
* offered. rfd takes it as one string argument and answers with the empty string; it applies to
|
|
992
|
+
* the pairings that follow, so an application sends it *before* `setInstallMode` or
|
|
993
|
+
* `addDevice`. An empty string clears it back to the interface's key.
|
|
994
|
+
*
|
|
995
|
+
* Only the BidCos processes have the method - hmipserver answers an unknown-method fault, which
|
|
996
|
+
* is what an application has to handle when a user types it into the wrong interface.
|
|
997
|
+
*
|
|
998
|
+
* The simulator does no cryptography: it remembers the key so a test can assert that the call
|
|
999
|
+
* happened, in which order, and with what. `sim.getTempKey(iface)` reads it back.
|
|
1000
|
+
*
|
|
1001
|
+
* @param {string} iface
|
|
1002
|
+
* @param {Array} params [passphrase]
|
|
1003
|
+
*/
|
|
1004
|
+
setTempKey(iface, params) {
|
|
1005
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1006
|
+
throw this.fault('unknownMethod', `${iface}/setTempKey`);
|
|
1007
|
+
}
|
|
1008
|
+
const [key] = params || [];
|
|
1009
|
+
if (key !== undefined && typeof key !== 'string') {
|
|
1010
|
+
throw this.fault('invalidArguments', 'setTempKey wants a string');
|
|
1011
|
+
}
|
|
1012
|
+
this.tempKey[iface] = key === undefined ? '' : key;
|
|
1013
|
+
this.log.info(iface, this.tempKey[iface] === '' ? 'temporary key cleared' : 'temporary key set');
|
|
1014
|
+
return '';
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* ------------------------------------------------------------------ teams */
|
|
1018
|
+
|
|
1019
|
+
/**
|
|
1020
|
+
* `listTeams()` - the team pseudo devices of a BidCos interface, with their channels.
|
|
1021
|
+
*
|
|
1022
|
+
* rfd keeps smoke detectors (and the other devices with a `TEAM_TAG`) in *teams*. A team is a
|
|
1023
|
+
* pseudo device whose address is `*` plus the serial of the device it was made from
|
|
1024
|
+
* (`*NEQ0448334`, type `HM-Sec-SD-2-Team`); its team channel carries the members in
|
|
1025
|
+
* `TEAM_CHANNELS`, and every member channel names the team channel in `TEAM`. The team devices
|
|
1026
|
+
* come with `listDevices` as well; `listTeams` is only them. hmipserver has no teams.
|
|
1027
|
+
*
|
|
1028
|
+
* @param {string} iface
|
|
1029
|
+
*/
|
|
1030
|
+
listTeams(iface) {
|
|
1031
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1032
|
+
throw this.fault('unknownMethod', `${iface}/listTeams`);
|
|
1033
|
+
}
|
|
1034
|
+
return this.devices[iface].devices.filter((device) => device.ADDRESS.startsWith('*'));
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* `setTeam(channelAddress, teamAddress)` - puts a channel into a team.
|
|
1039
|
+
*
|
|
1040
|
+
* The shape is rfd's, as Homematic Manager task 58 read it from a CCU3: every detector starts in
|
|
1041
|
+
* a team of its own (`*<its serial>`); joining another team takes it out of the old one. That a
|
|
1042
|
+
* team nobody is left in is deleted (`deleteDevices`) is the simulator's model, not a measurement. An empty
|
|
1043
|
+
* `teamAddress` puts the channel back into a team of its own, which is created again
|
|
1044
|
+
* (`newDevices`) with the type of the team it leaves. Channel and team must carry the same
|
|
1045
|
+
* `TEAM_TAG`; there is no call that creates a team. The changed channels are announced with
|
|
1046
|
+
* `updateDevice`. BidCos only.
|
|
1047
|
+
*
|
|
1048
|
+
* @param {string} iface
|
|
1049
|
+
* @param {Array} params [channelAddress, teamAddress]
|
|
1050
|
+
*/
|
|
1051
|
+
setTeam(iface, params) {
|
|
1052
|
+
if (!BIDCOS_INTERFACES.has(iface)) {
|
|
1053
|
+
throw this.fault('unknownMethod', `${iface}/setTeam`);
|
|
1054
|
+
}
|
|
1055
|
+
const [channelAddress, teamAddress = ''] = params || [];
|
|
1056
|
+
if (typeof channelAddress !== 'string' || typeof teamAddress !== 'string') {
|
|
1057
|
+
throw this.fault('invalidArguments', 'setTeam wants two strings');
|
|
1058
|
+
}
|
|
1059
|
+
const channel = this.requireDevice(iface, channelAddress);
|
|
1060
|
+
if (!channel.PARENT || !channel.TEAM_TAG || Array.isArray(channel.TEAM_CHANNELS)) {
|
|
1061
|
+
throw this.fault('invalidArguments', `${channelAddress} cannot be in a team`);
|
|
1062
|
+
}
|
|
1063
|
+
const oldTeam = channel.TEAM ? this.index[iface].get(channel.TEAM) : undefined;
|
|
1064
|
+
const ownDevice = `*${channel.PARENT}`;
|
|
1065
|
+
const ownTeam = `${ownDevice}:1`;
|
|
1066
|
+
const target = teamAddress === '' ? ownTeam : teamAddress;
|
|
1067
|
+
if (channel.TEAM === target) {
|
|
1068
|
+
return '';
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
let team = this.index[iface].get(target);
|
|
1072
|
+
if (team === undefined && target === ownTeam) {
|
|
1073
|
+
team = this.createTeam(iface, channel, oldTeam);
|
|
1074
|
+
}
|
|
1075
|
+
if (team === undefined) {
|
|
1076
|
+
throw this.fault('unknownInstance', `${iface}/${target}`);
|
|
1077
|
+
}
|
|
1078
|
+
if (!Array.isArray(team.TEAM_CHANNELS) || team.TEAM_TAG !== channel.TEAM_TAG) {
|
|
1079
|
+
throw this.fault('invalidArguments', `${target} is no team for ${channelAddress}`);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const changed = [channelAddress, target];
|
|
1083
|
+
if (oldTeam) {
|
|
1084
|
+
oldTeam.TEAM_CHANNELS = (oldTeam.TEAM_CHANNELS || []).filter((member) => member !== channelAddress);
|
|
1085
|
+
changed.push(oldTeam.ADDRESS);
|
|
1086
|
+
}
|
|
1087
|
+
team.TEAM_CHANNELS = [...team.TEAM_CHANNELS.filter((member) => member !== channelAddress), channelAddress];
|
|
1088
|
+
channel.TEAM = target;
|
|
1089
|
+
this.log.info(iface, 'team', channelAddress, '->', target);
|
|
1090
|
+
|
|
1091
|
+
for (const address of changed) {
|
|
1092
|
+
if (this.index[iface].has(address) && !(oldTeam && address === oldTeam.ADDRESS)) {
|
|
1093
|
+
this.tellClientsParams(iface, 'updateDevice', [address, 0]);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
// a team nobody is in any more is gone
|
|
1097
|
+
if (oldTeam && oldTeam.TEAM_CHANNELS.length === 0 && oldTeam.PARENT) {
|
|
1098
|
+
this.deleteDevice(iface, oldTeam.PARENT);
|
|
1099
|
+
} else if (oldTeam) {
|
|
1100
|
+
this.tellClientsParams(iface, 'updateDevice', [oldTeam.ADDRESS, 0]);
|
|
1101
|
+
}
|
|
1102
|
+
return '';
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/**
|
|
1106
|
+
* A team of a channel's own, `*<serial>`, shaped like the team it leaves (or, without one, as
|
|
1107
|
+
* `<PARENT_TYPE>-Team` with a `<TYPE>_TEAM` channel), and announced with `newDevices`.
|
|
1108
|
+
*/
|
|
1109
|
+
createTeam(iface, channel, like) {
|
|
1110
|
+
const parent = this.index[iface].get(channel.PARENT) || {};
|
|
1111
|
+
const likeDevice = like && like.PARENT ? this.index[iface].get(like.PARENT) : undefined;
|
|
1112
|
+
const address = `*${channel.PARENT}`;
|
|
1113
|
+
const type = likeDevice ? likeDevice.TYPE : `${channel.PARENT_TYPE}-Team`;
|
|
1114
|
+
const device = {
|
|
1115
|
+
ADDRESS: address,
|
|
1116
|
+
TYPE: type,
|
|
1117
|
+
VERSION: likeDevice ? likeDevice.VERSION : parent.VERSION || 1,
|
|
1118
|
+
FIRMWARE: likeDevice ? likeDevice.FIRMWARE : parent.FIRMWARE || '1.0',
|
|
1119
|
+
CHILDREN: [`${address}:0`, `${address}:1`],
|
|
1120
|
+
PARAMSETS: ['MASTER'],
|
|
1121
|
+
FLAGS: 9,
|
|
1122
|
+
};
|
|
1123
|
+
const maintenance = {
|
|
1124
|
+
ADDRESS: `${address}:0`,
|
|
1125
|
+
TYPE: 'MAINTENANCE',
|
|
1126
|
+
VERSION: device.VERSION,
|
|
1127
|
+
PARENT: address,
|
|
1128
|
+
PARENT_TYPE: type,
|
|
1129
|
+
PARAMSETS: ['MASTER', 'VALUES'],
|
|
1130
|
+
INDEX: 0,
|
|
1131
|
+
};
|
|
1132
|
+
const teamChannel = {
|
|
1133
|
+
ADDRESS: `${address}:1`,
|
|
1134
|
+
TYPE: like ? like.TYPE : `${channel.TYPE}_TEAM`,
|
|
1135
|
+
VERSION: device.VERSION,
|
|
1136
|
+
PARENT: address,
|
|
1137
|
+
PARENT_TYPE: type,
|
|
1138
|
+
PARAMSETS: ['MASTER', 'VALUES'],
|
|
1139
|
+
INDEX: 1,
|
|
1140
|
+
TEAM_TAG: channel.TEAM_TAG,
|
|
1141
|
+
TEAM_CHANNELS: [],
|
|
1142
|
+
};
|
|
1143
|
+
this.addDevices(iface, [device, maintenance, teamChannel]);
|
|
1144
|
+
return this.index[iface].get(teamChannel.ADDRESS);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
/** The temporary key an interface was last given, `''` when there is none. */
|
|
1148
|
+
getTempKey(iface) {
|
|
1149
|
+
return this.tempKey[iface] || '';
|
|
1150
|
+
}
|
|
1151
|
+
|
|
981
1152
|
/** Remaining seconds of the install mode, 0 when it is off. */
|
|
982
1153
|
getInstallMode(iface) {
|
|
983
1154
|
const until = this.installMode[iface] || 0;
|
|
@@ -1261,9 +1432,14 @@ class HmSim {
|
|
|
1261
1432
|
|
|
1262
1433
|
/** Calls one method on every connected logic layer. */
|
|
1263
1434
|
tellClients(iface, method, payload) {
|
|
1435
|
+
this.tellClientsParams(iface, method, [payload]);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
/** The same with several parameters after the interface id (`updateDevice(id, address, hint)`). */
|
|
1439
|
+
tellClientsParams(iface, method, params) {
|
|
1264
1440
|
for (const key of Object.keys(this.clients[iface] || {})) {
|
|
1265
1441
|
const client = this.clients[iface][key];
|
|
1266
|
-
client.methodCall(method, [client.id,
|
|
1442
|
+
client.methodCall(method, [client.id, ...params]);
|
|
1267
1443
|
}
|
|
1268
1444
|
}
|
|
1269
1445
|
|
|
@@ -1320,6 +1496,9 @@ class HmSim {
|
|
|
1320
1496
|
getServiceMessages: (iface) => this.getServiceMessages(iface),
|
|
1321
1497
|
setInstallMode: (iface, params) => this.setInstallMode(iface, params),
|
|
1322
1498
|
getInstallMode: (iface) => this.getInstallMode(iface),
|
|
1499
|
+
setTempKey: (iface, params) => this.setTempKey(iface, params),
|
|
1500
|
+
listTeams: (iface) => this.listTeams(iface),
|
|
1501
|
+
setTeam: (iface, params) => this.setTeam(iface, params),
|
|
1323
1502
|
deleteDevice: (iface, params) => this.deleteDevice(iface, params[0], params[1]),
|
|
1324
1503
|
replaceDevice: (iface, params) => this.replaceDevice(iface, params[0], params[1]),
|
|
1325
1504
|
reportValueUsage: (iface, params) => this.reportValueUsage(iface, params),
|