From 1558f0c7315ffc69bc251d44207db86a42d0c24e Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Sat, 12 Dec 2020 19:05:47 +0100 Subject: [PATCH] Zigbee add visual map of network --- CHANGELOG.md | 1 + tasmota/xdrv_01_webserver.ino | 12 ++ tasmota/xdrv_23_zigbee_1_headers.ino | 4 + tasmota/xdrv_23_zigbee_2_devices.ino | 6 + ...no => xdrv_23_zigbee_7_0_statemachine.ino} | 0 tasmota/xdrv_23_zigbee_7_5_map.ino | 180 ++++++++++++++++++ tasmota/xdrv_23_zigbee_8_parsers.ino | 25 ++- tasmota/xdrv_23_zigbee_A_impl.ino | 109 +++++++++-- 8 files changed, 323 insertions(+), 14 deletions(-) rename tasmota/{xdrv_23_zigbee_7_statemachine.ino => xdrv_23_zigbee_7_0_statemachine.ino} (100%) create mode 100644 tasmota/xdrv_23_zigbee_7_5_map.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a64d5f62..9a9ce16b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ All notable changes to this project will be documented in this file. - Zigbee better support for Tuya Protocol (#10074) - Support for SPI connected MFRC522 13.56MHz rfid card reader (#9916) - Letsencrypt R3 in addition to X3 CA (#10086) +- Zigbee add visual map of network ### Breaking Changed - KNX DPT9 (16-bit float) to DPT14 (32-bit float) by Adrian Scillato (#9811, #9888) diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index e9ae48e78..7e74f2e89 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -1661,6 +1661,18 @@ bool HandleRootStatusRefresh(void) ExecuteWebCommand(svalue, SRC_WEBGUI); } #endif // USE_SONOFF_RF +#ifdef USE_ZIGBEE + WebGetArg("zbj", tmp, sizeof(tmp)); + if (strlen(tmp)) { + snprintf_P(svalue, sizeof(svalue), PSTR("ZbPermitJoin")); + ExecuteWebCommand(svalue, SRC_WEBGUI); + } + WebGetArg("zbr", tmp, sizeof(tmp)); + if (strlen(tmp)) { + snprintf_P(svalue, sizeof(svalue), PSTR("ZbMap")); + ExecuteWebCommand(svalue, SRC_WEBGUI); + } +#endif // USE_ZIGBEE WSContentBegin(200, CT_HTML); WSContentSend_P(PSTR("{t}")); XsnsCall(FUNC_WEB_SENSOR); diff --git a/tasmota/xdrv_23_zigbee_1_headers.ino b/tasmota/xdrv_23_zigbee_1_headers.ino index 04448711c..33ca5381e 100644 --- a/tasmota/xdrv_23_zigbee_1_headers.ino +++ b/tasmota/xdrv_23_zigbee_1_headers.ino @@ -90,6 +90,10 @@ public: bool recv_until = false; // ignore all messages until the received frame fully matches bool eeprom_present = false; // is the ZBBridge EEPROM present? bool eeprom_ready = false; // is the ZBBridge EEPROM formatted and ready? + // Zigbee mapping + bool mapping_in_progress = false; // is there a mapping in progress + bool mapping_ready = false; // do we have mapping information ready + uint32_t mapping_end_time = 0; uint8_t on_error_goto = ZIGBEE_LABEL_ABORT; // on error goto label, 99 default to abort uint8_t on_timeout_goto = ZIGBEE_LABEL_ABORT; // on timeout goto label, 99 default to abort diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index 3c6137bc5..8532e5da8 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -717,6 +717,7 @@ public: // sequence number for Zigbee frames uint16_t shortaddr; // unique key if not null, or unspecified if null uint8_t seqNumber; + bool is_router; // flag used by ZbMap to distibguish routers from end-devices bool hidden; bool reachable; // Light information for Hue integration integration, last known values @@ -742,6 +743,7 @@ public: attr_list(), shortaddr(_shortaddr), seqNumber(0), + is_router(false), hidden(false), reachable(false), data(), @@ -768,6 +770,10 @@ public: inline bool getReachable(void) const { return reachable; } inline bool getPower(uint8_t ep =0) const; + inline bool isRouter(void) const { return is_router; } + inline bool isCoordinator(void) const { return 0x0000 == shortaddr; } + inline void setRouter(bool router) { is_router = router; } + inline void setLQI(uint8_t _lqi) { lqi = _lqi; } inline void setBatteryPercent(uint8_t bp) { batterypercent = bp; } diff --git a/tasmota/xdrv_23_zigbee_7_statemachine.ino b/tasmota/xdrv_23_zigbee_7_0_statemachine.ino similarity index 100% rename from tasmota/xdrv_23_zigbee_7_statemachine.ino rename to tasmota/xdrv_23_zigbee_7_0_statemachine.ino diff --git a/tasmota/xdrv_23_zigbee_7_5_map.ino b/tasmota/xdrv_23_zigbee_7_5_map.ino new file mode 100644 index 000000000..17c79d5e5 --- /dev/null +++ b/tasmota/xdrv_23_zigbee_7_5_map.ino @@ -0,0 +1,180 @@ +/* + xdrv_23_zigbee_7_5_map.ino - zigbee support for Tasmota + + Copyright (C) 2020 Theo Arends and Stephan Hadinger + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifdef USE_ZIGBEE + +class Z_Mapper_Edge { +public: + + // enum Edge_Type : uint8_t { + // Unknown = 0x00, + // Parent = 0x01, // node_1 is parent of node_2 + // Child = 0x02, // node_1 is child of node_2 + // Sibling = 0x03, // both nodes are siblings + // }; + + Z_Mapper_Edge(void) : + node_1(BAD_SHORTADDR), + node_2(BAD_SHORTADDR), + lqi(0x00) + // edge_type(Unknown) + {} + + // Z_Mapper_Edge(uint16_t node_a, uint16_t node_b, uint8_t _lqi, Edge_Type _type) : + Z_Mapper_Edge(uint16_t node_a, uint16_t node_b, uint8_t _lqi) : + node_1(BAD_SHORTADDR), + node_2(BAD_SHORTADDR), + lqi(_lqi) + // edge_type(_type) + { + setEdges(node_a, node_b); + } + + void setEdges(uint16_t node_a, uint16_t node_b); + + bool sameEdge(const Z_Mapper_Edge & edge2) const; + + // Edge_Type Z_Mapper_Edge_Type_Reverse(Edge_Type _type) { + // switch (_type) { + // case Parent: return Child; + // case Child: return Parent; + // default: return _type; + // } + // } + + // we always orientate the edge from with shortaddresses in ascending order + // invariant: node_1 < node_2 + uint16_t node_1; + uint16_t node_2; + + uint8_t lqi; + // Edge_Type edge_type; +}; + +// +// Handles the mapping of Zigbee devices +// +class Z_Mapper { +public: + Z_Mapper(void) : + edges() + {} + + void reset(void) { edges.reset(); } + + Z_Mapper_Edge & findEdge(const Z_Mapper_Edge & edge2); + bool addEdge(const Z_Mapper_Edge & edge2); + + void dumpInternals(void) const; + + LList edges; + +}; + +// global +Z_Mapper zigbee_mapper; + +/*********************************************************************************************\ + * Implementation for Z_Mapper_Edge +\*********************************************************************************************/ +void Z_Mapper_Edge::setEdges(uint16_t node_a, uint16_t node_b) { + if (node_a < node_b) { + node_1 = node_a; + node_2 = node_b; + } else if (node_a > node_b) { + node_1 = node_b; + node_2 = node_a; + } else { + // do nothing + } +} + +bool Z_Mapper_Edge::sameEdge(const Z_Mapper_Edge & edge2) const { + return (node_1 == edge2.node_1) && (node_2 == edge2.node_2); +} + +/*********************************************************************************************\ + * Implementation for Z_Mapper +\*********************************************************************************************/ +Z_Mapper_Edge & Z_Mapper::findEdge(const Z_Mapper_Edge & edge2) { + if ((edge2.node_1 == BAD_SHORTADDR) || (edge2.node_2 == BAD_SHORTADDR)) { return *(Z_Mapper_Edge*)nullptr; } + for (auto & edge : edges) { + if (edge2.sameEdge(edge)) { + return edge; + } + } + return *(Z_Mapper_Edge*)nullptr; +} + +bool Z_Mapper::addEdge(const Z_Mapper_Edge & edge2) { + if ((edge2.node_1 == BAD_SHORTADDR) || (edge2.node_2 == BAD_SHORTADDR)) { return false; } + Z_Mapper_Edge & cur_edge = findEdge(edge2); + if (&cur_edge == nullptr) { + edges.addHead(edge2); + } else { + //upgrade fields + if (edge2.lqi > cur_edge.lqi) { + cur_edge.lqi = edge2.lqi; + } + // if (cur_edge.edge_type == Z_Mapper_Edge::Unknown) { + // cur_edge.edge_type = edge2.edge_type; + // } else if ((edge2.edge_type == Z_Mapper_Edge::Parent) || (edge2.edge_type == Z_Mapper_Edge::Child)) { + // // Parent or Child always has priority over Sibling + // cur_edge.edge_type = edge2.edge_type; + // } + } + return true; +} + +void Z_Mapper::dumpInternals(void) const { + WSContentSend_P(PSTR("nodes:[" "{id:\"0x0000\",label:\"Coordinator\",group:\"o\",title:\"0x0000\"}")); + for (const auto & device : zigbee_devices.getDevices()) { + WSContentSend_P(PSTR(",{id:\"0x%04X\",group:\"%c\",title:\"0x%04X\",label:\""), + device.shortaddr, device.isRouter() ? 'r' : 'e', device.shortaddr); + + const char *fname = device.friendlyName; + if (fname != nullptr) { + WSContentSend_P(PSTR("%s"), fname); + } else { + WSContentSend_P(PSTR("0x%04X"), device.shortaddr); + } + WSContentSend_P("\"}"); + } + WSContentSend_P(PSTR("],")); + + WSContentSend_P(PSTR("edges:[")); + for (auto & edge : edges) { + uint32_t lqi_color = 0x000; + // if (edge.lqi >= 192) { + // lqi_color = 0x364; + // } else if (edge.lqi >= 128) { + // lqi_color = 0x346; + // } else if (edge.lqi > 0) { + // lqi_color = 0xd56; + // } + char hex[8]; + snprintf(hex, sizeof(hex), PSTR("%d"), edge.lqi); + + WSContentSend_P("{from:\"0x%04X\",to:\"0x%04X\",label:\"%s\",color:\"#%03X\"},", + edge.node_1, edge.node_2, (edge.lqi > 0) ? hex : "", lqi_color); + } + WSContentSend_P(PSTR("],")); +} + +#endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index f9c8019b9..d97aa8335 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -229,7 +229,7 @@ void Z_Send_State_or_Map(uint16_t shortaddr, uint8_t index, uint16_t zdo_cmd) { // This callback is registered to send ZbMap(s) to each device one at a time void Z_Map(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint32_t value) { if (BAD_SHORTADDR != shortaddr) { - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "sending `ZnMap 0x%04X`"), shortaddr); + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "sending `ZbMap 0x%04X`"), shortaddr); #ifdef USE_ZIGBEE_ZNP Z_Send_State_or_Map(shortaddr, value, ZDO_MGMT_LQI_REQ); #endif // USE_ZIGBEE_ZNP @@ -238,6 +238,8 @@ void Z_Map(uint16_t shortaddr, uint16_t groupaddr, uint16_t cluster, uint8_t end #endif // USE_ZIGBEE_EZSP } else { AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZbMap done")); + zigbee.mapping_in_progress = false; + zigbee.mapping_ready = true; } } /*********************************************************************************************\ @@ -1110,6 +1112,27 @@ int32_t Z_Mgmt_Lqi_Bind_Rsp(int32_t res, const class SBuffer &buf, boolean lqi) TrueFalseNull(m_permitjoin & 0x02), m_depth, m_lqi); + + // detect any router + Z_Device & device = zigbee_devices.findShortAddr(m_shortaddr); + if (device.valid()) { + if ((m_dev_type & 0x03) == 1) { // it is a router + device.setRouter(true); + } + } + + // Add information to zigbee mapper + // Z_Mapper_Edge::Edge_Type edge_type; + // switch ((m_dev_type & 0x70) >> 4) { + // case 0: edge_type = Z_Mapper_Edge::Parent; break; + // case 1: edge_type = Z_Mapper_Edge::Child; break; + // case 2: edge_type = Z_Mapper_Edge::Sibling; break; + // default: edge_type = Z_Mapper_Edge::Unknown; break; + + // } + // Z_Mapper_Edge edge(m_shortaddr, shortaddr, m_lqi, edge_type); + Z_Mapper_Edge edge(m_shortaddr, shortaddr, m_lqi); + zigbee_mapper.addEdge(edge); } ResponseAppend_P(PSTR("]}}")); diff --git a/tasmota/xdrv_23_zigbee_A_impl.ino b/tasmota/xdrv_23_zigbee_A_impl.ino index 1b3aad1c7..f7d6fbdc7 100644 --- a/tasmota/xdrv_23_zigbee_A_impl.ino +++ b/tasmota/xdrv_23_zigbee_A_impl.ino @@ -1052,6 +1052,26 @@ void CmndZbBindState(void) { CmndZbBindState_or_Map(false); } +void ZigbeeMapAllDevices(void) { + // we can't abort a mapping in progress + if (zigbee.mapping_in_progress) { return; } + // defer sending ZbMap to each device + zigbee_mapper.reset(); // clear all data in Zigbee mapper + + const static uint32_t DELAY_ZBMAP = 2000; // wait for 1s between commands + uint32_t wait_ms = DELAY_ZBMAP; + zigbee.mapping_in_progress = true; // mark mapping in progress + + zigbee_devices.setTimer(0x0000, 0, 0 /*wait_ms*/, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); + for (const auto & device : zigbee_devices.getDevices()) { + zigbee_devices.setTimer(device.shortaddr, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); + wait_ms += DELAY_ZBMAP; + } + wait_ms += DELAY_ZBMAP*2; + zigbee_devices.setTimer(BAD_SHORTADDR, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); + zigbee.mapping_end_time = wait_ms + millis(); +} + // // Command `ZbMap` // `ZbMap` as index if it does not fit. If default, `1` starts at the beginning @@ -1061,15 +1081,7 @@ void CmndZbMap(void) { RemoveSpace(XdrvMailbox.data); if (strlen(XdrvMailbox.data) == 0) { - // defer sending ZbMap to each device - const static uint32_t DELAY_ZBMAP = 2000; // wait for 1s between commands - uint32_t wait_ms = DELAY_ZBMAP; - zigbee_devices.setTimer(0x0000, 0, 0 /*wait_ms*/, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); - for (const auto & device : zigbee_devices.getDevices()) { - zigbee_devices.setTimer(device.shortaddr, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); - wait_ms += DELAY_ZBMAP; - } - zigbee_devices.setTimer(BAD_SHORTADDR, 0, wait_ms, 0, 0, Z_CAT_ALWAYS, 0 /* value = index */, &Z_Map); + ZigbeeMapAllDevices(); ResponseCmndDone(); } else { CmndZbBindState_or_Map(true); @@ -1317,8 +1329,8 @@ void CmndZbSave(void) { case -10: { // reinit EEPROM ZFS::erase(); - break; } + break; #endif default: saveZigbeeDevices(); @@ -1698,6 +1710,16 @@ extern "C" { } } // extern "C" +#define WEB_HANDLE_ZB_MAP "Zigbee Map" +#define WEB_HANDLE_ZB_PERMIT_JOIN "Zigbee Permit Join" +#define WEB_HANDLE_ZB_MAP_REFRESH "Zigbee Map Refresh" +const char HTTP_BTN_ZB_BUTTONS[] PROGMEM = + "" + "

" + "
"; +const char HTTP_AUTO_REFRESH_PAGE[] PROGMEM = ""; +const char HTTP_BTN_ZB_MAP_REFRESH[] PROGMEM = "

"; + void ZigbeeShow(bool json) { if (json) { @@ -1879,11 +1901,67 @@ void ZigbeeShow(bool json) } } - WSContentSend_P(PSTR("{t}")); // Terminate current multi column table and open new table + WSContentSend_P(PSTR("{t}

")); // Terminate current multi column table and open new table + if (zigbee.permit_end_time) { + // PermitJoin in progress + WSContentSend_P(PSTR("

[ Devices allowed to join ]

")); // Terminate current multi column table and open new table + } #endif } } +// Web handler to refresh the map, the redirect to show map +void ZigbeeMapRefresh(void) { + if ((!zigbee.init_phase) && (!zigbee.mapping_in_progress)) { + ZigbeeMapAllDevices(); + } + Webserver->sendHeader("Location","/zbm"); // Add a header to respond with a new location for the browser to go to the home page again + Webserver->send(302); +} + +// Display a graphical representation of the Zigbee map using vis.js network +void ZigbeeShowMap(void) { + AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP "Zigbee Mapper")); + + // if no map, then launch a new mapping + if ((!zigbee.init_phase) && (!zigbee.mapping_ready) && (!zigbee.mapping_in_progress)) { + ZigbeeMapAllDevices(); + } + + WSContentStart_P(PSTR("Tasmota Zigbee Mapping")); + WSContentSendStyle(); + + if (zigbee.init_phase) { + WSContentSend_P(PSTR("Zigbee not started")); + } else if (zigbee.mapping_in_progress) { + int32_t mapping_remaining = 1 + (zigbee.mapping_end_time - millis()) / 1000; + if (mapping_remaining < 0) { mapping_remaining = 0; } + WSContentSend_P(PSTR("Mapping in progress (%d s. remaining)"), mapping_remaining); + WSContentSend_P(HTTP_AUTO_REFRESH_PAGE); + } else if (!zigbee.mapping_ready) { + WSContentSend_P(PSTR("No mapping")); + } else { + WSContentSend_P(PSTR( + "" + "
" + "" + )); + WSContentSend_P(HTTP_BTN_ZB_MAP_REFRESH); + } + WSContentSpaceButton(BUTTON_MAIN); + WSContentStop(); +} + /*********************************************************************************************\ * Interface \*********************************************************************************************/ @@ -1920,12 +1998,17 @@ bool Xdrv23(uint8_t function) case FUNC_WEB_SENSOR: ZigbeeShow(false); break; -#ifdef USE_ZIGBEE_EZSP // GUI xmodem case FUNC_WEB_ADD_HANDLER: +#ifdef USE_ZIGBEE_EZSP WebServer_on(PSTR("/" WEB_HANDLE_ZIGBEE_XFER), HandleZigbeeXfer); - break; #endif // USE_ZIGBEE_EZSP + WebServer_on(PSTR("/zbm"), ZigbeeShowMap, HTTP_GET); // add web handler for Zigbee map + WebServer_on(PSTR("/zbr"), ZigbeeMapRefresh, HTTP_GET); // add web handler for Zigbee map refresh + break; + case FUNC_WEB_ADD_MAIN_BUTTON: + WSContentSend_P(HTTP_BTN_ZB_BUTTONS); + break; #endif // USE_WEBSERVER case FUNC_PRE_INIT: ZigbeeInit();