Jump to content
🌟 NOTIFICATION/Benachrichtigung: Welcome to our New Store! - shelly.com 🌟 ×

Script: Shelly BLU motion switches on the light after sunset


al3x

Recommended Posts

I have developed a script that turns on my light as soon as the Shelly BLU motion detects movement. This only happens after sunset. The sunset time is determined via an Astro service. When the Shelly BLU motion no longer detects movement, the light is turned off again.
The script can be expanded, and additional events such as light intensity can be incorporated. The script runs on a Shelly Pro Dimmer 2PM.

The required inputs are the MAC address of the BLU motion and the latitude and longitude of the BLU motion's location.

// v0.3

/******************* START CHANGE HERE *******************/
let CONFIG = {
    eventName: "shelly-blu",
    debug: false,
    active: false,
    mac: "b0:c7:de:XX:XX:XX",
    timeoutId: null,  // New timeout handler

    onMotionChange: function (motion) {
        if (motion) {
            // If motion is detected, reset the timer to turn off the light
            if (CONFIG.timeoutId) {
                clearTimeout(CONFIG.timeoutId);
                CONFIG.timeoutId = null;
                console.log("Timer has been reset.");
            }

            // On motion, check if it is after sunset
            checkSunsetTime(function(isAfterSunset) {
                if (isAfterSunset) {
                    Shelly.call("Light.set", { id: 0, on: true });
                    console.log("Light turned on.");
                } else {
                    console.log("It's before sunset. Light remains off.");
                }
            });
        } else {
            // If no more motion is detected, start a 3-minute timer to turn off the light
            CONFIG.timeoutId = setTimeout(function () {
                Shelly.call("Light.set", { id: 0, on: false });
                console.log("Light turned off after 3 minutes.");
                CONFIG.timeoutId = null; // Reset the timer
            }, 180000);  // 3 minutes in milliseconds
            console.log("3-minute timer started.");
        }
    },
};
/******************* STOP CHANGE HERE *******************/

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[0x02] = { n: "temperature", t: int16, f: 0.01, u: "tC" };
BTH[0x03] = { n: "humidity", t: uint16, f: 0.01, u: "%" };
BTH[0x05] = { n: "illuminance", t: uint24, f: 0.01 };
BTH[0x21] = { n: "motion", t: uint8 };
BTH[0x2d] = { n: "window", t: uint8 };
BTH[0x3a] = { n: "button", t: uint8 };
BTH[0x3f] = { n: "rotation", t: int16, f: 0.1 };

function logger(message, prefix) {
    if (!CONFIG.debug) {
        return;
    }
    let finalText = Array.isArray(message) ? message.map(item => JSON.stringify(item)).join(" ") : JSON.stringify(message);
    console.log(`${prefix ? prefix + ":" : ""} ${finalText}`);
}

function getByteSize(type) {
    if (type === uint8 || type === int8) return 1;
    if (type === uint16 || type === int16) return 2;
    if (type === uint24 || type === int24) return 3;
    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) << 😎 | 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) << 😎 | 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) {
        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;
        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") {
                logger("unknown type", "BTH");
                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;
    },
};

let lastPacketId = 0x100;

// Function to retrieve sunset time
function formatDate(date) {
    let day = ("0" + date.getDate()).slice(-2);
    let month = ("0" + (date.getMonth() + 1)).slice(-2); // Months are zero-based
    let year = date.getFullYear();
    let hours = ("0" + date.getHours()).slice(-2);
    let minutes = ("0" + date.getMinutes()).slice(-2);
    return day + "." + month + "." + year + " " + hours + ":" + minutes;
}

function fetchSunsetTime(callback) {
    let astroServiceURL = "https://api.sunrise-sunset.org/json?lat=50.1234567&lng=9.1234567&formatted=0";
    
    Shelly.call("HTTP.GET", { url: astroServiceURL }, function (result) {
        if (result && result.code === 200) {
            let data = JSON.parse(result.body);
            if (data && data.results && data.results.sunset) {
                let sunsetTime = new Date(data.results.sunset);
                console.log("Sunset time: " + formatDate(sunsetTime));
                callback(sunsetTime);
            } else {
                console.error("Error: Invalid data structure");
                callback(null);
            }
        } else {
            console.error("Error retrieving sunset time");
            callback(null);
        }
    });
}

// Function to check if it's after sunset
function checkSunsetTime(callback) {
    fetchSunsetTime(function(sunsetTime) {
        if (sunsetTime) {
            let currentTime = new Date();
            callback(currentTime >= sunsetTime); // Return true if after sunset
        } else {
            callback(false);
        }
    });
}

// Callback for BLE scanner
function bleScanCallback(event, result) {
    if (event !== BLE.Scanner.SCAN_RESULT) {
        return;
    }

    if (
        typeof result.service_data === "undefined" ||
        typeof result.service_data[BTHOME_SVC_ID_STR] === "undefined"
    ) {
        logger("Missing service_data member", "Error");
        return;
    }

    let unpackedData = BTHomeDecoder.unpack(
        result.service_data[BTHOME_SVC_ID_STR]
    );

    if (
        unpackedData === null ||
        typeof unpackedData === "undefined" ||
        unpackedData["encryption"]
    ) {
        logger("Encrypted devices are not supported", "Error");
        return;
    }

    if (lastPacketId === unpackedData.pid) {
        return;
    }

    lastPacketId = unpackedData.pid;

    unpackedData["rssi"] = result.rssi;
    unpackedData["mac"] = result.addr;

    logger([unpackedData, result], "RECEIVED");

    if (unpackedData["mac"] !== CONFIG.mac) {
        return;
    }

    if (typeof unpackedData.motion !== "undefined") {
        CONFIG.onMotionChange(unpackedData.motion === 1);
    }
}

BLE.Scanner.Start({ duration: BLE.SCANNING_DURATION_CONTINUOUS }, bleScanCallback);
 

ShellyProDimmer2_electrical_plan.png

Edited by al3x
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Erstelle neue...