В свое время я писал похожий таймер, который используется до сих пор для 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) );
[ { "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": "" } ]
Обсуждение