Das ist das Script EasyCall_BluEvents im Code-Block.
/*---------------------------------Supported Blu Events--------------------------------------------
Blu *all: ---> "alive" "single_push" "hold_push" "pairing_push" "unknown_push"
Blu Door/Window: ---> "open" "closed"
Blu Motion: ---> "no_motion" "motion_detected"
Blu Button: ---> "wake_up" "single_push" "double_push" "triple_push" "long_push"
--------------------------------------------------------------------------------------------------*/
let eDebug= false, //Create debug output, true/false, set this only to true for short periods!
//____Easy_Call_Config____
// easy_Start true/false, shows for 15 min, after a script start, blu "mac" ids near your Shelly.
var easy_Start= true,
actionMap= {
"BluB1SW_DGSteckdosenLeiste_on": {id: "MAC-ID", event: "single_push", call: ["Http.get",{url: "http://localhost/relay/0?turn=on"}] },
"BluB1SW_DGSteckdosenLeiste_off": {id: "MAC-ID", event: "long_push", call: ["Http.get",{url: "http://localhost/relay/0?turn=off"}] },
};//Entries name Restriction: without Space!, unique entries only!
//__Config_Blu_Events__
var battery_Fix= true, //Drop, 0% battery BTHome packages on Reboot Event, true/false
activeScan= true, //Active or Passiv Bluetooth Scan, only used when BT Gateway false!
get_All= false, //Get all data available and ship it in a Event, default false
debug= false; //Create debug output, true/false, set this only to true for short periods.
//~~~~~~~ Easy_Call v1.2 ~~~~~~~~~~
function Check_Event(d){ //Checking for Event Match
try{
let blu= 0, normal= 0;
if(easy_Start && !tH1) tH1= Timer.set(1000*60*15,0,function(){easy_Start= false;}); //15min Timer for easy_Start
//Filter for blu Events
if(bluShellys || easy_Start) blu= Efilter(d,{inData: true,filterValue: ['GBLE'], filterKey: ['mac','device_type','device_state']},eDebug); //Check for Blu Data
if(blu && blu.GBLE){
if(easy_Start) print('Found Device: Blu _[',blu.device_type,']_ ---event---> _[',blu.device_state,']_ \n id: ---> ',blu.mac);
for(a in actionMap){
if(actionMap[a].mac && actionMap[a].mac === blu.mac && actionMap[a].event === blu.device_state){
if(actionMap[a].call[2]) { //Check for Callback and pack event_data for Callback
Call(actionMap[a].call[0],actionMap[a].call[1],actionMap[a].call[2],{event_data:d.info,user_data:actionMap[a].call[3]},actionMap[a].call[4]);
}else{
Call(actionMap[a].call[0],actionMap[a].call[1],actionMap[a].call[2],actionMap[a].call[3],actionMap[a].call[4]);
}
}
}
}
//Filter for normal Events
if(dList.length < 1) return;
normal= Efilter(d,{device: dList, filterKey: ['event']},eDebug);
if(!normal) return; //Exit if useless Data
for(a in actionMap){
if(actionMap[a].event === normal.event){
if(actionMap[a].call[2]) { //Check for Callback and pack event_data for Callback
Call(actionMap[a].call[0],actionMap[a].call[1],actionMap[a].call[2],{event_data:d.info,user_data:actionMap[a].call[3]},actionMap[a].call[4]);
}else{
Call(actionMap[a].call[0],actionMap[a].call[1],actionMap[a].call[2],actionMap[a].call[3],actionMap[a].call[4]);
}
}
}
}catch(e){ErrorMsg(e,'Check_Event()',debug);} //Error Handler
}
var dList= [], bluShellys= false;
function Read_actionMap(a){ //Read actionMap
try{
//Check for valied actionMap Entrys
function cK(s){return typeof s !== 'string';}
if(!actionMap[a].id || !actionMap[a].event || !actionMap[a].call || cK(actionMap[a].id) || cK(actionMap[a].event) || !Array.isArray(actionMap[a].call)){
throw new Error('Wrong actionMap Parameter, dropped actionMap Entry ----> '+actionMap[a].id+' --> '+actionMap[a].event);
}
//Check for Blu mac
if(actionMap[a].id.length === 17) {
print("Info: add --> Blu Device ---> ",actionMap[a].id);
actionMap[a].mac= actionMap[a].id;
delete actionMap[a].id;
bluShellys= true;
}
//Add normal shellys to device List
if(actionMap[a].id) print("Info: add --> Normal Device ---> ",actionMap[a].id);
if(actionMap[a].id) dList.push(actionMap[a].id); //Device List
}catch(e){ErrorMsg(e,'Read_actionMap()',debug);} //Error Handler
}
//========= Blu_Events v2.4 =========
// RS Zeile 106 0x2e ergaenzt
let activeScan= true; //Active or Passiv Bluetooth Scan, only used when BT Gateway false!
//notUsed-->let _cid = "0ba9"; //Allterco, Company ID(MFD)
let devID1= "SBBT"; //Blu Button1, deviceID, --> SBBT-002C
let devID2= "SBDW"; //Blu Door/Window, deviceID --> SBDW-002C
let devID3= 'SBMO'; //Blu Motion, deviceID, --> SBMO-003Z
let uuid= "fcd2"; //BTHome, Service ID --> UUID(16bit)
let bluMap= {//Device Parameter, you can find the full BTH Device List at 'https://bthome.io/format'
//bthObjectID:[Property,Datatype,Factor/Unit],
'0xf0':['device_type_id','uint8','split2'], //All Blu Devices, special case [default --> uint16]
'0xf1':['firmware_version','uint8','split4'], //All Blu Devices, special case [default--> uint32]
'0xf2':['extra_data','uint8','split3'], //All Blu Devices, special case [default--> uint24] --> maybe FW version?
'0x00':['pid','uint8'], //All Blu Devices
'0x01':['battery','uint8','%'], //All Blu Devices
'0x3a':['button','uint8'], //All Blu Devices
'0x05':['illuminance','uint24',0.01], //Blu Motion & D/W
'0x45':['temperature','sint16',0.1], //Blu Motion & H&T
//notUsed-->'0x1a':['door','uint8'], //Blu D/W
//notUsed-->'0x20':['moisture','uint8'], //Unknown
'0x41':['distance','uint16',0.1], //BLU Sonic
'0x03':['humidity','uint16',0.01], //BLU H&T
'0x2d':['window','uint8'], //Blu D/W
'0x3f':['rotation','int16',0.1], //Blu D/W
'0x21':['motion','uint8'], //Blu Motion
'0x2e':['humidity','uint8','%'], //Blu Humidity ergaenzt RS
};
function CreateEvent(obj){ //Create Blu data and send Blu Events
try{
//Somehow filter for device_type, with out local names
obj.gen= 'GBLE'; obj.device_type= 'Unknown-Type';
if(typeof obj.button === 'number' && !obj.illuminance || obj.device_type_id === '2.1') obj.device_type= 'Button';
if(typeof obj.illuminance === 'number' && !obj.motion || obj.device_type_id === '2.2') obj.device_type= 'Door-Window';
if(typeof obj.motion === 'number' || obj.device_type_id === '2.5') obj.device_type= 'Motion';
//notUsed-->obj.device_type || obj.device_type_id ? obj.gen= 'GBLE': obj.device_type= 'Unknown-Type';
//Create device_states
switch(obj.device_type){
case 'Button':
obj.device_state= obj.input;
break;
case 'Door-Window':
obj.window === 0 ? obj.device_state= obj.input||'closed': obj.device_state= obj.input||'open';
break;
case 'Motion':
obj.motion === 0 ? obj.device_state= obj.button_input||'no_motion': obj.device_state= obj.input||'motion_detected';
break;
default:
obj.device_state= 'unknown_state';
}
if(obj.input) delete obj.input;
if(obj.firmware_version){
obj.device_state= 'alive';
if(battery_Fix && obj.battery === 0) delete obj.battery; //Workaround a Blu FW_Update Battery Bug
if(battery_Fix && get_All && obj.battery_string) delete obj.battery_string; //Workaround a Blu FW_Update Battery Bug
}
if(debug) print('\nDebug: Blu_data:\n',obj);
Shelly.emitEvent(''+obj.device_state, obj); //Sending Event
if(debug) print('Debug: sending Event __[',obj.device_state,']__');
}catch(e){ErrorMsg(e,'SendEvent()',debug);}
}
function CheckInput(bI){ //Check for Blu Button Events
try{
if(typeof bI !== 'number') return null;
let buttonMap= ['wake_up', 'single_push', 'double_push', 'triple_push', 'long_push', 'pairing_push', 'default_reset_push'];
if(bI > 6 && bI !== 254) bI= 'unknown_push';
if(bI < 7) bI= buttonMap[bI];
if(bI === 254) bI= 'hold_push';
return bI;
}catch(e){ErrorMsg(e,'CheckInput()',debug);}
}
function DeviceName(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,'DeviceName()',debug);}
}
function Unpack(d,m,r,l){ //Create BTHome obj and Unpack BTHome data
try{
//Setup declaring variabel and functions
if(typeof d !== "string" || d.length < 3) return null;
var obj= {mac: m, rssi: r, device_name: DeviceName(l)}, spC= 0, tmp= []; //Add extra data
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;
if(obj.encryption) throw new Error('BThome Service Data encripted, encription is not supported!');
if(get_All){
obj.bthome_version= byte >> 5; //Getting BTHome Version
(byte & 0x02) ? obj.interval= 'irregular': obj.interval= 'regular'; //Get transmission interval
if(obj.bthome_version !== 2) throw new Error('Wrong BThome Version: found v.'+obj.bthome_version+' only v.2 supported!');
}else{delete obj.encryption; if(obj.device_name === "Hidden-Device") delete obj.device_name;} //Reduce data
d= d.slice(1); //Delete useless Info byte
//Unpack BThome Values
for(let value of d){ //Search for matching BTHome hex ID
if(d.length < 1) break;
if(!spC) byte= btoh(d[0]); //Getting BTHome object ID
let bluData= bluMap['0x'+byte]; //Getting blu Data
if(bluData === undefined){ //Debug handling
print('Error: Unknown BThome Data--> HexID: 0x',byte,', you can add more id from the full objID list--> https://bthome.io/format');
obj.new_BTH_HexID= byte;
obj.new_Data= btoa(d);
obj.info= 'Please send the newBTH_HexID and newData to this script developer so that they can integrate it.';
break;
}
if(!spC) d= d.slice(1); //Delete usless bth ID byte
// Merge value bytes
let max= Number(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]+' max->'+max);
if(max === 1) value= d.at(0);
else if(max === 2) value= (d.at(1) << 8) | d.at(0);
else if(max === 3) value= (d.at(2) << 16) | (d.at(1) << 8) | d.at(0);
//notUsed-->else if(max === 4) value= (d.at(3) << 24) | (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 loop
//Adding String/Unit to value
let is_Str= typeof bluData[2] === 'string';
if (typeof bluData[2] === 'number') value= value*bluData[2]; //Adding factor
if(is_Str && !spC && Cut(bluData[2],'split')) spC= Number(Cut(bluData[2],'split','split'))+1; //Get split counter
if(spC){
tmp.push(value); //Saving value for special case
spC--; //reduce split counter
}else{
obj[bluData[0]]= value; //Saving value
}
if(is_Str && !spC && get_All) value= ''+value+bluData[2]; //Adding unit
if(is_Str && !spC && get_All) obj[bluData[0]+'_string']= value; //Saving String Value
if(spC === 1){
value= [];
for(let i in tmp){
value[(tmp.length-1)-i]= tmp[i]; //Reverse Array
}
tmp= []; //Clear useless data
obj[bluData[0]]= value.join('.'); //Join FW version into String
spC= 0; //Exit special case
}
}
return obj;
}catch(e){ErrorMsg(e,'Unpack()',debug);}
}
var old_pid= [-1,-1,-1,-1], old_mac= [-1,-1,-1,-1], iX= 0;
function ScanCB(e,r) { //BT Scan Loop
try{
if(e !== 2 || !r) return; //Exit if empty scan
if(!r.service_data || !r.service_data[uuid]) return; //Exit if not BTHome data
let obj= Unpack(r.service_data[uuid],r.addr,r.rssi,r.local_name); //Create BTHome Obj & Unpack BTHome Data
if(!obj) throw new Error('Failed to unpack service_data --> _[ '+btoa(r.service_data[uuid])+' ]_');
//Anti Double msg Logic
if((obj.pid === old_pid[0] && obj.mac === old_mac[0])||(obj.pid === old_pid[1] && obj.mac === old_mac[1])||
(obj.pid === old_pid[2] && obj.mac === old_mac[2])||(obj.pid === old_pid[3] && obj.mac === old_mac[3])
) return;//Exit if double msg
if(debug) print('Debug: Anti Double, saved Data:',iX,'\n',old_pid,'\n',old_mac);
if(iX >= old_pid.length) iX= 0;
old_pid[iX]= obj.pid; old_mac[iX]= obj.mac;
iX++ //increas counter;
if(!get_All && obj.pid) delete obj.pid; //reduce data
if(debug){ //Debug output
r.service_data[uuid]= btoa(r.service_data[uuid]);
r.advData= btoa(r.advData);
print('\nDebug: BT_data:\n',r,'\nDebug: BTHome_data:\n', obj);}
r= undefined, e= undefined; //Delete useless data
obj.input= CheckInput(obj.button);
if(!obj.input) delete obj.input; //Delete useless data
CreateEvent(obj); //Creating Event out of bthObj
}catch(e){ErrorMsg(e,'ScanCB()',debug);}
}
tH1= 0; //Global "easy_Start"" Timer Handler
function Main(){ //Main syncron Script Code
BLE.Scanner.Start({duration_ms: -1, active: activeScan}, ScanCB); //Sub to passiv BT Scanner, start a new scan if no scanner is runnning
BLE.Scanner.isRunning() ? print('Status: BLE.Scanner is scanning in Background.'): print('Status: BLE.Scanner cannot be initiated, check your Bluetooth settings!');
Object.keys(actionMap).forEach(Read_actionMap); //Read actionMap
Shelly.addEventHandler(Check_Event); //Start Check_Event() Loop
}
//Toolbox v1.0(base), 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= {}; //d.info= d.info.data;
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 ErrorChk(r,e,m,d){ //Shelly.call error check
try{
aC--; if(aC<0) aC= 0;
if(d.CB && d.uD) d.CB(r,d.uD); if(d.CB && !d.uD) d.CB(r);
if(!d.CB && d.uD) print('Debug: ',d.uD); if(e) throw new Error(Str(m));
if(Str(r) && Str(r.code) && r.code !== 200) throw new Error(Str(r));
}catch(e){ErrorMsg(e,'ErrorChk(), call Answer');}}
function Cqueue(){ //Shelly.call queue
try{
if(!cCache[0] && !nCall[0]) return;
while(cCache[0] && aC < callLimit){if(cCache[0] && !nCall[0]){nCall= cCache[0]; cCache.splice(0,1);}
if(nCall[0] && aC < callLimit){Call(nCall[0],nCall[1],nCall[2],nCall[3],nCall[4]); nCall= [];}} if(tH9){Timer.clear(tH9); tH9= 0;}
if(nCall[0] || cCache[0])if(cSp <= 0) cSp= 0.1; tH9= Timer.set(1000*cSp,0,function(){tH9= 0; Cqueue();});}catch(e){ErrorMsg(e,'Cqueue()');}}
function Call(m,p,CB,uD,deBug){ //Upgrade Shelly.call
try{
let d= {};
if(deBug) print('Debug: calling:',m,p); if(CB) d.CB= CB; if(Str(uD)) d.uD= uD; if(!m && CB){CB(uD); return;}
if(aC < callLimit){aC++; Shelly.call(m,p,ErrorChk,d);}else if(cCache.length < cacheLimit){
cCache.push([m,p,CB,uD,deBug]); if(deBug) print('Debug: save call:',m,p,', call queue now:',cCache.length); Cqueue();
}else{throw new Error('to many Calls in use, droping call: '+Str(m)+', '+Str(p));}}catch(e){ErrorMsg(e,'Call()');}}
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 null; 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(){ //Wating 2sek, to avoid a Shelly FW Bug
try{
if(Main && !tH9){tH9= Timer.set(2000,0,function(){print('\nStatus: started Script _[', scriptN,']_');
if(callLimit > 4){callLimit= 4;} try{Main();}catch(e){ErrorMsg(e,'Main()'); tH9= 0; Setup();}});}}catch(e){ErrorMsg(e,'Setup()');}}
function ErrorMsg(e,s,deBug){ //Toolbox formatted Error Msg
try{
let i=0; if(Cut(e.message, '-104: Timed out')) i= 'wrong URL or device may be offline';
if(Cut(e.message, 'calls in progress')) i= 'reduce _[ callLimit ]_ by 1 and try again, its a global variabel at the end of the toolbox';
if(s === 'Main()' || deBug) 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() --->',JSON.stringify(e));}}
var tH8= 0, tH9= 0, aC= 0, cCache= [], nCall= [], callLimit= 4, cacheLimit= 40, cSp= 0.1; //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 v1.0(base), Shelly FW >1.0.8
Setup();
Alles anzeigen
MAC-ID mit Blu-ID (11:22:33:44:55:66), event und url ersetzen. Bei entfernten Aktor IP-Adresse anstatt localhost.