Nochmal eine optimierung...
PID-Check bevor ich Pharse um keine mehrfache Ausführungen zu bearbeitete.
sendBatch() das nicht zu viele http reuqests gesendet werden.
devs + lastPid sind duaerhaft angewachsen, dieser werden jetzt wieder freigegegeben.
Leider habe ich nur zwei BTHome Geräte... Teste gerne mit mehreren Devices.
let C={
name:"test",
host:"192.168.xxx.xxx",
api_key:"xxxxxxx",
scan_interval:5,
hb_interval:10,
offline_after:30,
debug:false
};
let VERSION="1.0.0";
let BTHOME="fcd2";
let EDDYSTONE="feaa";
let CALIB_UUID_PREFIX="B1E0CA11";
let _P="/shelly/";
let _H="0123456789abcdef";
let _s3="\x04\x05\x0A\x42\x4B\x4C";
let _s4="\x3E\x4D\x4E\x4F\x50";
let _s2="\x02\x03\x06\x07\x08\x0B\x0C\x0E\x12\x13\x14\x3D\x40\x41\x43\x45\x46\x47\x48\x49\x4A\x51\x52";
function objSize(id){
let c=String.fromCharCode(id);
if(_s3.indexOf(c)>=0)return 3;
if(_s4.indexOf(c)>=0)return 4;
if(_s2.indexOf(c)>=0)return 2;
if(id<=0x53)return 1;
return -1;
}
let known={};
let knownUuid={};
let discMode=false;
let discDevs={};
let discTimer=null;
let devs={};
let pending=[];
let ready=false;
let scanning=false;
let bootTs=0;
let lastPid={};
let calibBuf={};
let sending=false;
function fastPid(raw){
if(!raw||raw.length<2)return -1;
if(raw.charCodeAt(0)&0x01)return -1;
let i=1;
while(i<raw.length){
let id=raw.charCodeAt(i);
i++;
if(id===0x00){
if(i>=raw.length)return -1;
return raw.charCodeAt(i);
}
if(id===0x53){
if(i>=raw.length)break;
i+=1+raw.charCodeAt(i);
continue;
}
let size=objSize(id);
if(size<0)return -1;
i+=size;
}
return -1;
}
let batchTimer=null;
let hbTimer=null;
let pollTimer=null;
let retryTimer=null;
let offlineInitTimer=null;
function ts(){return Math.floor(Date.now()/1000);}
function mem(tag){
Shelly.call("Sys.GetStatus",{},function(r){
if(r)print("[MEM] "+tag+" free="+r.ram_free);
});
}
function macKey(a){return a.split(":").join("").toLowerCase();}
function toHex(b){return _H[(b>>4)&0xF]+_H[b&0xF];}
function url(p){return "http://"+C.host+":8099"+p;}
function post(p,body,cb){
if(C.api_key)body.api_key=C.api_key;
Shelly.call("HTTP.Request",{
method:"POST",url:url(p),body:JSON.stringify(body),
content_type:"application/json",timeout:5
},function(r,ec){
if(ec!==0||!r||!r.body){if(cb)cb(null);return;}
try{if(cb)cb(JSON.parse(r.body));}catch(e){if(cb)cb(null);}
});
}
function get(p,cb){
let u=url(p);
if(C.api_key)u+=(u.indexOf("?")>=0?"&":"?")+"api_key="+C.api_key;
Shelly.call("HTTP.Request",{
method:"GET",url:u,timeout:5
},function(r,ec){
if(ec!==0||!r||!r.body){if(cb)cb(null);return;}
try{if(cb)cb(JSON.parse(r.body));}catch(e){if(cb)cb(null);}
});
}
function parseBTHome(raw){
if(!raw||raw.length<2)return null;
let result={pid:-1,battery:-1,button:-1};
if(raw.charCodeAt(0)&0x01)return null;
let i=1;
while(i<raw.length){
let id=raw.charCodeAt(i);
i++;
if(id===0x53){
if(i>=raw.length)break;
i+=1+raw.charCodeAt(i);
continue;
}
let size=objSize(id);
if(size<0||i+size>raw.length)break;
if(id===0x00||id===0x01||id===0x3A){
let val=0;
for(let b=0;b<size;b++){
val=val+(raw.charCodeAt(i+b)<<(b*8));
}
if(id===0x00)result.pid=val;
else if(id===0x01)result.battery=val;
else if(id===0x3A)result.button=val;
}
i+=size;
}
return result;
}
function parseEddystoneBatt(raw){
if(!raw||raw.length<4)return -1;
if(raw.charCodeAt(0)!==0x20)return -1;
let mV=(raw.charCodeAt(2)<<8)|raw.charCodeAt(3);
if(mV===0)return -1;
let pct=Math.round((mV-2000)/10);
if(pct<0)return 0;
if(pct>100)return 100;
return pct;
}
function parseIBeacon(advData){
if(!advData)return null;
let raw=BLE.GAP.ParseManufacturerData(advData);
if(!raw||raw.length<25)return null;
if(raw.charCodeAt(0)!==0x4C||raw.charCodeAt(1)!==0x00)return null;
if(raw.charCodeAt(2)!==0x02||raw.charCodeAt(3)!==0x15)return null;
let u="";
for(let i=4;i<20;i++){
u+=toHex(raw.charCodeAt(i));
if(i===7||i===9||i===11||i===13)u+="-";
}
let txp=raw.charCodeAt(24);
if(txp>127)txp-=256;
return {uuid:u,major:(raw.charCodeAt(20)<<8)|raw.charCodeAt(21),minor:(raw.charCodeAt(22)<<8)|raw.charCodeAt(23),txpower:txp};
}
function isCalibBeacon(ib){
if(!ib||!ib.uuid)return false;
return ib.uuid.substring(0,8).toUpperCase()===CALIB_UUID_PREFIX;
}
function handleCalibBeacon(ib,rssi){
let key=ib.major+":"+ib.minor;
let e=calibBuf[key];
if(!e){e={s:0,c:0,t:ib.txpower};calibBuf[key]=e;}
e.s+=rssi;
e.c++;
}
function sendCalibration(){
let readings=[];
for(let key in calibBuf){
let e=calibBuf[key];
if(e.c===0)continue;
let parts=key.split(":");
let avg=Math.round(e.s/e.c);
readings.push({major:parseInt(parts[0]),minor:parseInt(parts[1]),rssi:avg,distance:Math.round(Math.pow(10,(e.t-avg)/25)*100)/100});
}
calibBuf={};
if(readings.length===0)return;
if(C.debug)print("[CAL] "+readings.length);
post(_P+"calibration",{scanner:C.name,readings:readings},null);
}
function sendButtonEvent(mac,event,rssi,pid){
let names=["none","press","double_press","triple_press","long_press"];
let name=(event>=1&&event<=4)?names[event]:"unknown";
if(C.debug)print("[BTN] "+mac+" -> "+name+" pid="+JSON.stringify(pid));
post(_P+"event",{scanner:C.name,mac:mac,event:name,rssi:rssi,pid:pid,timestamp:ts()},null);
}
function loadKnown(macs){
known={};
knownUuid={};
if(!macs)return;
for(let i=0;i<macs.length;i++){
let m=macs[i];
if(m.length===12)known[m]=true;
else if(m.length===36&&m.indexOf("-")>0)knownUuid[m]=true;
}
}
function register(){
print("[REG] Registering...");
let info=Shelly.getDeviceInfo();
let wifi=Shelly.getComponentStatus("wifi");
post(_P+"register",{
scanner:C.name,
ip:(wifi&&wifi.sta_ip)?wifi.sta_ip:"0.0.0.0",
model:info?(info.model||"Shelly"):"Shelly",
firmware:info?(info.fw_id||"?"):"?",
scan_interval:C.scan_interval
},function(d){
if(!d||!d.success){
print("[REG] Failed, retry 10s");
if(retryTimer)Timer.clear(retryTimer);
retryTimer=Timer.set(10000,false,register);
return;
}
loadKnown(d.known_macs);
if(C.debug){
let cM=0,cU=0;
for(let k in known)cM++;
for(let k in knownUuid)cU++;
print("[REG] OK: "+cM+"M "+cU+"U");
}else{
print("[REG] OK");
}
mem("post-reg");
if(d.scan_interval)C.scan_interval=d.scan_interval;
ready=true;
startScan();
startTimers();
});
}
function startScan(){
if(scanning)return;
BLE.Scanner.Subscribe(onScan);
BLE.Scanner.Start({duration_ms:-1,active:true});
scanning=true;
}
function onScan(ev,res){
if(ev!==BLE.Scanner.SCAN_RESULT||!res||!res.addr)return;
if(discMode){
let mac=res.addr.toUpperCase();
let ib=parseIBeacon(res.advData);
let dk=ib?ib.uuid:macKey(mac);
if(!discDevs[dk]||res.rssi>discDevs[dk].r){
let entry={m:mac,n:res.local_name||"",r:res.rssi,tp:"mac"};
if(ib){
entry.tp="ibeacon";
entry.iu=ib.uuid.toUpperCase();
entry.ima=ib.major;
entry.imi=ib.minor;
}else if(res.service_data&&res.service_data[BTHOME]){
entry.tp="bthome";
}
discDevs[dk]=entry;
}
return;
}
let ib=parseIBeacon(res.advData);
if(ib&&isCalibBeacon(ib)){
handleCalibBeacon(ib,res.rssi);
return;
}
let mac=res.addr.toUpperCase();
let mk=macKey(mac);
let isMac=known[mk]===true;
let isUuid=false;
let dk=mk;
if(!isMac){
if(ib&&knownUuid[ib.uuid]){
isUuid=true;
dk=ib.uuid;
}else{
return;
}
}
let t=ts();
let d=devs[dk];
if(!d){
d={o:false,s:0,r:-100,m:mac,t:0,b:-1,u:isUuid,ui:""};
devs[dk]=d;
}
d.s=t;
d.r=res.rssi;
d.m=mac;
if(isUuid&&ib)d.ui=ib.uuid;
let raw=res.service_data?res.service_data[BTHOME]:null;
if(raw){
let pid=fastPid(raw);
if(pid>=0){
if(lastPid[dk]===pid)return;
lastPid[dk]=pid;
}
}
let bth=null;
if(raw)bth=parseBTHome(raw);
if(bth&&bth.battery>=0&&bth.battery<=100)d.b=bth.battery;
if(d.b<0&&res.service_data&&res.service_data[EDDYSTONE]){
let eB=parseEddystoneBatt(res.service_data[EDDYSTONE]);
if(eB>=0)d.b=eB;
}
if(bth&&bth.button>=1){
sendButtonEvent(mac,bth.button,res.rssi,bth.pid);
}
let wasOff=!d.o;
d.o=true;
let scan={address:mac,rssi:res.rssi,is_online:1,local_name:""};
if(d.b>=0)scan.battery=d.b;
if(isUuid&&ib){
scan.ibeacon_uuid=ib.uuid;
scan.ibeacon_major=ib.major;
scan.ibeacon_minor=ib.minor;
scan.ibeacon_txpower=ib.txpower;
}
if(wasOff){
if(C.debug){
let l=isUuid?dk.substring(0,8)+"...":mac;
print("[ON] "+l+" R:"+res.rssi);
}
if(pending.length<20)pending.push(scan);
d.t=t;
sendBatch();
}else if(t-d.t>=C.scan_interval){
if(pending.length<20)pending.push(scan);
d.t=t;
}
}
function checkOffline(){
let t=ts();
let ct=C.offline_after*3;
for(let mk in devs){
let d=devs[mk];
let age=t-d.s;
if(d.o&&age>=C.offline_after){
d.o=false;
d.t=t;
if(C.debug){
let l=d.u?d.ui.substring(0,8)+"...":d.m;
print("[OFF] "+l);
}
let os={address:d.m,rssi:-100,is_online:0,local_name:""};
if(d.u&&d.ui)os.ibeacon_uuid=d.ui;
if(pending.length<20)pending.push(os);
}
if(!d.o&&age>=ct){
delete devs[mk];
delete lastPid[mk];
}
}
}
function sendBatch(){
if(!ready||sending)return;
checkOffline();
if(pending.length===0)return;
let scans=pending;
pending=[];
sending=true;
if(C.debug)print("[TX] "+scans.length);
post(_P+"scan",{scanner:C.name,scans:scans,discovery:false},function(d){
sending=false;
if(!d)print("[ERR] Batch failed");
});
}
function heartbeat(){
if(!ready)return;
sendCalibration();
Shelly.call("Sys.GetStatus",{},function(sr){
let hb={scanner:C.name,uptime:ts()-bootTs};
if(sr&&sr.ram_free){
hb.free_mem=sr.ram_free;
if(C.debug)print("[MEM] hb free="+sr.ram_free);
}
post(_P+"heartbeat",hb,function(d){
if(!d||!d.success){
print("[ERR] HB failed");
ready=false;
if(retryTimer)Timer.clear(retryTimer);
retryTimer=Timer.set(5000,false,register);
}
});
});
}
function offlineInit(){
offlineInitTimer=null;
let cnt=0;
for(let mk in known){
if(!devs[mk]){
if(pending.length<20){
pending.push({address:mk,rssi:-100,is_online:0,local_name:""});
cnt++;
}
}
}
for(let uu in knownUuid){
if(!devs[uu]){
let c="";
for(let j=0;j<uu.length;j++){
if(uu.charAt(j)!=="-")c+=uu.charAt(j);
}
if(pending.length<20){
pending.push({address:c,rssi:-100,is_online:0,local_name:"",ibeacon_uuid:uu});
cnt++;
}
}
}
if(cnt>0)sendBatch();
}
function startTimers(){
if(batchTimer)Timer.clear(batchTimer);
batchTimer=Timer.set(C.scan_interval*1000,true,sendBatch);
if(hbTimer)Timer.clear(hbTimer);
hbTimer=Timer.set(C.hb_interval*1000,true,heartbeat);
if(offlineInitTimer)Timer.clear(offlineInitTimer);
offlineInitTimer=Timer.set(C.offline_after*1000,false,offlineInit);
if(pollTimer)Timer.clear(pollTimer);
pollTimer=Timer.set(300000,true,pollDevices);
}
function startDiscovery(duration){
if(discMode)return "already running";
if(!duration||duration<5)duration=15;
discMode=true;
discDevs={};
if(discTimer)Timer.clear(discTimer);
discTimer=Timer.set(duration*1000,false,discEnd);
return "started";
}
function discEnd(){
discMode=false;
discTimer=null;
sendDiscoveryResults();
}
function sendDiscoveryResults(){
let devices=[];
for(let k in discDevs){
let dd=discDevs[k];
let entry={mac:dd.m,name:dd.n,rssi:dd.r,type:dd.tp};
if(dd.iu){
entry.ibeacon_uuid=dd.iu;
entry.ibeacon_major=dd.ima;
entry.ibeacon_minor=dd.imi;
}
devices.push(entry);
}
discDevs={};
let dUrl="http://"+C.host+"/ble/api.php?action=report_discovery";
if(C.api_key)dUrl+="&api_key="+C.api_key;
Shelly.call("HTTP.Request",{
method:"POST",url:dUrl,
body:JSON.stringify({client_id:C.name,timestamp:ts(),devices:devices}),
content_type:"application/json",timeout:15
},function(r,ec){
if(ec!==0||!r||r.code!==200)print("[DISC] Send failed");
});
}
function pollDevices(){
get(_P+"devices?scanner="+C.name,function(d){
if(!d||!d.success||!d.known_macs)return;
loadKnown(d.known_macs);
});
}
function status(){
let kon=0,koff=0,kn=0,ku=0;
for(let mk in devs){if(devs[mk].o)kon++;else koff++;}
for(let k in known)kn++;
for(let k in knownUuid)ku++;
return JSON.stringify({ready:ready,known_mac:kn,known_uuid:ku,online:kon,offline:koff,uptime:ts()-bootTs,pending:pending.length,discovery:discMode});
}
bootTs=ts();
print("=== BLE Presence Shelly v"+VERSION+" ===");
print("[BLE] "+C.name+" -> "+C.host+(C.api_key?" [API]":" [NO KEY]"));
mem("boot");
retryTimer=Timer.set(3000,false,register);


Kommentar