//========= Blu_to_MQTT v1.0 ======== //Config var BluEvent_ScriptID= 1; //Blu_Events Script ID. var mqttLite= false; //Send only one event/rpc Topic, true/false var mqttID= "shellyBLU"; //MQTT Topic ID, (behind topicPrefix) var topicPrefix= ""; //Here you can set your own mqtt Topic Prefix, defult is an empty String var mqttIDwithDType= true; //Adds a device type to the MQTT topic ID, can be disabled for backward compatibility, true/false var mqttQOS= 0; //MQTT QOS Setting, Can be 0 - at most once, 1 - at least once or 2 exactly once. Default is 0 var uFixer= true; //Automaticly, try to fix a Shelly FW MQTT connection Bug, true/false var debug= false; //Show debug msg in log, true/false, warning setting this to 'true' will delay reaktion speed a lot var custom_Names= { //Optional, set custom names for specific Blu Mac addresses 'b4:35:33:fe:98:94': 'KitchenEG', }; //No "/" or space allowed inside custom names!!! var tH1= 0; //Global Timer function FixMQTT() { //Restart Shelly if MQTT connection is false for over 10 min. print('Debug: MQTT connection Status is _[', MQTT.isConnected(), ']_'); if(!MQTT.isConnected()){print('Debug: Trying to Fix MQTT Connection with Reboot'); Shelly.call('Shelly.Reboot');} if(tHandel) Timer.clear(tHandel); } function SendMQTTmsg(obj){ try{ obj= Efilter(obj,{device: ['script:'+BluEvent_ScriptID], inData: true}); //Filter Events if(!obj) return; //Exit if useless Data if(MQTT.isConnected()){ //Create MainTopic and input ID var mainTopic= mqttID+'-', dID= 'Error_no_ID'; var edID= Str(obj.doorStateID)||Str(obj.windowStateID)||Str(obj.motionID); if(edID) dID= 'd'+ edID; if(Str(obj.inputID)) dID= Str(obj.inputID); //Clear uFixer Timer if(tH1) Timer.clear(tH1); tH1= 0; //Custom name logic var cNames= Object.keys(custom_Names), cName= undefined; if(cNames.length) cNames.forEach(function(mac){if(obj.mac === mac) cName= custom_Names[mac];}); cNames= null; //Clear usless data //Add stuff to MainTopic, Prefix, MQTTid, custom Name if(mqttIDwithDType && obj.deviceType) mainTopic+= obj.deviceType+'-'; cName ? mainTopic+= cName+'/': mainTopic+= obj.mac+'/'; if(topicPrefix.length !== 0) mainTopic= topicPrefix+'/'+mainTopic; //MQTT lite logic if(mqttLite){ MQTT.publish(mainTopic + 'events/rpc', Str(obj), mqttQOS); if(debug) print('Debug: 1 MQTT message have been sent.'); obj= {}; //Clear useless Data return; } var ts= obj.ts; //Timestamp var topicMap= { //Map of known MQTT Topics inputKey: 'status/input:'+dID, 'info/custom_Name': cName, 'info/battery': obj.battery, 'info/rssi': obj.rssi, 'info/lastTimeStamp': ts, 'info/lastAktion': obj.event, 'info/lastAktionID': dID, 'info/mac': obj.mac, 'info/device': obj.device, 'info/deviceType': obj.deviceType, 'info/gateway': info.id, 'info/deviceMode': obj.deviceMode, 'status/deviceState': obj.deviceState, 'status/illuminance': obj.illuminance, 'status/rotationLvl': obj.rotationLvl,}; obj= {}; //Clear useless Data //Send MQTT Msg/topics, publish all msgs var oldLength= Object.keys(topicMap).length; if(debug) print('\nDebug: MQTT Publishing_Topic: ',mainTopic,'\nDebug: MQTT Data:\n',topicMap); for(key of Object.keys(topicMap)){ if(topicMap[key] !== undefined){ if(key === 'inputKey'){ key= topicMap[key]; delete topicMap.inputKey; topicMap[key]= ts; ts= 0; } if(debug) print('Debug: sending------>',key,'--->',topicMap[key]); let topic= mainTopic+key, value= ''+topicMap[key]; MQTT.publish(topic, value, mqttQOS); //MQTT.publish(mainTopic+key, Str(topicMap[key]), mqttQOS) delete topicMap[key]; //Delete useless Data } } if(debug) print('Debug:',oldLength-Object.keys(topicMap).length,'out of',oldLength,'possible MQTT topics have been sent.'); }else{ print('Error: MQTT is still not ready, cant send msg'); if (uFixer && !tH1) { print('Debug: Trying to Fix MQTT Connection Bug.'); tH1 = Timer.set(12 * 60 * 1000, false, FixMQTT); } } }catch(e){ErrorMsg(e,'SendMQTTmsg()');} } //========= Blu_Events v1.1 ======== let activeScan = true; //Active or Passiv Bluetooth Scan //notUsed--> let _cid = "0ba9"; //Allterco, Company ID(MFD) let uuid = "fcd2"; //BTHome, Service ID let devID1 = "SBBT"; //Blu Button1, deviceID, --> SBBT-002C, evt. 002C = Device Charge??? let devID2 = "SBDW"; //Blu Door/Window, deviceID --> SBDW-002C let devID3 = 'SBMO'; //Blu Motion, deviceID, --> SBMO-003Z let bluMap = {//Device Parameter, you can find the full BTH Device List at 'https://bthome.io/format' //bthObjectID:[Property,Datatype,Factor/Unit], '0x00':['pid','uint8'], //All Blu Devices '0x01':['battery','uint8','%'], //All Blu Devices '0x3a':['inputID','uint8'], //All Blu Devices '0x05':['illuminance','uint24',0.01], //Blu Motion and D/W '0x1a':['doorStateID','uint8'], //Blu D/W '0x2d':['windowStateID','uint8'], //Blu D/W '0x3f':['rotationLvl','int16',0.1], //Blu D/W '0x21':['motionID','uint8'], //Blu Motion }; function CreateEvent(obj){ //Create Blu Data and send Blu Events try{ if(typeof obj.inputID === 'number' && !obj.illuminance) obj.deviceType= 'Button'; //Somehow filter for blu Devices if(typeof obj.illuminance === 'number' && !obj.motion) obj.deviceType= 'Door-Window'; if(typeof obj.motionID === 'number') obj.deviceType= 'Motion'; if(obj.deviceType) obj.gen= 'BLU'; if(typeof obj.doorStateID === 'number') obj.deviceMode= 'Door'; //Create Blu Window/Door Data if(typeof obj.windowStateID === 'number') obj.deviceMode= 'Window'; if(obj.deviceMode) obj.deviceState = Str(obj.doorStateID) || Str(obj.windowStateID); if(obj.deviceState === '1') obj.deviceState= 'Open'; if(obj.deviceState === '0') obj.deviceState= 'Closed'; if(typeof obj.motionID === 'number') obj.deviceState= obj.motionID; //Create Blu Motion Data if(obj.motionID === 1) obj.deviceState= 'Motion-Detected'; if(obj.motionID === 0) obj.deviceState= 'No-Motion'; if(debug) print('\nDebug: Blu Data:\n',obj); Shelly.emitEvent(obj.buttonInput || 'Status_' + obj.deviceState, obj); //Sending Event if(debug) print('Debug: sending Event __[',obj.buttonInput || ('Status_'+obj.deviceState),']__'); }catch(e){ErrorMsg(e,'SendEvent()');} } function ButtonEvents(bInput){ //Check for Blu Button Events try{ if(typeof bInput !== 'number') return; let buttonMap= ['wake_up', 'single_push', 'double_push', 'triple_push', 'long_push', 'pairing_push', 'default_reset_push']; if(bInput > 6 && bInput !== 254) bInput= 'unknown_push'; if(bInput < 7) bInput= buttonMap[bInput]; if(bInput === 254) bInput= 'hold_push'; return bInput; }catch(e){ErrorMsg(e,'ButtonEvents()');} } function GetDeviceName(name){ //Check for locale Device Name try{ if(!name) return 'Hidden-Device'; if(Cut(name,devID1)) return 'Blu-Button1'; if(Cut(name,devID2)) return 'Blu-Door-Window'; if(Cut(name,devID3)) return 'Blu-Motion'; return 'Unknown-Device--> '+ name; }catch(e){ErrorMsg(e,'GetDeviceName()');} } function Unpack(d){ //Unpack BThome data try{ //Setup declaring variabel and functions if(typeof d !== "string" || d.length < 3) return; var obj= {}, byte= 0, max= 0, value= 0; function Int_To_uInt(int, bytes){ let mask= 1 << (bytes - 1); if(int & mask) return int-(1 << bytes); return int; } //Unpack Info BThome Byte byte= d.at(0); //Getting first Byte as dezimal if(byte & 0x01) obj.encryption= true; //Getting encryption; obj.version= byte >> 5; //Getting BTHome Version //(byte & 0x02) ? obj.interval= 'irregular': obj.interval= 'regular'; //Getting sending intervall //Used wrong by Blu Devices, they only send regular if(obj.version !== 2) throw new Error('wrong BThome Version: found v.'+obj.version+' only v.2 supported!'); if(obj.encryption) throw new Error('BThome Service Date encripted, encription is not supported!'); d= d.slice(1); //Delete useless Info byte //Unpack BThome Values for(byte of d){ //Search for matching BTHome hex ID if(d.length < 1) break; byte= btoh(d[0]); let bluData= bluMap['0x'+byte]; //Getting blu Data if(bluData === undefined){ print('Error: Unknown BThome Data--> HexID: 0x', byte, ', you can add more id from the full objID list--> https://bthome.io/format'); break; } d= d.slice(1); //Delete usless bth ID byte // Merge value bytes let max= Cut(bluData[1],'int','int')/8; //Getting max Bytes out of dataType if(d.length < max) throw new Error('Wrong DataType, '+d.length+' Bytes, payload to big for DataType: '+bluData[1]); if(max === 1) value= d.at(0); if(max === 2) value= 0xffff & ((d.at(1) << 8) | d.at(0)); if(max === 3) value= 0x00ffffff & ((d.at(2) << 16) | (d.at(1) << 8) | d.at(0)); d= d.slice(max); //Delete useless value Bytes if(!Cut(bluData[1],'u','u')) value= Int_To_uInt(value,max*8); //convert int to uint if(value === undefined) break; //Exit value unpacking loop //Adding special Parameter to value if (typeof bluData[2] === 'number') value= value*bluData[2]; //Adding factor obj[bluData[0]]= value; if (typeof bluData[2] === 'string') value= ''+value+bluData[2]; //Adding unit if (typeof bluData[2] === 'string') obj[bluData[0]+'String']= value; } return obj; }catch(e){ErrorMsg(e,'Unpack()');} } function ScanCB(e, res) { //BT Scan Loop try{ if(e !== 2 || !res) return; //Exit if empty Result if(!res.service_data || !res.service_data[uuid]) return; //Exit if not BTHome data let bthObj= Unpack(res.service_data[uuid]); //Unpack BTHome Data if(debug){ res.service_data[uuid]= btoa(res.service_data[uuid]); res.advData= btoa(res.advData); print('\nDebug: BT Data:\n',res,'\nDebug: BTHome Data:\n', bthObj);} if(!bthObj) throw new Error('Failed to Unpack service_data, '+Str(res)); delete res.service_data; delete res.advData; //Delete useless res data bthObj.device= GetDeviceName(res.local_name); //Getting local Device Name if(bthObj.device === 'Hidden-Device') delete bthObj.device; //Reduce bth Object if useless device Name bthObj.mac= res.addr, bthObj.rssi= res.rssi; //bthObj.gateway= info.id; res= 0; //Delete useless res Object bthObj.buttonInput = ButtonEvents(bthObj.inputID); if(!bthObj.buttonInput) delete bthObj.buttonInput; //Reduce bth Object if useless buttonInput; CreateEvent(bthObj); //Creating Event out of bthObj }catch(e){ErrorMsg(e,'ScanCB()');} } function Main(){ //Main syncron Script Code BLE.Scanner.Start({duration_ms: -1, active: activeScan}, ScanCB); //Infinity Scan for BT data if(BLE.Scanner.SCAN_RESULT) print('Status: BT Scanner is scanning in Background'); Shelly.addEventHandler(SendMQTTmsg); //Create Event Loop BluEvent_ScriptID= scriptID; //Setting Blu_Events Script ID } // Dekats Toolbox, a universal Toolbox for Shelly scripts function Efilter(d,p,deBug) { //Event Filter, d=eventdata, p={device:[], filterKey:[], filterValue:[], noInfo:true, inData:true}->optional_parameter try{ let fR= {}; if(p.noInfo){fR= d; d= {}; d.info= fR; fR= {};} if(p.inData && d.info.data){Object.assign(d.info,d.info.data) delete d.info.data;} if(!d.info) fR.useless= true; if(p.device && p.device.length && p.device.indexOf(d.info.component) === -1) fR.useless= true; if(p.device && p.device.length && !fR.useless && !p.filterKey && !p.filterValue) fR= d.info; if(p.filterKey && !fR.useless) for(f of p.filterKey) for(k in d.info) if(f === k) fR[k]= d.info[k]; if(p.filterValue && !fR.useless) for(f of p.filterValue) for(v of d.info) if(Str(v) && f === v) fR[Str(v)]= v; if(deBug) print('\nDebug: EventData-> ', d, '\n\nDebug: Result-> ', fR, '\n'); if(Str(fR) === '{}' || fR.useless){return;} return fR;}catch(e){ErrorMsg(e,'Efilter()');}} function Str(d){ //Upgrade JSON.stringify try{ if(d === null || d === undefined) return null; if(typeof d === 'string')return d; return JSON.stringify(d);}catch(e){ErrorMsg(e,'Str()');}} function Cut(f,k,o,i){ //Upgrade slice f=fullData, k=key-> where to cut, o=offset->offset behind key, i=invertCut try{ let s= f.indexOf(k); if(s === -1) return; if(o) s= s+o.length || s+o; if(i) return f.slice(0,s); return f.slice(s);}catch(e){ErrorMsg(e,'Cut()');}} function Setup(l){ //Wating 2sek, to avoid a Shelly FW Bug try{ if(Main && !tH9){tH9= Timer.set(2000,l,function(){print('\nStatus: started Script _[', scriptN,']_'); if(callLimit > 5){callLimit= 5;} try{Main();}catch(e){ErrorMsg(e,'Main()'); Setup();}});}}catch(e){ErrorMsg(e,'Setup()');}} function ErrorMsg(e,s){ //Formatted Error Msg try{ let i= 0; if(Cut(e.message, '-104: Timed out')) i= 'wrong URL or device may be offline'; if(s === 'Main()') i= e.stack; if(Cut(e.message, '"Main" is not')) i= 'define a Main() function before using Setup()'; print('Error:',s || "",'---> ',e.type,e.message); if(i) print('Info: maybe -->',i);}catch(e){print('Error: ErrorMsg() --->',e);}} var tH9= 0, callLimit= 4; //Toolbox global variable var Status= Shelly.getComponentStatus, Config= Shelly.getComponentConfig; //Renamed native function var info= Shelly.getDeviceInfo(), scriptID= Shelly.getCurrentScriptId(), scriptN= Config('script',scriptID).name; //Pseudo const, variabel //Toolbox v2.7-Alpha(cut), Shelly FW >1.0.8 Setup();