Инструменты пользователя

Инструменты сайта


iot:software:node-red:flows:zigbee_watchdog

Сторожевой таймер для Zigbee на Node-RED

В свое время я писал похожий таймер, который используется до сих пор для Wi-Fi сенсоров. А теперь, с увеличением сети Zigbee, пришло время написать что-то аналогичное, но с учетом специфики работы этих устройств.

Подготовка

Для работы этого потока, необходимо настроить функцию «Availability» и публикацию сообщений «Last Seen» в конфигурации Zigbee2MQTT. Кратко об этом написано тут.

Вот данные об интервале передачи диагностической информации пассивных устройств, которые есть у меня, и установленные мной значения задержки для режима доступности:

Производитель Модель Интервал Тайм-аут
SONOFF SNZB-02 30 60
SONOFF SNZB-03 81 85
SONOFF SNZB-04 81 85
Xiaomi GZCGQ01LM 55 60
Xiaomi SJCGQ11LM 50 60
TuYa ZG-204ZL 30 60
TuYa TS0041 225 240
TuYa TS0042 225 240
TuYa TS0043 225 240
TuYa 809WZT 240 250
TuYa IH012-RT01 240 250

Активные устройства, т.е. с питанием от сети, передают данные часто, поэтому тайм-аут в 5 минут вполне оправдан.

Нестандартные узлы

В этом потоке используются следующие узлы, которых нет в стандартной поставке:

Описание работы цепочки

Узел «mqtt in» подписывается и получает скопом все состояния устройств. Далее запрашивается из базы данных самое старое состояние, записывается текущее состояние и, если они не равны, формируется и отправляется письмо. Если отправка письма была успешная, это фиксируется в базе данных.

Скриншот потока

Второй узел «mqtt in» собирает статусы «last_seen» и сохраняет их в потоковый контекст для последующего использования в письмах.

В настройках указываются локализованные типы и местоположения устройств для формирования более понятных сообщений в письмах. К примеру, вместо «motionsensor corridor в сети!», будет отправлена строка «Датчик движения в коридоре в сети!».

Примеры писем

:!: Для корректной работы потока название устройства в Zigbee2MQTT должно быть в форме «тип/местоположение». К примеру, для датчика движения в коридоре название должно быть «motionsensor/corridor», а полный путь MQTT, вместе с базовой темой, – «zigbee2mqtt/motionsensor/corridor». В противном случае, путь будет не правильно разбираться!

Запрос для создания таблицы:

CREATE TABLE "statuses" (
	"_id"	INTEGER,
	"timestamp"	TEXT,
	"unixTime"	INTEGER,
	"type"	TEXT,
	"location"	TEXT,
	"status"	TEXT,
	"state"	INTEGER DEFAULT 0,
	PRIMARY KEY("_id" AUTOINCREMENT)
);

Код для загрузки

zigbee-watchdog.json
[
    {
        "id": "703a0f5a70803f11",
        "type": "tab",
        "label": "Сторожевой таймер",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "2f35ae8bcf662597",
        "type": "group",
        "z": "703a0f5a70803f11",
        "name": "Zigbee",
        "style": {
            "label": true
        },
        "nodes": [
            "41311c98adfcbb12",
            "48a419a95fbf7a9e",
            "c6017d6582894c46",
            "1660db2410dbe6b2",
            "bee2ed4d9a75ba41",
            "6c24d44625fe2af5",
            "1f2097283a89279f",
            "1c61b3cc0ad1f0c7",
            "ef7fcc8bc5fcae1e",
            "39f48e8c50923645",
            "c8800af4c1b22aa9",
            "dd5250e1c6271fea",
            "ddbae2ba5a9b5bc5",
            "b27b65899e35d282",
            "22e6577d2b9acb68",
            "6a816788de7b3eca",
            "fe04dc1fcec90d27",
            "4e999ed2724c2648",
            "f416a47ce8995a61"
        ],
        "x": 14,
        "y": 19,
        "w": 932,
        "h": 322
    },
    {
        "id": "41311c98adfcbb12",
        "type": "mqtt in",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Zigbee availability",
        "topic": "zigbee2mqtt/+/+/availability",
        "qos": "2",
        "datatype": "utf8",
        "broker": "1765f6372ba832dc",
        "nl": false,
        "rap": false,
        "inputs": 0,
        "x": 130,
        "y": 120,
        "wires": [
            [
                "48a419a95fbf7a9e"
            ]
        ]
    },
    {
        "id": "48a419a95fbf7a9e",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Выбор записей из БД",
        "rules": [
            {
                "t": "move",
                "p": "payload",
                "pt": "msg",
                "to": "status.current",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "device",
                "pt": "msg",
                "to": "$split (topic, \"/\" )",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "timestamp",
                "pt": "msg",
                "to": "[ $now( '[Y0001]-[M01]-[D01] [H01]:[m01]:[s01]', '+1000' ), $millis() ]",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "\"SELECT unixTime, status FROM statuses WHERE type = \\\"\" & device[1] & \"\\\" AND location = \\\"\" & \tdevice[2] & \"\\\" AND state = 0 LIMIT 1\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 380,
        "y": 120,
        "wires": [
            [
                "c6017d6582894c46"
            ]
        ]
    },
    {
        "id": "c6017d6582894c46",
        "type": "link call",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Вызов БД",
        "links": [
            "1660db2410dbe6b2"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 580,
        "y": 120,
        "wires": [
            [
                "1f2097283a89279f"
            ]
        ]
    },
    {
        "id": "1660db2410dbe6b2",
        "type": "link in",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "link in 3",
        "links": [],
        "x": 565,
        "y": 60,
        "wires": [
            [
                "bee2ed4d9a75ba41"
            ]
        ]
    },
    {
        "id": "bee2ed4d9a75ba41",
        "type": "sqlite",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "mydb": "03356afd1f8a24c9",
        "sqlquery": "msg.topic",
        "sql": "",
        "name": "Запрос к базе данных",
        "x": 740,
        "y": 60,
        "wires": [
            [
                "6c24d44625fe2af5"
            ]
        ]
    },
    {
        "id": "6c24d44625fe2af5",
        "type": "link out",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "link out 4",
        "mode": "return",
        "links": [],
        "x": 905,
        "y": 60,
        "wires": []
    },
    {
        "id": "1f2097283a89279f",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Запись в базу данных",
        "rules": [
            {
                "t": "set",
                "p": "timestamp",
                "pt": "msg",
                "to": "$append( timestamp, payload[0].unixTime )",
                "tot": "jsonata"
            },
            {
                "t": "move",
                "p": "payload[0].status",
                "pt": "msg",
                "to": "status.previous",
                "tot": "msg"
            },
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "\"INSERT INTO \\\"statuses\\\" ( timestamp, unixTime, type, location, status ) VALUES ( \\\"\" &\t    \t    timestamp[0] & \"\\\", \" & timestamp[1] & \", \\\"\" &\tdevice[1] & \"\\\", \\\"\" & device[2] & \"\\\", \\\"\" & status.current & \"\\\" )\"\t",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 820,
        "y": 120,
        "wires": [
            [
                "1c61b3cc0ad1f0c7"
            ]
        ]
    },
    {
        "id": "1c61b3cc0ad1f0c7",
        "type": "link call",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Вызов БД",
        "links": [
            "1660db2410dbe6b2"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 100,
        "y": 180,
        "wires": [
            [
                "ddbae2ba5a9b5bc5"
            ]
        ]
    },
    {
        "id": "ef7fcc8bc5fcae1e",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Подготовка письма",
        "rules": [
            {
                "t": "set",
                "p": "email.subject",
                "pt": "msg",
                "to": "\"Изменился статус устройства (\" & $now('[H01]:[m01]:[s01] [D01].[M01].[Y0001]', '+1000') & \")\"",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "payload",
                "pt": "msg",
                "to": "(\t\t    $localSymbol := function( $value ){(\t   $value := $string( $value );\t       $contains( $value, /1{1}[1-4]{1}$/ ) ? \"\" : \t       $contains( $value, /[2-4]{1}$/ ) ? \"ы\" : \t       $contains( $value, /1{1}$/ ) ? \"у\"   \t)};\t\t\t\t    $localMsgOnline := $count( timestamp ) = 3 ? (\t\t        $localTime := $round( ( timestamp[1] - timestamp[2] ) / 60000 );\t\t ( \"\\n\\nБыл недоступен (offline) \" & $localTime & \" минут\" & $localSymbol( $localTime ) & \".\" ) )\t    : \"\\n\\nВероятно, это новый датчик.\\n\\nДобавьте его название и расположение в настройках потока Node-RED.\" ;\t\t    $localLastSeen := $flowContext( \"lastSeen.\" & device[1] & \".\" & device[2] & \"[1]\" );\t\t$localLastSeen := $exists( $localLastSeen ) ? $round( ( timestamp[1] - $localLastSeen ) / 60000 ) : null;\t\t$localMsgOffline := $type( $localLastSeen ) = \"number\" ? \t( \"\\n\\nПоследнее сообщение (last seen) \" & $localLastSeen & \" минут\" & $localSymbol( $localLastSeen ) & \" назад.\" ) : \t\"\\n\\nВремя последнего сообщения неизвестно!\";\t\t    $localType := $lookup( $flowContext( \"names\" ), device[1] );\t    $localType := $exists( $localType ) ? $localType : device[1];\t\t    $localLocation := $lookup( $flowContext( \"names\" ), device[2] );\t    $localLocation := $exists( $localLocation ) ? $localLocation : device[2];\t\t    $localStatus := $lookup( $flowContext( \"names\" ), status.current );\t    $localStatus := $exists( $localStatus ) ? $localStatus : status.current;\t\t$localType & \" \" & $localLocation & \" \" & $localStatus\t& \"!\" & ( status.current = \"online\" ? $localMsgOnline : $localMsgOffline );\t\t)",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 560,
        "y": 180,
        "wires": [
            [
                "39f48e8c50923645"
            ]
        ]
    },
    {
        "id": "39f48e8c50923645",
        "type": "email-send",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "transport": "07868703c10c6510",
        "from": "nodered@domain.zone",
        "to": "nikolay@soloshin.su",
        "cc": "",
        "bcc": "",
        "subject": "",
        "contentType": "text",
        "name": "Отправка почты",
        "x": 830,
        "y": 180,
        "wires": [
            [
                "c8800af4c1b22aa9"
            ]
        ]
    },
    {
        "id": "c8800af4c1b22aa9",
        "type": "switch",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Фильтр статуса ответа",
        "property": "payload.response",
        "propertyType": "msg",
        "rules": [
            {
                "t": "cont",
                "v": "250 OK",
                "vt": "str"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 1,
        "x": 150,
        "y": 240,
        "wires": [
            [
                "dd5250e1c6271fea"
            ]
        ]
    },
    {
        "id": "dd5250e1c6271fea",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Обновление статуса в базе",
        "rules": [
            {
                "t": "set",
                "p": "topic",
                "pt": "msg",
                "to": "\"UPDATE statuses SET state = 1 WHERE type = \\\"\" & device[1] \t& \"\\\" AND location = \\\"\" & device[2] \t& \"\\\" AND status != \\\"\" & status.current \t& \"\\\" AND state != 1\"",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 500,
        "y": 240,
        "wires": [
            [
                "f416a47ce8995a61"
            ]
        ]
    },
    {
        "id": "ddbae2ba5a9b5bc5",
        "type": "switch",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Фильтр повторов",
        "property": "status.current",
        "propertyType": "msg",
        "rules": [
            {
                "t": "neq",
                "v": "status.previous",
                "vt": "msg"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 1,
        "x": 310,
        "y": 180,
        "wires": [
            [
                "ef7fcc8bc5fcae1e"
            ]
        ]
    },
    {
        "id": "b27b65899e35d282",
        "type": "mqtt in",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Zigbee last seen",
        "topic": "zigbee2mqtt/+/+",
        "qos": "2",
        "datatype": "auto-detect",
        "broker": "1765f6372ba832dc",
        "nl": false,
        "rap": false,
        "inputs": 0,
        "x": 120,
        "y": 300,
        "wires": [
            [
                "6a816788de7b3eca"
            ]
        ]
    },
    {
        "id": "22e6577d2b9acb68",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Сохранение в контекст",
        "rules": [
            {
                "t": "set",
                "p": "path",
                "pt": "msg",
                "to": "$split(topic, \"/\")",
                "tot": "jsonata"
            },
            {
                "t": "set",
                "p": "lastSeen[msg.path[1]][msg.path[2]]",
                "pt": "flow",
                "to": "[\t    payload.last_seen,\t$toMillis( payload.last_seen )\t\t]",
                "tot": "jsonata"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 570,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "6a816788de7b3eca",
        "type": "switch",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Фильтрация",
        "property": "payload.last_seen",
        "propertyType": "msg",
        "rules": [
            {
                "t": "nempty"
            }
        ],
        "checkall": "false",
        "repair": false,
        "outputs": 1,
        "x": 330,
        "y": 300,
        "wires": [
            [
                "22e6577d2b9acb68"
            ]
        ]
    },
    {
        "id": "fe04dc1fcec90d27",
        "type": "inject",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Настройки",
        "props": [
            {
                "p": "names.online",
                "v": "в сети",
                "vt": "str"
            },
            {
                "p": "names.offline",
                "v": "не в сети",
                "vt": "str"
            },
            {
                "p": "names.router",
                "v": "Роутер Zigbee",
                "vt": "str"
            },
            {
                "p": "names.switch",
                "v": "Выключатель",
                "vt": "str"
            },
            {
                "p": "names.lightsensor",
                "v": "Датчик освещенности",
                "vt": "str"
            },
            {
                "p": "names.motionsensor",
                "v": "Датчик движения",
                "vt": "str"
            },
            {
                "p": "names.contactsensor",
                "v": "Датчик положения двери",
                "vt": "str"
            },
            {
                "p": "names.tempandhumsensor",
                "v": "Датчик температуры и влажности",
                "vt": "str"
            },
            {
                "p": "names.leaksensor",
                "v": "Датчик протечки",
                "vt": "str"
            },
            {
                "p": "names.bathroom",
                "v": "в ванной",
                "vt": "str"
            },
            {
                "p": "names.corridor",
                "v": "в коридоре",
                "vt": "str"
            },
            {
                "p": "names.anteroom",
                "v": "в предбаннике",
                "vt": "str"
            },
            {
                "p": "names.bedroom",
                "v": "в спальне",
                "vt": "str"
            },
            {
                "p": "names.balcony",
                "v": "на балконе",
                "vt": "str"
            },
            {
                "p": "names.outdoor",
                "v": "на улице",
                "vt": "str"
            },
            {
                "p": "names.kitchen",
                "v": "на кухне",
                "vt": "str"
            },
            {
                "p": "names.storeroom",
                "v": "в кладовке",
                "vt": "str"
            },
            {
                "p": "names.nursery",
                "v": "в детской",
                "vt": "str"
            },
            {
                "p": "names.bathroom_toilet",
                "v": "в зоне туалета",
                "vt": "str"
            },
            {
                "p": "names.bathroom_tub",
                "v": "в зоне ванны",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "x": 130,
        "y": 60,
        "wires": [
            [
                "4e999ed2724c2648"
            ]
        ]
    },
    {
        "id": "4e999ed2724c2648",
        "type": "change",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Сохранение в контекст",
        "rules": [
            {
                "t": "move",
                "p": "names",
                "pt": "msg",
                "to": "names",
                "tot": "flow"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 390,
        "y": 60,
        "wires": [
            []
        ]
    },
    {
        "id": "f416a47ce8995a61",
        "type": "link call",
        "z": "703a0f5a70803f11",
        "g": "2f35ae8bcf662597",
        "name": "Вызов базы данных",
        "links": [
            "1660db2410dbe6b2"
        ],
        "linkType": "static",
        "timeout": "30",
        "x": 820,
        "y": 240,
        "wires": [
            []
        ]
    },
    {
        "id": "1765f6372ba832dc",
        "type": "mqtt-broker",
        "name": "localhost",
        "broker": "localhost",
        "port": "1883",
        "clientid": "",
        "autoConnect": true,
        "usetls": false,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "false",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willRetain": "false",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    },
    {
        "id": "03356afd1f8a24c9",
        "type": "sqlitedb",
        "db": "/databases/watchdog/zigbee.db",
        "mode": "RWC"
    },
    {
        "id": "07868703c10c6510",
        "type": "email-transport",
        "name": "General",
        "host": "smtp.com",
        "port": "465",
        "secure": true,
        "authType": "none",
        "proxy": ""
    }
]

Дисклеймер

  • Использование материалов данной базы знаний разрешено на условиях лицензии, указанной внизу каждой страницы! При использовании материалов активная гиперссылка на соответствующую страницу данной базы знаний обязательна!
  • Автор не несет и не может нести какую либо ответственность за последствия использования материалов, размещенных в данной базе знаний. Все материалы предоставляются по принципу «как есть». Используйте их исключительно на свой страх и риск.
  • Все высказывания, мысли или идеи автора, размещенные в материалах данной базе знаний, являются исключительно его личным субъективным мнением и могут не совпадать с мнением читателей!
  • При размещении ссылок в данной базе знаний на интернет-страницы третьих лиц автор не несет ответственности за их техническую функциональность (особенно отсутствие вирусов) и содержание! При обнаружении таких ссылок, можно и желательно сообщить о них в комментариях к соответствующей статье.

Обсуждение

Ваш комментарий:
I L E V D V᠎ F Z N V F D O S D H
 
Последнее изменение: 2023/12/03 20:11 — Николай Солошин