This is a guide for controlling SOMA motors using the SOMA Connect U1 and Shelly BLU devices. This has been tested with Shelly BLU Gateway and Shelly BLU RC Button 4 and Shelly BLU Button 1. This will likely work with many other Shelly devices as well. If we test this with new products we will add those here as well. If you need assistance with this integration contact SOMA support.
First you need to set up your SOMA motor and Connect. The guides for this are available in separate articles here. Then you will need to set up the Shelly devices. Follow the guides Shelly provides for that here. For me all that was necessary was to download the Shelly app and click "Add Device". This will ask you for your WiFi credentials and add the BLU Gateway to your network. Next find the IP address of the added gateway. The exact ways to do that are beyond the scope of this guide.
Access the web interface of the BLU gateway and click "Scripts". Create a new script and give it a name you can identify later. Here I named the script "ble-shelly-soma.js"
In the code window you need to paste the following code:
/******************* START CHANGE HERE *******************/
let CONFIG = {
// When set to true, debug messages will be logged to the console
debug: true,
// When set to true the scan will be active, otherwise it will be passive.
// 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,
// SOMA Connect IP address
ip: '192.168.0.111',
// When `allowedMacAddresses` is set to null, evets from every bluetooth device are accepted.
allowedMacAddresses: null,
//allowedMacAddresses: [
// "aa:bc:12:34:56:78", // events only from these mac addresses are allowed.
// "11:22:33:45:5a:bc",
//],
/**
* Called when button action is reported from the filtered Shelly BLU devices.
* @param {Boolean} motion true, when there is a motion, false otherwise.
* @param {Object} eventData Object, containing all parameters received from the Shelly BLU Motion device. Example: {"encryption":false,"BTHome_version":2,"pid":16,"battery":100,"illuminance":109,"motion":1,"button":1,"rssi":-53,"address":"aa:bc:12:34:56:78"}
*/
buttonHandler: function (button, eventData) {
// Toggle the first replay ON/OFF based on the motion value.
//Shelly.call("Switch.Set", { id: 0, on: motion });
console.log("Button", button);
if(button[0] === 1) {
console.log("Trigger movement");
soma_move('fa:2d:1d:af:a4:6b', 0);
} else if(button[0] === 2) {
console.log("Trigger movement");
soma_move('fa:2d:1d:af:a4:6b', 100);
}
},
/**
* Called when packet from filtered Shelly BLU Motion devices is received.
* @param {Object} eventData Object, containing all parameters received from the Shelly BLU Motion device. Example: {"encryption":false,"BTHome_version":2,"pid":16,"battery":100,"illuminance":109,"motion":1,"button":1,"rssi":-53,"address":"aa:bc:12:34:56:78"}
*/
onStatusUpdate: function (eventData) {
// Do nothing at the moment.
}
};
/******************* STOP CHANGE HERE *******************/
const ALLTERCO_MFD_ID_STR = "0ba9";
const BTHOME_SVC_ID_STR = "fcd2";
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
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;
if (type === uint16 || type === int16) return 2;
if (type === uint24 || type === int24) return 3;
//impossible as advertisements are much smaller;
return 255;
}
// functions for decoding and unpacking the service data from Shelly BLU devices
const BTHomeDecoder = {
utoi: function (num, bitsz) {
const mask = 1 << (bitsz - 1);
return num & mask ? num - (1 << bitsz) : num;
},
getUInt8: function (buffer) {
return buffer.at(0);
},
getInt8: function (buffer) {
return this.utoi(this.getUInt8(buffer), 8);
},
getUInt16LE: function (buffer) {
return 0xffff & ((buffer.at(1) << 8) | buffer.at(0));
},
getInt16LE: function (buffer) {
return this.utoi(this.getUInt16LE(buffer), 16);
},
getUInt24LE: function (buffer) {
return (
0x00ffffff & ((buffer.at(2) << 16) | (buffer.at(1) << 8) | buffer.at(0))
);
},
getInt24LE: function (buffer) {
return this.utoi(this.getUInt24LE(buffer), 24);
},
getBufValue: function (type, buffer) {
if (buffer.length < getByteSize(type)) return null;
let res = null;
if (type === uint8) res = this.getUInt8(buffer);
if (type === int8) res = this.getInt8(buffer);
if (type === uint16) res = this.getUInt16LE(buffer);
if (type === int16) res = this.getInt16LE(buffer);
if (type === uint24) res = this.getUInt24LE(buffer);
if (type === int24) res = this.getInt24LE(buffer);
return res;
},
// Unpacks the service data buffer from a Shelly BLU device
unpack: function (buffer) {
//beacons might not provide BTH service data
if (typeof buffer !== "string" || buffer.length === 0) return null;
let result = {};
let _dib = buffer.at(0);
result["encryption"] = _dib & 0x1 ? true : false;
result["BTHome_version"] = _dib >> 5;
if (result["BTHome_version"] !== 2) return null;
//can not handle encrypted data
if (result["encryption"]) return result;
buffer = buffer.slice(1);
let _bth;
let _value;
while (buffer.length > 0) {
_bth = BTH[buffer.at(0)];
if (typeof _bth === "undefined") {
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;
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;
},
};
function onReceivedPacket(data) {
if (CONFIG._processedMacAddresses !== null) {
if (CONFIG._processedMacAddresses.indexOf(data.address) < 0) {
//logger(["Received event from", data.address, "outside of the allowed addresses"], "Info");
console.log("EVENT");
return;
}
}
if (
typeof CONFIG.buttonHandler === "function" &&
typeof data.button !== "undefined"
) {
if(data.button instanceof Array) {
CONFIG.buttonHandler(data.button , data);
} else {
CONFIG.buttonHandler([data.button], data);
}
}
if (typeof CONFIG.onStatusUpdate === "function") {
CONFIG.onStatusUpdate(data);
//logger("New status update", "Info");
console.log("Info: New status update ", data);
}
}
//saving the id of the last packet, this is used to filter the duplicated packets
let lastPacketId = 0x100;
// Callback for the BLE scanner object
function BLEScanCallback(event, result) {
//exit if not a result of a scan
if (event !== BLE.Scanner.SCAN_RESULT) {
return;
}
//exit if service_data member is missing
if (
typeof result.service_data === "undefined" ||
typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined"
) {
return;
}
let unpackedData = BTHomeDecoder.unpack(
result.service_data[BTHOME_SVC_ID_STR]
);
//exit if unpacked data is null or the device is encrypted
if (
unpackedData === null ||
typeof unpackedData === "undefined" ||
unpackedData["encryption"]
) {
//logger("Encrypted devices are not supported", "Error");
return;
}
//exit if the event is duplicated
if (lastPacketId === unpackedData.pid) {
return;
}
lastPacketId = unpackedData.pid;
unpackedData.rssi = result.rssi;
unpackedData.address = result.addr;
onReceivedPacket(unpackedData);
}
// Initializes the script and performs the necessary checks and configurations
function init() {
//exit if can't find the config
if (typeof CONFIG === "undefined") {
console.log("Error: Undefined config");
return;
}
//get the config of ble component
let BLEConfig = Shelly.getComponentConfig("ble");
//exit if the BLE isn't enabled
if (!BLEConfig.enable) {
console.log(
"Error: The Bluetooth is not enabled, please enable it from settings"
);
return;
}
//check if the scanner is already running
if (BLE.Scanner.isRunning()) {
console.log("Info: The BLE gateway is running, the BLE scan configuration is managed by the device");
}
else {
//start the scanner
let bleScanner = BLE.Scanner.Start({
duration_ms: BLE.Scanner.INFINITE_SCAN,
active: CONFIG.active
});
if (!bleScanner) {
console.log("Error: Can not start new scanner");
}
}
if (
typeof CONFIG.allowedMacAddresses !== "undefined"
) {
if (CONFIG.allowedMacAddresses !== null) {
// Process configured mac addresses all to lower case and remove duplicates.
CONFIG._processedMacAddresses =
CONFIG
.allowedMacAddresses
.map(function (mac) { return mac.toLowerCase(); })
.filter(function (value, index, array) { return array.indexOf(value) === index; })
}
else {
CONFIG._processedMacAddresses = null;
}
}
//subscribe a callback to BLE scanner
BLE.Scanner.Subscribe(BLEScanCallback);
}
function soma_move(mac, pos) {
const uri =
'http://' + CONFIG.ip + ':3000/set_shade_position?mac=' + mac + '&pos=' + pos;
console.log(uri);
Shelly.call('http.request', {
method: 'GET',
url: uri,
},
function (response) {
if (response && response.code && response.code === 200) {
console.log("response.code : ", response.code);
console.log(JSON.stringify(response.body));
Shelly.emitEvent("HTTP-result", response.body);
}
else {
console.log("Error: HTTP request failed.");
console.log("response.code : ", response.code);
}
});
}
init();
Next you will need to modify the code to work for your specific case. Modify the CONFIG.ip variable to point to your local SOMA Connect. This will need to be accessible by the BLU Gateway. You can also filter the MAC addresses of the buttons you want to be able to control your Shades if you want. The buttonHandler function is what you will need to modify the most for your specific case. Here it has been set up to listen to any Shelly button device and on single click move the motor with MAC fa:2d:1d:af:a4:6b up. On double click it will move down. Button events also have the button MAC address if you want to filter by that and multi-button devices will report all their buttons as an array. I will be adding to this guide as we get feedback and questions from support so don't hesitate to contact us. If you make any improvements to this feel free to share that as well.