Skrypt bramy BLE Shelly Gen2 do integracji z BLU Button1

Umożliw płynną interakcję między Shelly BLU Button1 a innymi urządzeniami Shelly dzięki temu skryptowi Gen2, który obsługuje lokalne żądania API na podstawie zdarzeń przycisku. Użytkownicy mogą przypisywać konkretne adresy URL do akcji przycisku, takich jak pojedyncze, podwójne i potrójne naciśnięcia. Skrypt efektywnie kolejkuje i przetwarza żądania URL, z zastrzeżeniem limitu pięciu jednoczesnych wywołań RPC. Przed wdrożeniem skonfiguruj adres MAC dla swojego BLU Button1 i zdefiniuj pożądane akcje dla każdego zdarzenia przycisku. Idealny do automatyzacji reakcji w różnych ekosystemach Shelly, ten skrypt pozwala usprawnić sterowanie różnymi generacjami urządzeń.

* Co należy zmienić przed użyciem:
bluButtonAddress -> Tutaj należy wpisać adres MAC swojego przycisku blu.
Ten skrypt pomoże Ci znaleźć adres MAC: https://github.com/ALLTERCO/shelly-script-examples/blob/main/ble-shelly-scanner.js

Akcje:

Tutaj należy wpisać adresy URL, które mają zostać wykonane przy określonym zdarzeniu. Adresy URL, które mają być wywoływane przy pojedynczym/krótkim naciśnięciu przycisku, należy umieścić w obiekcie singlePush. To samo dotyczy podwójnego i potrójnego naciśnięcia. Przykład poniżej.

Ograniczenia:
Obecnie istnieje limit 5 wywołań RPC w tym samym czasie i dlatego skrypt będzie wykonywał co 3 adresy URL z 1-sekundowym opóźnieniem.
Ograniczenia można sprawdzić tutaj: https://shelly-api-docs.shelly.cloud/gen2/Scripts/ShellyScriptLanguageFeatures#resource-limits

Kolejność wykonywania adresów URL nie może być zagwarantowana

        javascript
/* =========ZMIENIĆ TUTAJ========= */
let CONFIG = {
    bluButtonAddress: "b4:35:22:fe:56:e5", //adres MAC przycisku shelly blu button1, który będzie wyzwalał akcje
    actions: { //adresy URL wywoływane przy zdarzeniu
        //przy dodawaniu adresów URL musisz oddzielać je przecinkami i ujmować w cudzysłowy
        singlePush: [ //adresy URL, które zostaną wykonane przy zdarzeniu singlePush z przycisku blu button1
            "http://192.168.1.35/roller/0?go=open",
            "http://192.168.1.36/relay/0?turn=off",
            "http://192.168.1.36/relay/1?turn=on"
        ],
        doublePush: [ //adresy URL, które zostaną wykonane przy zdarzeniu doublePush z przycisku blu button1
            "http://192.168.1.35/roller/0?go=close"
        ],
        triplePush: [ //adresy URL, które zostaną wykonane przy zdarzeniu triplePush z przycisku blu button1
            "http://192.168.1.38/color/0?turn=on&red=200&green=0&blue=0",
            "http://192.168.1.38/light/0?turn=on",
            "http://192.168.1.40/rpc/Switch.Set?id=0&on=false",
            "http://192.168.1.40/rpc/Switch.Set?id=1&on=false"
        ],
        longPush: [ //adresy URL, które zostaną wykonane przy zdarzeniu longPush z przycisku blu button1
            "http://192.168.1.41/rpc/Cover.Close",
            "http://192.168.1.42/rpc/Cover.Close"
        ]
    }
};
/** =============================== PRZESTAŃ ZMIENIAĆ TUTAJ =============================== */

let urlsPerCall = 3; 
let urlsQueue = [];
let callsCounter = 0;

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;

let BTH = {};
BTH[0x00] = { n: "pid", t: uint8 };
BTH[0x01] = { n: "Battery", t: uint8, u: "%" };
BTH[0x05] = { n: "Illuminance", t: uint24, f: 0.01 };
BTH[0x1a] = { n: "Door", t: uint8 };
BTH[0x20] = { n: "Moisture", t: uint8 };
BTH[0x2d] = { n: "Window", t: uint8 };
BTH[0x3a] = { n: "Button", t: uint8 };

function getByteSize(type) {
    if (type === uint8 || type === int8) return 1;
    if (type === uint16 || type === int16) return 2;
    if (type === uint24 || type === int24) return 3;
    //niemożliwe, ponieważ reklamy są znacznie mniejsze;
    return 255;
}

let BTHomeDecoder = {
    utoi: function (num, bitsz) {
        let 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;
    },
    unpack: function (buffer) {
        //beacony mogą nie udostępniać danych usługi BTH
        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;
        //nie można obsłużyć zaszyfrowanych danych
        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: nieznany typ");
                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;
            buffer = buffer.slice(getByteSize(_bth.t));
        }
        return result;
    },
};

function callQueue() {
    if(callsCounter < 6 - urlsPerCall) {
        for(let i = 0; i < urlsPerCall && i < urlsQueue.length; i++) {
            let url = urlsQueue.splice(0, 1)[0];
            callsCounter++;
            Shelly.call("HTTP.GET", { 
                url: url, 
                timeout: 5
                }, 
                function(_, error_code, _, data) {
                    if(error_code !== 0) {
                        console.log("Wywołanie", data.url, "nie powiodło się");
                    }
                    else {
                        console.log("Wywołanie", data.url, "powiodło się");
                    }

                    callsCounter--;
                }, 
                { url: url }
            );
        }
    }

    //jeśli w kolejce są jeszcze adresy URL
    if(urlsQueue.length > 0) {
        Timer.set(
            1000, //opóźnienie
            false, 
            function() {
                callQueue();
            }
        );
    }
}

let lastPacketId = 0x100;
function bleScanCallback(event, result) {
    //zakończ, jeśli wywołanie nie dotyczy otrzymanego wyniku
    if (event !== BLE.Scanner.SCAN_RESULT) {
        return;
    }

    //zakończ, jeśli dane nie pochodzą z Shelly Blu button1 i jeśli adres MAC się nie zgadza
    if (    typeof result.local_name === "undefined" || 
            typeof result.addr === "undefined" || 
            result.local_name.indexOf("SBBT") !== 0 || 
            result.addr !== CONFIG.bluButtonAddress
    ) {
        return;
    }

    let servData = result.service_data;

    //zakończ, jeśli dane usługi są null/urządzenie jest zaszyfrowane
    if(servData === null || typeof servData === "undefined" || typeof servData[BTHOME_SVC_ID_STR] === "undefined") {
        console.log("Nie można obsłużyć zaszyfrowanych urządzeń");
        return;
    }

    let receivedData = BTHomeDecoder.unpack(servData[BTHOME_SVC_ID_STR]);

    //zakończ, jeśli rozpakowane dane są null lub urządzenie jest zaszyfrowane
    if(receivedData === null || typeof receivedData === "undefined" || receivedData["encryption"]) {
        console.log("Nie można obsłużyć zaszyfrowanych urządzeń");
        return;
    }

    //zakończ, jeśli zdarzenie jest zduplikowane
    if (lastPacketId === receivedData.pid) {
        return;
    }

    lastPacketId = receivedData["pid"];

    //pobieranie i wykonywanie akcji
    let actionType = ["", "singlePush", "doublePush", "triplePush", "longPush"][receivedData["Button"]];

    let actionUrls = CONFIG.actions[actionType];

    //zakończ, jeśli zdarzenie nie istnieje w konfiguracji    
    if(typeof actionType === "undefined") {
        console.log("Nieznany typ zdarzenia w konfiguracji");
        return;
    }

    //zapisz wszystkie adresy URL do kolejki dla bieżącego zdarzenia
    for(let i in actionUrls) {
        urlsQueue.push(actionUrls[i]);
    }

    callQueue();
}

function bleScan() {
    //sprawdź, czy Bluetooth jest włączony
    let bleConfig = Shelly.getComponentConfig("ble");

    //zakończ, jeśli Bluetooth nie jest włączony
    if(bleConfig.enable === false) {
        console.log("BLE nie jest włączony");
        return;
    }

    //uruchom skaner
    let bleScanner = BLE.Scanner.Start({
        duration_ms: BLE.Scanner.INFINITE_SCAN,
        active: true
    });

    //zakończ, jeśli nie można uruchomić skanera
    if(bleScanner === false) {
        console.log("Błąd podczas uruchamiania skanera BLE");
        return;
    }

    BLE.Scanner.Subscribe(bleScanCallback);
    console.log("BLE został pomyślnie uruchomiony");
}

function init() {
    //zakończ, jeśli nie ma konfiguracji
    if(typeof CONFIG === "undefined") {
        console.log("Nie można odczytać konfiguracji");
        return;
    }

    //zakończ, jeśli nie ma adresu przycisku blu
    if(typeof CONFIG.bluButtonAddress !== "string") {
        console.log("Błąd adresu przycisku Shelly BLU button1");
        return;
    }
    
    //zakończ, jeśli nie ma obiektu akcji
    if(typeof CONFIG.actions === "undefined") {
        console.log("Nie można znaleźć obiektu akcji w konfiguracji");
        return;
    }

    //uruchom skan BLE
    bleScan();
}

//uruchom skrypt
init();