Zigbee add visual map of network

This commit is contained in:
Stephan Hadinger 2020-12-12 19:05:47 +01:00
parent d418d7bdd8
commit 1558f0c731
8 changed files with 323 additions and 14 deletions

View File

@ -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)

View File

@ -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);

View File

@ -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

View File

@ -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; }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
#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<Z_Mapper_Edge> 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

View File

@ -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("]}}"));

View File

@ -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<x>` 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 =
"<button onclick='la(\"&zbj=1\");'>" WEB_HANDLE_ZB_PERMIT_JOIN "</button>"
"<p></p>"
"<form action='zbm' method='get'><button>" WEB_HANDLE_ZB_MAP "</button></form>";
const char HTTP_AUTO_REFRESH_PAGE[] PROGMEM = "<script>setTimeout(function(){location.reload();},1990);</script>";
const char HTTP_BTN_ZB_MAP_REFRESH[] PROGMEM = "<p></p><form action='zbr' method='get'><button>" WEB_HANDLE_ZB_MAP_REFRESH "</button></form>";
void ZigbeeShow(bool json)
{
if (json) {
@ -1879,11 +1901,67 @@ void ZigbeeShow(bool json)
}
}
WSContentSend_P(PSTR("</table>{t}")); // Terminate current multi column table and open new table
WSContentSend_P(PSTR("</table>{t}<p></p>")); // Terminate current multi column table and open new table
if (zigbee.permit_end_time) {
// PermitJoin in progress
WSContentSend_P(PSTR("<p><b>[ <span style='color:#080;'>Devices allowed to join</span> ]</b></p>")); // 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(
"<script type=\"text/javascript\" src=\"https://unpkg.com/vis-network/standalone/umd/vis-network.min.js\"></script>"
"<div id=\"mynetwork\" style=\"background-color:#fff;width:800px;height:400px;border:1px solid lightgray;resize:both;\"></div>"
"<script type=\"text/javascript\">"
"var container=document.getElementById(\"mynetwork\");"
"var options={groups:{o:{shape:\"circle\",color:\"#d55\"},r:{shape:\"box\",color:\"#fb7\"},e:{shape:\"ellipse\",color:\"#adf\"}}};"
"var data={"
));
zigbee_mapper.dumpInternals();
WSContentSend_P(PSTR(
"};"
"var network=new vis.Network(container,data,options);</script>"
));
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();