Skip to content

Commit

Permalink
fixed #181
Browse files Browse the repository at this point in the history
  • Loading branch information
windkh committed Oct 4, 2024
1 parent 5b06ddc commit 72139af
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 62 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Changelog
All notable changes to this project will be documented in this file.

## [11.4.0] - 2024-10-04
### support for shelly RC button 4 - [#181](https://github.com/windkh/node-red-contrib-shelly/issues/181)

## [11.3.0] - 2024-09-27
### removed all promises

Expand Down
49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ Generation 3 devices:
- Shelly 1 Mini, 1PM Mini, PM Mini
- Shelly Plus H&T

BLU Devices
- Shelly RC Button 4

Others may work but are not really tested so far.

Expand Down Expand Up @@ -786,6 +788,8 @@ pane telling you that installation of the webhook failed.
## BLU Gateway (Shelly BLU Gateway)
The node is able to control a shelly BLU gateway. This node should be only used with callback mode and event output.
It uploads two scripts: one for the callback like in all other gen 2 devices and one that scans for bluetooth signals.
This script was taken from https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-shelly-blu.js and is licensed
under the apache 2.0 license!

The output of the node is an event object. Bluetooth event objects have the event name "shelly-blu".

Expand All @@ -808,6 +812,51 @@ msg.payload.info.data.address



## Some notes about BLU Support
Almost all gen2+ device support bluetooth and can be used as blu gateway by using callback mode and activating
the gateway support in the config node. Messages from the BLU devices are in BTHomeV2 format: https://bthome.io/format/



## BLU RC Button 4
The node is able to control a shelly RC button 4. This device communicates via bluetooth. You must use at least one shelly as bluetooth
gateway to be able to receive message in node-red from that device.
This gateway node should be only used with callback mode and event output.
It uploads two scripts: one for the callback like in all other gen 2 devices and one that scans for bluetooth signals.
This script was taken from https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-shelly-blu.js and is licensed
under the apache 2.0 license!

The output of the node is an event object. Bluetooth event objects have the event name "shelly-blu".

BLU devices contain a detailed description about the types that are used: E.g. the documenation for the shelly RC button 4
can be found here: https://shelly-api-docs.shelly.cloud/docs-ble/Devices/wall_us/
Type 0x3A is tranmitted if a button is pressed. The value can be on of the following:
0x00 - none (button not pressed)
0x01 - press
0x02 - double press
0x03 - triple press
0x04 - long press
0xFE - hold
The buttons are transmitted as an array with the values mentioned above.

```
msg.payload.info.event === "shelly-blu"
```


You can take the mac-address to find out what BLU device sent the messages:


```
msg.payload.info.data.address
```


### Examples:
[**shelly BLU RC button 4 flow**](examples/blubutton4.json)



# Shelly REST API
For a complete documentation see also
https://shelly-api-docs.shelly.cloud/#common-http-api
Expand Down
1 change: 1 addition & 0 deletions examples/blubutton4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id":"5ae5ea9d59cabfd2","type":"shelly-gen2","z":"62a53d6e4cbdfb9a","hostname":"192.168.178.76","description":"ShellyBluGateway","mode":"callback","verbose":false,"server":"7f2759a4fe573634","outputmode":"event","uploadretryinterval":5000,"pollinginterval":5000,"pollstatus":false,"getstatusoncommand":false,"devicetype":"SNGW-BT01","devicetypemustmatchexactly":false,"captureblutooth":true,"outputs":1,"x":330,"y":100,"wires":[["cca090e1ec3df949","1036e2e1278be0a8"]]},{"id":"cc48394b6f62d257","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1050,"y":100,"wires":[]},{"id":"47a53d4373d2f908","type":"inject","z":"62a53d6e4cbdfb9a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":160,"y":100,"wires":[["5ae5ea9d59cabfd2"]]},{"id":"cca090e1ec3df949","type":"function","z":"62a53d6e4cbdfb9a","name":"filter for BLU events","func":"if(msg.payload.info){\n let isBluMessage = msg.payload.info.event === \"shelly-blu\";\n if (isBluMessage){\n node.send([msg, null]);\n }\n}\nelse{\n node.send([null, msg]);\n}\n","outputs":2,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":100,"wires":[["274ab400bebe2077"],["659e6672bcb7be07"]]},{"id":"93977f546b1ba812","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1050,"y":60,"wires":[]},{"id":"274ab400bebe2077","type":"function","z":"62a53d6e4cbdfb9a","name":"switch RC button 4","func":"// see also https://shelly-api-docs.shelly.cloud/docs-ble/Devices/wall_us/\n\n// TODO: adapt your mac addresses here\nlet rcbutton4 = \"7c:c6:b6:75:1b:1d\";\n\nif (msg.payload.info.data.address == rcbutton4){\n let buttons = msg.payload.info.data.button;\n\n let messages = [];\n\n for(let i=0; i<buttons.length; i++){\n let button = buttons[i];\n\n // we filter away no action and button down\n if(button != 0x00 && button != 0xFE){\n let newMsg = { payload: button }\n messages.push(newMsg);\n } else {\n messages.push(null);\n }\n }\n\n node.send(messages);\n}\n","outputs":4,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":850,"y":80,"wires":[["93977f546b1ba812"],["cc48394b6f62d257"],["fc5f78a1987deb87"],["483beb35d8609845"]]},{"id":"659e6672bcb7be07","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1030,"y":260,"wires":[]},{"id":"1036e2e1278be0a8","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1030,"y":300,"wires":[]},{"id":"fc5f78a1987deb87","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1050,"y":140,"wires":[]},{"id":"483beb35d8609845","type":"debug","z":"62a53d6e4cbdfb9a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1050,"y":180,"wires":[]},{"id":"7f2759a4fe573634","type":"shelly-gen2-server","port":"20000","hostname":"","hostip":"192.168.178.121"}]
4 changes: 2 additions & 2 deletions shelly/99-shelly.js
Original file line number Diff line number Diff line change
Expand Up @@ -2365,7 +2365,7 @@ module.exports = function (RED) {
success = true;
}
else if (mode === 'callback'){
let scriptPath = path.resolve(__dirname, './scripts/callback.script');
let scriptPath = path.resolve(__dirname, './scripts/callback.js');
const buffer = fs.readFileSync(scriptPath);
// const buffer = await readFile(scriptPath); #96 nodejs V19
let script = buffer.toString();
Expand Down Expand Up @@ -2399,7 +2399,7 @@ module.exports = function (RED) {

let mode = node.mode;
if (mode === 'callback'){
let scriptPath = path.resolve(__dirname, './scripts/blugateway.script');
let scriptPath = path.resolve(__dirname, './scripts/ble-shelly-blu.js');
const buffer = fs.readFileSync(scriptPath);
// const buffer = await readFile(scriptPath); #96 nodejs V19
let script = buffer.toString();
Expand Down
154 changes: 94 additions & 60 deletions shelly/scripts/blugateway.script → shelly/scripts/ble-shelly-blu.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,71 @@
// derived from https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-shelly-blu.js
let CONFIG = {
eventName: "shelly-blu",
debug: false,
active: false,
};
/**
* This script will use BLE observer to listen for advertising data from nearby Shelly BLU devices,
* decodes the data using a BTHome data structure, and emits the decoded data for further processing.
*
* This script DOESN'T execute actions, only emit events. Can be used with `ble-events-handler.js` example.
* You can configure the event name, by default its `shelly-blu`, the body of the event contains all the data
* parsed from the BLE device
*
* Represents data provided by each device.
* Every value illustrating a sensor reading (e.g., button) may be a singular sensor value or
* an array of values if the object has multiple instances.
*
* @typedef {Object} DeviceData
* @property {number} pid - Packet ID.
* @property {number} battery - The battery level of the device in percentage (%).
* @property {number} rssi - The signal strength in decibels (dB).
* @property {string} address - The MAC address of the Shelly BLU device.
* @property {string} model - The model of the Shelly BLU device.
* @property {number | number[]} [temperature] - The temperature value in degrees Celsius if the device has a temperature sensor. (Can be an array if has multiple instances)
* @property {number | number[]} [humidity] - The humidity value in percentage (%) if the device has a humidity sensor. (Can be an array if has multiple instances)
* @property {number | number[]} [illuminance] - The illuminance value in lux if the device has a light sensor. (Can be an array if has multiple instances)
* @property {number | number[]} [motion] - Motion status: 0 for clear, 1 for motion (if the device has a motion sensor). (Can be an array if has multiple instances)
* @property {number | number[]} [window] - Window status: 0 for closed, 1 for open (if the device has a reed switch). (Can be an array if has multiple instances)
* @property {number | number[]} [button] - The number of presses if the device has a button. (Can be an array if has multiple instances)
* @property {number | number[]} [rotation] - The angle of rotation in degrees if the device has a gyroscope. (Can be an array if has multiple instances)
*
* @example
* {"component":"script:*","name":"script","id":*,"now":*,"info":{"component":"script:*","id":*,"event":"shelly-blu","data":{"encryption":false,"BTHome_version":2,"pid":118,"battery":100,"button":1,"rssi":-76,"address":*},"ts":*}}
*/

let ALLTERCO_MFD_ID_STR = "0ba9";
let BTHOME_SVC_ID_STR = "fcd2";

let uint8 = 0;
let int8 = 1;
let uint16 = 2;
let int16 = 3;
let uint24 = 4;
let int24 = 5;

//Logs the provided message with an optional prefix to the console.
function logger(message, prefix) {
//exit if the debug isn't enabled
if (!CONFIG.debug) {
return;
}
/******************* START CHANGE HERE *******************/
const CONFIG = {
// Specify the destination event where the decoded BLE data will be emitted. It allows for easy identification by other applications/scripts
eventName: "shelly-blu",

let finalText = "";
// If the script owns the scanner and this value is set to true, the scan will be active.
// If the script does not own the scanner, it may remain passive even when set to true.
// Active scan means the scanner will ping back the Bluetooth device to receive all its data, but it will drain the battery faster
active: false,

//if the message is list loop over it
if (Array.isArray(message)) {
for (let i = 0; i < message.length; i++) {
finalText = finalText + " " + JSON.stringify(message[i]);
}
} else {
finalText = JSON.stringify(message);
}
// When set to true, debug messages will be logged to the console
debug: false,
};
/******************* STOP CHANGE HERE *******************/

//the prefix must be string
if (typeof prefix !== "string") {
prefix = "";
} else {
prefix = prefix + ":";
}
const BTHOME_SVC_ID_STR = "fcd2";

//log the result
console.log(prefix, finalText);
}
const uint8 = 0;
const int8 = 1;
const uint16 = 2;
const int16 = 3;
const uint24 = 4;
const int24 = 5;

// The BTH object defines the structure of the BTHome data
let BTH = {};
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "battery", t: uint8, u: "%" };
BTH[0x02] = { n: "temperature", t: int16, f: 0.01, u: "tC" };
BTH[0x03] = { n: "humidity", t: uint16, f: 0.01, u: "%" };
BTH[0x05] = { n: "illuminance", t: uint24, f: 0.01 };
BTH[0x21] = { n: "motion", t: uint8 };
BTH[0x2d] = { n: "window", t: uint8 };
BTH[0x3a] = { n: "button", t: uint8 };
BTH[0x3f] = { n: "rotation", t: int16, f: 0.1 };
const BTH = {
0x00: { n: "pid", t: uint8 },
0x01: { n: "battery", t: uint8, u: "%" },
0x02: { n: "temperature", t: int16, f: 0.01, u: "tC" },
0x03: { n: "humidity", t: uint16, f: 0.01, u: "%" },
0x05: { n: "illuminance", t: uint24, f: 0.01 },
0x21: { n: "motion", t: uint8 },
0x2d: { n: "window", t: uint8 },
0x2e: { n: "humidity", t: uint8, u: "%" },
0x3a: { n: "button", t: uint8 },
0x3f: { n: "rotation", t: int16, f: 0.1 },
0x45: { n: "temperature", t: int16, f: 0.1, u: "tC" },
};

function getByteSize(type) {
if (type === uint8 || type === int8) return 1;
Expand All @@ -65,9 +76,9 @@ function getByteSize(type) {
}

// functions for decoding and unpacking the service data from Shelly BLU devices
let BTHomeDecoder = {
const BTHomeDecoder = {
utoi: function (num, bitsz) {
let mask = 1 << (bitsz - 1);
const mask = 1 << (bitsz - 1);
return num & mask ? num - (1 << bitsz) : num;
},
getUInt8: function (buffer) {
Expand Down Expand Up @@ -120,21 +131,39 @@ let BTHomeDecoder = {
while (buffer.length > 0) {
_bth = BTH[buffer.at(0)];
if (typeof _bth === "undefined") {
logger("unknown type", "BTH");
console.log("BTH: Unknown type");
break;
}
buffer = buffer.slice(1);
_value = this.getBufValue(_bth.t, buffer);
if (_value === null) break;
if (typeof _bth.f !== "undefined") _value = _value * _bth.f;
result[_bth.n] = _value;

if (typeof result[_bth.n] === "undefined") {
result[_bth.n] = _value;
}
else {
if (Array.isArray(result[_bth.n])) {
result[_bth.n].push(_value);
}
else {
result[_bth.n] = [
result[_bth.n],
_value
];
}
}

buffer = buffer.slice(getByteSize(_bth.t));
}
return result;
},
};

// Еmitting the decoded BLE data to a specified event. It allows other scripts to receive and process the emitted data
/**
* Еmitting the decoded BLE data to a specified event. It allows other scripts to receive and process the emitted data
* @param {DeviceData} data
*/
function emitData(data) {
if (typeof data !== "object") {
return;
Expand All @@ -158,7 +187,6 @@ function BLEScanCallback(event, result) {
typeof result.service_data === "undefined" ||
typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined"
) {
logger("Missing service_data member", "Error");
return;
}

Expand All @@ -172,7 +200,7 @@ function BLEScanCallback(event, result) {
typeof unpackedData === "undefined" ||
unpackedData["encryption"]
) {
logger("Encrypted devices are not supported", "Error");
console.log("Error: Encrypted devices are not supported");
return;
}

Expand All @@ -185,6 +213,7 @@ function BLEScanCallback(event, result) {

unpackedData.rssi = result.rssi;
unpackedData.address = result.addr;
unpackedData.model = result.local_name;

emitData(unpackedData);
}
Expand All @@ -198,7 +227,7 @@ function init() {
}

//get the config of ble component
let BLEConfig = Shelly.getComponentConfig("ble");
const BLEConfig = Shelly.getComponentConfig("ble");

//exit if the BLE isn't enabled
if (!BLEConfig.enable) {
Expand All @@ -214,7 +243,7 @@ function init() {
}
else {
//start the scanner
let bleScanner = BLE.Scanner.Start({
const bleScanner = BLE.Scanner.Start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: CONFIG.active
});
Expand All @@ -226,6 +255,11 @@ function init() {

//subscribe a callback to BLE scanner
BLE.Scanner.Subscribe(BLEScanCallback);

// disable console.log when logs are disabled
if (!CONFIG.debug) {
console.log = function() {};
}
}

init();
init();
File renamed without changes.

0 comments on commit 72139af

Please sign in to comment.