From 3a1155f2b63c8bfca93040fc91e890e00c3a006f Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 27 May 2020 19:42:43 +0200 Subject: [PATCH 01/48] reduce footprint of multicast udp listener --- lib/UdpListener/library.properties | 7 + lib/UdpListener/src/UdpListener.h | 207 +++++++++++++++++++++++++++++ tasmota/support_udp.ino | 58 +++++--- 3 files changed, 255 insertions(+), 17 deletions(-) create mode 100644 lib/UdpListener/library.properties create mode 100644 lib/UdpListener/src/UdpListener.h diff --git a/lib/UdpListener/library.properties b/lib/UdpListener/library.properties new file mode 100644 index 000000000..1d453bc6c --- /dev/null +++ b/lib/UdpListener/library.properties @@ -0,0 +1,7 @@ +name=UdpListener +version=1.0 +author=Ivan Grokhotkov, Stephan Hadinger +maintainer=Stephan +sentence=UdpListener optimized for static and limite memory allocation, to reduce memory footprint of receiving SSDP request, as a replacement for WifiUdp. +paragraph=This class only handles receiving UDP Multicast packets. For sending packets, use WifiUdp. +architectures=esp8266 diff --git a/lib/UdpListener/src/UdpListener.h b/lib/UdpListener/src/UdpListener.h new file mode 100644 index 000000000..90cacff1e --- /dev/null +++ b/lib/UdpListener/src/UdpListener.h @@ -0,0 +1,207 @@ +/* + UdpListener.h - webserver for Tasmota + + Copyright (C) 2020 Theo Arends & 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 .@ +*/ + +// adapted from: +/* + UdpContext.h - UDP connection handling on top of lwIP + + Copyright (c) 2014 Ivan Grokhotkov. All rights reserved. + This file is part of the esp8266 core for Arduino environment. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +/* + * This is a stripped down version of Udp handler to avoid overflowing + * memory when lots of multicast SSDP packets arrive. + * The pbuf is freed immediately upon arrival of the packet. + * + * Packet data are kept in a statically area in RAM and keeps + * only the first bytes (200 by default) of each packet. + * The number of packets treated is limited (3 by default), any + * new packet arriving is dropped. + * + * This class does only receiving multicast packets for LWIP2 +*/ + +#ifndef UDPMULTICASTLISTENER_H +#define UDPMULTICASTLISTENER_H + +// #include + +extern "C" { +#include +#include +} + +template +struct UdpPacket { + IPAddress srcaddr; + IPAddress dstaddr; + int16_t srcport; + netif* input_netif; + size_t len; + uint8_t buf[PACKET_SIZE]; +}; + +template +class UdpListener +{ +public: + + typedef std::function rxhandler_t; + + UdpListener(size_t packet_number) + : _pcb(0) + , _packet_number(packet_number) + , _buffers(nullptr) + , _udp_packets(0) + , _udp_ready(false) + , _udp_index(0) + { + _packet_number = packet_number; + _buffers = new UdpPacket[_packet_number]; + _pcb = udp_new(); + } + + ~UdpListener() + { + udp_remove(_pcb); + _pcb = 0; + delete[] _buffers; + _buffers = nullptr; + } + + void reset(void) + { + _udp_packets = 0; + _udp_index = 0; + } + + bool listen(const IPAddress& addr, uint16_t port) + { + if (!_buffers) { return false; } + udp_recv(_pcb, &_s_recv, (void *) this); + err_t err = udp_bind(_pcb, addr, port); + return err == ERR_OK; + } + + void disconnect() + { + udp_disconnect(_pcb); + } + + bool next() + { + if (!_buffers) { return false; } + if (_udp_packets > 0) { + if (!_udp_ready) { + // we just consume the first packet + _udp_ready = true; + } else { + _udp_packets--; + _udp_index = (_udp_index + 1) % _packet_number; // advance to next buffer index in ring + if (_udp_packets == 0) { + _udp_ready = false; + } + } + } else { + _udp_ready = false; + } + return _udp_ready; + } + + UdpPacket * read(void) + { + if (!_buffers) { return nullptr; } + if (_udp_ready) { // we have a packet ready to consume + return &_buffers[_udp_index]; + } else { + return nullptr; + } + } + +private: + + void _recv(udp_pcb *upcb, pbuf *pb, + const ip_addr_t *srcaddr, u16_t srcport) + { + if (!_buffers) { pbuf_free(pb); return; } + // Serial.printf(">>> _recv: _udp_packets = %d, _udp_index = %d, tot_len = %d\n", _udp_packets, _udp_index, pb->tot_len); + if (_udp_packets >= _packet_number) { + // we don't have slots anymore, drop packet + pbuf_free(pb); + return; + } + + uint8_t next_slot = (_udp_index + _udp_packets) % _packet_number; + + size_t packet_len = pb->tot_len; + if (packet_len > PACKET_SIZE) { packet_len = PACKET_SIZE; } + + uint8_t * dst = &_buffers[next_slot].buf[0]; + void* buf = pbuf_get_contiguous(pb, dst, PACKET_SIZE, packet_len, 0); + if (buf) { + + if (buf != dst) + memcpy(dst, buf, packet_len); + _buffers[next_slot].len = packet_len; + + _buffers[next_slot].srcaddr = srcaddr; + _buffers[next_slot].dstaddr = ip_current_dest_addr(); + _buffers[next_slot].srcport = srcport; + _buffers[next_slot].input_netif = ip_current_input_netif(); + _udp_packets++; // we have one packet ready + } + pbuf_free(pb); // free memory immediately + } + + static void _s_recv(void *arg, + udp_pcb *upcb, pbuf *p, + CONST ip_addr_t *srcaddr, u16_t srcport) + { + reinterpret_cast(arg)->_recv(upcb, p, srcaddr, srcport); + } + +private: + udp_pcb* _pcb; + uint8_t _packet_number; + + UdpPacket * _buffers; + + // how many packets are ready. + int8_t _udp_packets; // number of udp packets ready to consume + bool _udp_ready; // is a packet currenlty consumed after a call to next() + // ring buffer ranges from 0..(_packet_number-1) + int8_t _udp_index; // current index in the ring buffer +}; + +#endif //UDPCONTEXTLIGHT_H \ No newline at end of file diff --git a/tasmota/support_udp.ino b/tasmota/support_udp.ino index 451135e6a..e8974d664 100644 --- a/tasmota/support_udp.ino +++ b/tasmota/support_udp.ino @@ -19,10 +19,16 @@ #ifdef USE_EMULATION -#define UDP_BUFFER_SIZE 200 // Max UDP buffer size needed for M-SEARCH message +#ifndef UDP_BUFFER_SIZE +#define UDP_BUFFER_SIZE 120 // Max UDP buffer size needed for M-SEARCH message +#endif +#ifndef UDP_MAX_PACKETS +#define UDP_MAX_PACKETS 3 // we support x more packets than the current one +#endif #define UDP_MSEARCH_SEND_DELAY 1500 // Delay in ms before M-Search response is send #include +#include "UdpListener.h" Ticker TickerMSearch; IPAddress udp_remote_ip; // M-Search remote IP address @@ -31,6 +37,8 @@ uint16_t udp_remote_port; // M-Search remote port bool udp_connected = false; bool udp_response_mutex = false; // M-Search response mutex to control re-entry +UdpListener UdpCtx(UDP_MAX_PACKETS); + /*********************************************************************************************\ * UPNP/SSDP search targets \*********************************************************************************************/ @@ -48,10 +56,14 @@ const char SSDP_ALL[] PROGMEM = "ssdp:all"; bool UdpDisconnect(void) { if (udp_connected) { + // flush any outgoing packet PortUdp.flush(); #ifdef USE_DEVICE_GROUPS + // stop + UdpCtx.stop(); PortUdp.stop(); #else // USE_DEVICE_GROUPS + // stop all WiFiUDP::stopAll(); #endif // !USE_DEVICE_GROUPS AddLog_P(LOG_LEVEL_DEBUG, PSTR(D_LOG_UPNP D_MULTICAST_DISABLED)); @@ -64,13 +76,19 @@ bool UdpConnect(void) { if (!udp_connected && !restart_flag) { // Simple Service Discovery Protocol (SSDP) - if (PortUdp.beginMulticast(WiFi.localIP(), IPAddress(239,255,255,250), 1900)) { - AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED)); - udp_response_mutex = false; - udp_connected = true; - } else { + + UdpCtx.reset(); + if (igmp_joingroup(WiFi.localIP(), IPAddress(239,255,255,250)) == ERR_OK) { // addr 239.255.255.250 + ip_addr_t addr = IPADDR4_INIT(INADDR_ANY); + if (UdpCtx.listen(&addr, 1900)) { // port 1900 + // OK + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED)); + udp_response_mutex = false; + udp_connected = true; + } + } + if (!udp_connected) { // if connection failed AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_JOIN_FAILED)); - udp_connected = false; } } return udp_connected; @@ -79,14 +97,20 @@ bool UdpConnect(void) void PollUdp(void) { if (udp_connected) { - while (PortUdp.parsePacket()) { - char packet_buffer[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP/SSDP packet + // parsePacket + while (UdpCtx.next()) { + // while (PortUdp.parsePacket()) { + UdpPacket *packet; - int len = PortUdp.read(packet_buffer, UDP_BUFFER_SIZE -1); - packet_buffer[len] = 0; + packet = UdpCtx.read(); + if (packet->len >= UDP_BUFFER_SIZE) { + packet->len--; // leave space for NULL terminator + } + packet->buf[packet->len] = 0; // add NULL at the end of the packer + char * packet_buffer = (char*) &packet->buf; - AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet (%d)"), len); -// AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("\n%s"), packet_buffer); + AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet (%d)"), packet->len); + // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("\n%s"), packet_buffer); // Simple Service Discovery Protocol (SSDP) if (Settings.flag2.emulation) { @@ -97,11 +121,11 @@ void PollUdp(void) #endif udp_response_mutex = true; - udp_remote_ip = PortUdp.remoteIP(); - udp_remote_port = PortUdp.remotePort(); + udp_remote_ip = packet->srcaddr; + udp_remote_port = packet->srcport; -// AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet from %s:%d\n%s"), -// udp_remote_ip.toString().c_str(), udp_remote_port, packet_buffer); + // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet from %s:%d\n%s"), + // udp_remote_ip.toString().c_str(), udp_remote_port, packet_buffer); uint32_t response_delay = UDP_MSEARCH_SEND_DELAY + ((millis() &0x7) * 100); // 1500 - 2200 msec From c65a3dfba7b687dfcec58e020432005eb8e71001 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 27 May 2020 20:48:40 +0200 Subject: [PATCH 02/48] Fix compilation issue --- lib/UdpListener/src/UdpListener.h | 2 +- tasmota/support_udp.ino | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/UdpListener/src/UdpListener.h b/lib/UdpListener/src/UdpListener.h index 90cacff1e..a94787902 100644 --- a/lib/UdpListener/src/UdpListener.h +++ b/lib/UdpListener/src/UdpListener.h @@ -204,4 +204,4 @@ private: int8_t _udp_index; // current index in the ring buffer }; -#endif //UDPCONTEXTLIGHT_H \ No newline at end of file +#endif //UDPMULTICASTLISTENER_H \ No newline at end of file diff --git a/tasmota/support_udp.ino b/tasmota/support_udp.ino index e8974d664..588ad2f2f 100644 --- a/tasmota/support_udp.ino +++ b/tasmota/support_udp.ino @@ -58,9 +58,9 @@ bool UdpDisconnect(void) if (udp_connected) { // flush any outgoing packet PortUdp.flush(); + UdpCtx.disconnect(); #ifdef USE_DEVICE_GROUPS // stop - UdpCtx.stop(); PortUdp.stop(); #else // USE_DEVICE_GROUPS // stop all From 0327c4a5478f31f068090a1075aa3a46e4f62409 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 27 May 2020 23:52:25 +0200 Subject: [PATCH 03/48] Udp fix for ESP32 --- lib/UdpListener/src/UdpListener.h | 2 ++ tasmota/support_udp.ino | 39 ++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/UdpListener/src/UdpListener.h b/lib/UdpListener/src/UdpListener.h index a94787902..a369a5d81 100644 --- a/lib/UdpListener/src/UdpListener.h +++ b/lib/UdpListener/src/UdpListener.h @@ -55,6 +55,7 @@ #ifndef UDPMULTICASTLISTENER_H #define UDPMULTICASTLISTENER_H +#ifdef ESP8266 // #include extern "C" { @@ -204,4 +205,5 @@ private: int8_t _udp_index; // current index in the ring buffer }; +#endif // ESP8266 #endif //UDPMULTICASTLISTENER_H \ No newline at end of file diff --git a/tasmota/support_udp.ino b/tasmota/support_udp.ino index 588ad2f2f..cccb64b4e 100644 --- a/tasmota/support_udp.ino +++ b/tasmota/support_udp.ino @@ -22,13 +22,9 @@ #ifndef UDP_BUFFER_SIZE #define UDP_BUFFER_SIZE 120 // Max UDP buffer size needed for M-SEARCH message #endif -#ifndef UDP_MAX_PACKETS -#define UDP_MAX_PACKETS 3 // we support x more packets than the current one -#endif #define UDP_MSEARCH_SEND_DELAY 1500 // Delay in ms before M-Search response is send #include -#include "UdpListener.h" Ticker TickerMSearch; IPAddress udp_remote_ip; // M-Search remote IP address @@ -37,7 +33,14 @@ uint16_t udp_remote_port; // M-Search remote port bool udp_connected = false; bool udp_response_mutex = false; // M-Search response mutex to control re-entry +#ifdef ESP8266 +#ifndef UDP_MAX_PACKETS +#define UDP_MAX_PACKETS 3 // we support x more packets than the current one +#endif + +#include "UdpListener.h" UdpListener UdpCtx(UDP_MAX_PACKETS); +#endif /*********************************************************************************************\ * UPNP/SSDP search targets @@ -58,7 +61,9 @@ bool UdpDisconnect(void) if (udp_connected) { // flush any outgoing packet PortUdp.flush(); +#ifdef ESP8266 UdpCtx.disconnect(); +#endif #ifdef USE_DEVICE_GROUPS // stop PortUdp.stop(); @@ -76,7 +81,7 @@ bool UdpConnect(void) { if (!udp_connected && !restart_flag) { // Simple Service Discovery Protocol (SSDP) - +#ifdef ESP8266 UdpCtx.reset(); if (igmp_joingroup(WiFi.localIP(), IPAddress(239,255,255,250)) == ERR_OK) { // addr 239.255.255.250 ip_addr_t addr = IPADDR4_INIT(INADDR_ANY); @@ -86,6 +91,12 @@ bool UdpConnect(void) udp_response_mutex = false; udp_connected = true; } +#else // ESP32 + if (PortUdp.beginMulticast(WiFi.localIP(), IPAddress(239,255,255,250), 1900)) { + AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_REJOINED)); + udp_response_mutex = false; + udp_connected = true; +#endif } if (!udp_connected) { // if connection failed AddLog_P(LOG_LEVEL_INFO, PSTR(D_LOG_UPNP D_MULTICAST_JOIN_FAILED)); @@ -97,19 +108,24 @@ bool UdpConnect(void) void PollUdp(void) { if (udp_connected) { - // parsePacket +#ifdef ESP8266 while (UdpCtx.next()) { - // while (PortUdp.parsePacket()) { UdpPacket *packet; - packet = UdpCtx.read(); if (packet->len >= UDP_BUFFER_SIZE) { packet->len--; // leave space for NULL terminator } packet->buf[packet->len] = 0; // add NULL at the end of the packer char * packet_buffer = (char*) &packet->buf; + int32_t len = packet->len; +#else // ESP32 + while (PortUdp.parsePacket()) { + char packet_buffer[UDP_BUFFER_SIZE]; // buffer to hold incoming UDP/SSDP packet - AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet (%d)"), packet->len); + int32_t len = PortUdp.read(packet_buffer, UDP_BUFFER_SIZE -1); + packet_buffer[len] = 0; +#endif + AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: Packet (%d)"), len); // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("\n%s"), packet_buffer); // Simple Service Discovery Protocol (SSDP) @@ -121,8 +137,13 @@ void PollUdp(void) #endif udp_response_mutex = true; +#ifdef ESP8266 udp_remote_ip = packet->srcaddr; udp_remote_port = packet->srcport; +#else + udp_remote_ip = PortUdp.remoteIP(); + udp_remote_port = PortUdp.remotePort(); +#endif // AddLog_P2(LOG_LEVEL_DEBUG_MORE, PSTR("UDP: M-SEARCH Packet from %s:%d\n%s"), // udp_remote_ip.toString().c_str(), udp_remote_port, packet_buffer); From ea482721351f92ce8eb036cfbd98a6b8bf3e95fc Mon Sep 17 00:00:00 2001 From: Staars Date: Thu, 28 May 2020 09:57:42 +0200 Subject: [PATCH 04/48] make NimBLEDevice::initialized static --- libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp b/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp index 1540f6327..1695a5177 100644 --- a/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp +++ b/libesp32/NimBLE-Arduino/src/NimBLEDevice.cpp @@ -40,7 +40,7 @@ static const char* LOG_TAG = "NimBLEDevice"; /** * Singletons for the NimBLEDevice. */ -bool initialized = false; +static bool initialized = false; #if defined(CONFIG_BT_NIMBLE_ROLE_OBSERVER) NimBLEScan* NimBLEDevice::m_pScan = nullptr; #endif From d5e7384704cb72e548460219f7dcd4d27c069765 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Thu, 28 May 2020 11:55:27 +0200 Subject: [PATCH 05/48] add flash syntax --- tools/Esptool/ESP32/readme.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/Esptool/ESP32/readme.txt b/tools/Esptool/ESP32/readme.txt index b0730aaff..2d09d6faa 100644 --- a/tools/Esptool/ESP32/readme.txt +++ b/tools/Esptool/ESP32/readme.txt @@ -1 +1,5 @@ This files are needed for flashing Tasmota with esptool.py to a ESP32 + +Command syntax for flashing Tasmota32 firmware on ESP32 via Esptool (replace COM Port Number!): + +esptool.py --chip esp32 --port COM5 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dout --flash_freq 40m --flash_size detect 0x1000 bootloader_dout_40m.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 tasmota32.bin From 8e95a679848655536c4440b278ed95ba9376bfc8 Mon Sep 17 00:00:00 2001 From: blakadder Date: Thu, 28 May 2020 16:38:52 +0200 Subject: [PATCH 06/48] Update readme.txt --- tools/Esptool/ESP32/readme.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/Esptool/ESP32/readme.txt b/tools/Esptool/ESP32/readme.txt index 2d09d6faa..bdedc87de 100644 --- a/tools/Esptool/ESP32/readme.txt +++ b/tools/Esptool/ESP32/readme.txt @@ -1,4 +1,4 @@ -This files are needed for flashing Tasmota with esptool.py to a ESP32 +These files are needed for flashing Tasmota32 with esptool.py to an ESP32. Command syntax for flashing Tasmota32 firmware on ESP32 via Esptool (replace COM Port Number!): From 357341899c624597d616dcf99045be6c4941043b Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Fri, 29 May 2020 19:29:03 +0200 Subject: [PATCH 07/48] ILI9488 ESP32 --- lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp | 232 ++++++++---------- platformio_override_sample.ini | 2 +- tasmota/tasmota_globals.h | 4 +- tasmota/xdsp_08_ILI9488.ino | 25 +- tasmota/xdsp_10_RA8876.ino | 3 + 5 files changed, 132 insertions(+), 134 deletions(-) diff --git a/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp b/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp index 3e0aca9ee..e507fdf1b 100644 --- a/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp +++ b/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp @@ -19,7 +19,7 @@ #include // if using software spi this optimizes the code -#define SWSPI_OPTMODE + #define ILI9488_START start(); #define ILI9488_STOP stop(); @@ -37,9 +37,6 @@ ILI9488::ILI9488(int8_t cs,int8_t mosi,int8_t sclk,int8_t bp) : Renderer(ILI9488 _hwspi = 0; } - -#include "spi_register.h" - /* CPU Clock = 80 Mhz @@ -73,6 +70,10 @@ GPIO15: PERIPHS_IO_MUX_MTDO_U uint8_t ili9488_start; +#ifndef ESP32 +// ESP8266 +#include "spi_register.h" +#define SWSPI_OPTMODE uint32_t ili9488_clock; uint32_t ili9488_usr; uint32_t ili9488_usr1; @@ -192,32 +193,6 @@ void ILI9488::stop(void) { ili9488_start=0; } - -#if 0 -// code from espressif SDK -/****************************************************************************** - * FunctionName : spi_lcd_9bit_write - * Description : SPI 9bits transmission function for driving LCD TM035PDZV36 - * Parameters : uint8 spi_no - SPI module number, Only "SPI" and "HSPI" are valid - * uint8 high_bit - first high bit of the data, 0 is for "0",the other value 1-255 is for "1" - * uint8 low_8bit- the rest 8bits of the data. -*******************************************************************************/ -void spi_lcd_9bit_write(uint8_t high_bit,uint8_t low_8bit) -{ - uint32_t regvalue; - uint8_t bytetemp; - - if(high_bit) bytetemp=(low_8bit>>1)|0x80; - else bytetemp=(low_8bit>>1)&0x7f; - - regvalue= ((8&SPI_USR_COMMAND_BITLEN)<>= 1) { + WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_sclk); + if(d&bit) WRITE_PERI_REG( PIN_OUT_SET, 1<<_mosi); + else WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_mosi); + WRITE_PERI_REG( PIN_OUT_SET, 1<<_sclk); } + WRITE_PERI_REG( PIN_OUT_SET, 1<<_cs); } -*/ +#else +// ESP32 section +void ILI9488::writedata(uint8_t d) { + fastSPIwrite(d,1); +} + +void ILI9488::writecommand(uint8_t c) { + fastSPIwrite(c,0); +} + +#include "soc/spi_reg.h" +#include "soc/spi_struct.h" +#include "esp32-hal-spi.h" +#include "esp32-hal.h" +#include "soc/spi_struct.h" + +// since ardunio transferBits ia completely disfunctional +// we use our own hardware driver for 9 bit spi +void ILI9488::fastSPIwrite(uint8_t d,uint8_t dc) { + digitalWrite( _cs, LOW); + + uint32_t regvalue=d>>1; + if (dc) regvalue|=0x80; + else regvalue&=0x7f; + if (d&1) regvalue|=0x8000; + + REG_SET_BIT(SPI_USER_REG(3), SPI_USR_MOSI); + REG_WRITE(SPI_MOSI_DLEN_REG(3), 9 - 1); + uint32_t *dp=(uint32_t*)SPI_W0_REG(3); + *dp=regvalue; + REG_SET_BIT(SPI_CMD_REG(3), SPI_USR); + while (REG_GET_FIELD(SPI_CMD_REG(3), SPI_USR)); + + digitalWrite( _cs, HIGH); +} + +SPISettings ili9488_spiSettings; + +void ILI9488::start(void) { + if (ili9488_start) return; + SPI.beginTransaction(ili9488_spiSettings); + ili9488_start=1; +} +void ILI9488::stop(void) { + if (!ili9488_start) return; + SPI.endTransaction(); + ili9488_start=0; +} +#endif + uint16_t ILI9488::GetColorFromIndex(uint8_t index) { if (index>=sizeof(ili9488_colors)/2) index=0; @@ -339,14 +344,23 @@ void ILI9488::begin(void) { pinMode(_bp, OUTPUT); digitalWrite(_bp,HIGH); } + +#ifndef ESP32 if ((_sclk==14) && (_mosi==13) && (_cs==15)) { // we use hardware spi + SPI.begin(); _hwspi=1; spi_lcd_mode_init(); } else { // we must use software spi _hwspi=0; } +#else + SPI.begin(_sclk,-1,_mosi, -1); + ili9488_spiSettings = SPISettings(10000000, MSBFIRST, SPI_MODE3); + _hwspi=1; +#endif + ILI9488_START delay(1); @@ -844,9 +858,9 @@ uint32_t pack_rgb(uint32_t r, uint32_t g, uint32_t b) { return ulswap(data); } +#ifndef ESP32 // fill a rectangle -void ILI9488::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, - uint16_t color) { +void ILI9488::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) { ILI9488_START // rudimentary clipping (drawChar w/big text requires this) @@ -990,7 +1004,35 @@ void ILI9488::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, #endif } +#else +// ESP32 +void ILI9488::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) { + // rudimentary clipping (drawChar w/big text requires this) + if((x >= _width) || (y >= _height)) return; + if((x + w - 1) >= _width) w = _width - x; + if((y + h - 1) >= _height) h = _height - y; + + setAddrWindow(x, y, x+w-1, y+h-1); + + uint8_t r = (color & 0xF800) >> 11; + uint8_t g = (color & 0x07E0) >> 5; + uint8_t b = color & 0x001F; + + r = (r * 255) / 31; + g = (g * 255) / 63; + b = (b * 255) / 31; + + for(y=h; y>0; y--) { + for(x=w; x>0; x--) { + writedata(r); + writedata(g); + writedata(b); + } + } + ILI9488_STOP +} +#endif // Pass 8-bit (each) R,G,B, get back 16-bit packed color uint16_t ILI9488::color565(uint8_t r, uint8_t g, uint8_t b) { @@ -1040,65 +1082,3 @@ void ILI9488::invertDisplay(boolean i) { writecommand(i ? ILI9488_INVON : ILI9488_INVOFF); ILI9488_STOP } - -void ICACHE_RAM_ATTR ILI9488::fastSPIwrite(uint8_t d,uint8_t dc) { - - WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_cs); - WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_sclk); - if(dc) WRITE_PERI_REG( PIN_OUT_SET, 1<<_mosi); - else WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_mosi); - WRITE_PERI_REG( PIN_OUT_SET, 1<<_sclk); - - for(uint8_t bit = 0x80; bit; bit >>= 1) { - WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_sclk); - if(d&bit) WRITE_PERI_REG( PIN_OUT_SET, 1<<_mosi); - else WRITE_PERI_REG( PIN_OUT_CLEAR, 1<<_mosi); - WRITE_PERI_REG( PIN_OUT_SET, 1<<_sclk); - } - WRITE_PERI_REG( PIN_OUT_SET, 1<<_cs); -} - -/* - - uint16_t ILI9488::readcommand16(uint8_t c) { - digitalWrite(_dc, LOW); - if (_cs) - digitalWrite(_cs, LOW); - - spiwrite(c); - pinMode(_sid, INPUT); // input! - uint16_t r = spiread(); - r <<= 8; - r |= spiread(); - if (_cs) - digitalWrite(_cs, HIGH); - - pinMode(_sid, OUTPUT); // back to output - return r; - } - - uint32_t ILI9488::readcommand32(uint8_t c) { - digitalWrite(_dc, LOW); - if (_cs) - digitalWrite(_cs, LOW); - spiwrite(c); - pinMode(_sid, INPUT); // input! - - dummyclock(); - dummyclock(); - - uint32_t r = spiread(); - r <<= 8; - r |= spiread(); - r <<= 8; - r |= spiread(); - r <<= 8; - r |= spiread(); - if (_cs) - digitalWrite(_cs, HIGH); - - pinMode(_sid, OUTPUT); // back to output - return r; - } - - */ diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 695973c2e..22256f679 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -192,6 +192,6 @@ lib_extra_dirs = libesp32 lib_ignore = - ILI9488 + ; ILI9488 ; SSD3115 cc1101 diff --git a/tasmota/tasmota_globals.h b/tasmota/tasmota_globals.h index 37e39e451..44826fa64 100644 --- a/tasmota/tasmota_globals.h +++ b/tasmota/tasmota_globals.h @@ -300,7 +300,7 @@ const char kWebColors[] PROGMEM = #undef USE_HM10 // Disable support for HM-10 as a BLE-bridge as an alternative is using the internal ESP32 BLE #undef USE_KEELOQ // Disable support for Jarolift rollers by Keeloq algorithm as it's library cc1101 is not compatible with ESP32 -#undef USE_DISPLAY_ILI9488 // Disable as it's library JaretBurkett_ILI9488-gemu-1.0 is not compatible with ESP32 +//#undef USE_DISPLAY_ILI9488 // Disable as it's library JaretBurkett_ILI9488-gemu-1.0 is not compatible with ESP32 //#undef USE_DISPLAY_SSD1351 // Disable as it's library Adafruit_SSD1351_gemu-1.0 is not compatible with ESP32 #endif // ESP32 @@ -335,7 +335,7 @@ const char kWebColors[] PROGMEM = #ifdef USE_DEVICE_GROUPS #define SendDeviceGroupMessage(DEVICE_INDEX, REQUEST_TYPE, ...) _SendDeviceGroupMessage(DEVICE_INDEX, REQUEST_TYPE, __VA_ARGS__, 0) #define SendLocalDeviceGroupMessage(REQUEST_TYPE, ...) _SendDeviceGroupMessage(0, REQUEST_TYPE, __VA_ARGS__, 0) -uint8_t device_group_count = 0; +uint8_t device_group_count = 1; #endif // USE_DEVICE_GROUPS #ifdef DEBUG_TASMOTA_CORE diff --git a/tasmota/xdsp_08_ILI9488.ino b/tasmota/xdsp_08_ILI9488.ino index 1dc877e18..e49e7bffe 100644 --- a/tasmota/xdsp_08_ILI9488.ino +++ b/tasmota/xdsp_08_ILI9488.ino @@ -84,18 +84,33 @@ void ILI9488_InitDriver() bppin=Pin(GPIO_BACKLIGHT); } - // init renderer - if (PinUsed(GPIO_SSPI_CS) && PinUsed(GPIO_SSPI_MOSI) && PinUsed(GPIO_SSPI_SCLK)) { - ili9488 = new ILI9488(Pin(GPIO_SSPI_CS),Pin(GPIO_SSPI_MOSI),Pin(GPIO_SSPI_SCLK),bppin); +#ifdef ESP32 +#undef HW_SPI_MOSI +#define HW_SPI_MOSI 23 +#undef HW_SPI_MISO +#define HW_SPI_MISO 19 +#undef HW_SPI_CLK +#define HW_SPI_CLK 18 +#else +#undef HW_SPI_MOSI +#define HW_SPI_MOSI 13 +#undef HW_SPI_MISO +#define HW_SPI_MISO 12 +#undef HW_SPI_CLK +#define HW_SPI_CLK 14 +#endif + + // init renderer, must use hardware spi + if (PinUsed(GPIO_SSPI_CS) && (Pin(GPIO_SSPI_MOSI)==HW_SPI_MOSI) && (Pin(GPIO_SSPI_SCLK)==HW_SPI_CLK)) { + ili9488 = new ILI9488(Pin(GPIO_SSPI_CS),Pin(GPIO_SSPI_MOSI),Pin(GPIO_SSPI_SCLK),bppin); } else { - if (PinUsed(GPIO_SPI_CS) && PinUsed(GPIO_SPI_MOSI) && PinUsed(GPIO_SPI_CLK)) { + if (PinUsed(GPIO_SPI_CS) && (Pin(GPIO_SPI_MOSI)==HW_SPI_MOSI) && (Pin(GPIO_SPI_CLK)==HW_SPI_CLK)) { ili9488 = new ILI9488(Pin(GPIO_SPI_CS),Pin(GPIO_SPI_MOSI),Pin(GPIO_SPI_CLK),bppin); } else { return; } } - SPI.begin(); ili9488->begin(); renderer = ili9488; renderer->DisplayInit(DISPLAY_INIT_MODE,Settings.display_size,Settings.display_rotate,Settings.display_font); diff --git a/tasmota/xdsp_10_RA8876.ino b/tasmota/xdsp_10_RA8876.ino index 6a44708cb..aa8e82f4d 100644 --- a/tasmota/xdsp_10_RA8876.ino +++ b/tasmota/xdsp_10_RA8876.ino @@ -72,8 +72,11 @@ void RA8876_InitDriver() bg_color = RA8876_BLACK; #ifdef ESP32 +#undef HW_SPI_MOSI #define HW_SPI_MOSI 23 +#undef HW_SPI_MISO #define HW_SPI_MISO 19 +#undef HW_SPI_CLK #define HW_SPI_CLK 18 #else #undef HW_SPI_MOSI From 10e059c3631074895f84d53f8570508f7a8a8665 Mon Sep 17 00:00:00 2001 From: rando-calrissian <37273799+rando-calrissian@users.noreply.github.com> Date: Fri, 29 May 2020 11:18:39 -0700 Subject: [PATCH 08/48] Add files via upload Added the ability to set the displayed temperature units (C/F) for Xiaomi LYWSD02 devices. --- tasmota/xsns_62_MI_ESP32.ino | 471 ++++++++++++++++++++--------------- 1 file changed, 266 insertions(+), 205 deletions(-) diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index 28c927db9..1844edc2c 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -53,6 +53,8 @@ struct { uint32_t willSetTime:1; uint32_t shallReadBatt:1; uint32_t willReadBatt:1; + uint32_t shallSetUnit:1; + uint32_t willSetUnit:1; } mode; struct { uint8_t sensor; // points to to the number 0...255 @@ -152,7 +154,7 @@ BLEScanResults MI32foundDevices; const char S_JSON_MI32_COMMAND_NVALUE[] PROGMEM = "{\"" D_CMND_MI32 "%s\":%d}"; const char S_JSON_MI32_COMMAND[] PROGMEM = "{\"" D_CMND_MI32 "%s%s\"}"; -const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery"; +const char kMI32_Commands[] PROGMEM = "Period|Time|Page|Battery|Unit"; #define FLORA 1 #define MJ_HT_V1 2 @@ -185,7 +187,8 @@ enum MI32_Commands { // commands useable in console or rules CMND_MI32_PERIOD, // set period like TELE-period in seconds between read-cycles CMND_MI32_TIME, // set LYWSD02-Time from ESP8266-time CMND_MI32_PAGE, // sensor entries per web page, which will be shown alternated - CMND_MI32_BATTERY // read all battery levels + CMND_MI32_BATTERY, // read all battery levels + CMND_MI32_UNIT // toggles the displayed unit between C/F (LYWSD02) }; enum MI32_TASK { @@ -193,6 +196,7 @@ enum MI32_TASK { MI32_TASK_CONN = 1, MI32_TASK_TIME = 2, MI32_TASK_BATT = 3, + MI32_TASK_UNIT = 4, }; /*********************************************************************************************\ @@ -225,26 +229,18 @@ class MI32SensorCallback : public NimBLEClientCallbacks { class MI32AdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { void onResult(NimBLEAdvertisedDevice* advertisedDevice) { - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); - if (advertisedDevice->getServiceData().length() == 0) { - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); - MI32Scan->erase(advertisedDevice->getAddress()); - return; - } + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice.getAddress().toString().c_str(),advertisedDevice.getServiceData().length()); + if (advertisedDevice->getServiceData().length() == 0) return; uint16_t uuid = advertisedDevice->getServiceDataUUID().getNative()->u16.value; - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("UUID: %x"),uuid); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%x"),uuid); uint8_t addr[6]; memcpy(addr,advertisedDevice->getAddress().getNative(),6); MI32_ReverseMAC(addr); if(uuid==0xfe95) { - MI32ParseResponse((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); + MI32ParseResponse((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr); } else if(uuid==0xfdcd) { - MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); - } - else { - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); - MI32Scan->erase(advertisedDevice->getAddress()); + MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr); } }; }; @@ -394,62 +390,56 @@ void MI32StartTask(uint32_t task){ if (MI32.mode.willReadBatt == 1) return; MI32StartBatteryTask(); break; + case MI32_TASK_UNIT: + if (MI32.mode.shallSetUnit == 0) return; + MI32StartUnitTask(); + break; default: break; } } -bool MI32ConnectActiveSensor(){ // only use inside a task !! - MI32.mode.connected = 0; +void MI32ConnectActiveSensor(){ // only use inside a task !! MI32Client = nullptr; - Wifi.counter = Wifi.counter + 20; // hopefully less interference - NimBLEAddress _address = NimBLEAddress(MIBLEsensors[MI32.state.sensor].serial); + esp_bd_addr_t address; + memcpy(address,MIBLEsensors[MI32.state.sensor].serial,sizeof(address)); if(NimBLEDevice::getClientListSize()) { - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: found any clients in the cList"),D_CMND_MI32); - MI32Client = NimBLEDevice::getClientByPeerAddress(_address); + MI32Client = NimBLEDevice::getClientByPeerAddress(NimBLEAddress(address)); if(MI32Client){ - // Should be impossible - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: got connected client"),D_CMND_MI32); + if(!MI32Client->connect(NimBLEAddress(address), 0,false)) { + MI32.mode.willConnect = 0; + vTaskDelete( NULL ); + } } else { - // Should be the norm after the first iteration MI32Client = NimBLEDevice::getDisconnectedClient(); - DEBUG_SENSOR_LOG(PSTR("%s: got disconnected client"),D_CMND_MI32); } } - - if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) { - MI32.mode.willConnect = 0; - DEBUG_SENSOR_LOG(PSTR("%s: max connection already reached"),D_CMND_MI32); - return false; - } if(!MI32Client) { - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will create client"),D_CMND_MI32); + if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) { + MI32.mode.willConnect = 0; + vTaskDelete( NULL ); + } MI32Client = NimBLEDevice::createClient(); MI32Client->setClientCallbacks(&MI32SensorCB , false); - MI32Client->setConnectionParams(12,12,0,48); - MI32Client->setConnectTimeout(30); + MI32Client->setConnectionParams(12,12,0,51); + MI32Client->setConnectTimeout(10); + if (!MI32Client->connect(NimBLEAddress(address),0,false)) { + MI32.mode.willConnect = 0; + NimBLEDevice::deleteClient(MI32Client); + vTaskDelete( NULL ); + } } - if (!MI32Client->connect(_address,false)) { - MI32.mode.willConnect = 0; - NimBLEDevice::deleteClient(MI32Client); - DEBUG_SENSOR_LOG(PSTR("%s: did not connect client"),D_CMND_MI32); - return false; - } - DEBUG_SENSOR_LOG(PSTR("%s: did create new client"),D_CMND_MI32); - return true; - // } } void MI32StartScanTask(){ if (MI32.mode.connected) return; MI32.mode.runningScan = 1; - // Wifi.counter = Wifi.counter + 3; xTaskCreatePinnedToCore( MI32ScanTask, /* Function to implement the task */ "MI32ScanTask", /* Name of the task */ - 8192, /* Stack size in words */ + 4096, /* Stack size in words */ NULL, /* Task input parameter */ 0, /* Priority of the task */ NULL, /* Task handle. */ @@ -458,18 +448,17 @@ void MI32StartScanTask(){ } void MI32ScanTask(void *pvParameters){ - if (MI32Scan == nullptr) MI32Scan = NimBLEDevice::getScan(); - DEBUG_SENSOR_LOG(PSTR("%s: Scan Cache Length: %u"),D_CMND_MI32, MI32Scan->getResults().getCount()); - MI32Scan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks); - MI32Scan->setActiveScan(false); - MI32Scan->start(5, MI32scanEndedCB, true); // hard coded duration + NimBLEScan* pScan = NimBLEDevice::getScan(); + pScan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks); + pScan->setActiveScan(false); + pScan->start(5, MI32scanEndedCB); // hard coded duration uint32_t timer = 0; while (MI32.mode.runningScan){ if (timer>15){ vTaskDelete( NULL ); } timer++; - vTaskDelay(1000/ portTICK_PERIOD_MS); + vTaskDelay(1000); } vTaskDelete( NULL ); } @@ -493,62 +482,47 @@ void MI32SensorTask(void *pvParameters){ MI32.mode.willConnect = 0; vTaskDelete( NULL ); } - if (MI32ConnectActiveSensor()){ - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ - MI32Client->disconnect(); - NimBLEDevice::deleteClient(MI32Client); - MI32.mode.willConnect = 0; - vTaskDelay(100/ portTICK_PERIOD_MS); - vTaskDelete( NULL ); - } - timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); - } - - timer = 150; - switch(MIBLEsensors[MI32.state.sensor].type){ - case LYWSD03MMC: - MI32.mode.readingDone = 0; - if(MI32connectLYWSD03forNotification()) timer=0; - break; - default: + MI32ConnectActiveSensor(); + MI32.mode.readingDone = 1; + switch(MIBLEsensors[MI32.state.sensor].type){ + case LYWSD03MMC: + MI32.mode.readingDone = 0; + MI32connectLYWSD03(); + break; + default: + break; + } + uint32_t timer = 0; + while (!MI32.mode.readingDone){ + if (timer>150){ break; } - - while (!MI32.mode.readingDone){ - if (timer>150){ - break; - } - timer++; - vTaskDelay(100/ portTICK_PERIOD_MS); - } - MI32Client->disconnect(); - DEBUG_SENSOR_LOG(PSTR("%s: requested disconnect"),D_CMND_MI32); + timer++; + vTaskDelay(100); } - vTaskDelay(500/ portTICK_PERIOD_MS); + MI32Client->disconnect(); + NimBLEDevice::deleteClient(MI32Client); + vTaskDelay(500); MI32.mode.connected = 0; vTaskDelete( NULL ); } -bool MI32connectLYWSD03forNotification(){ +void MI32connectLYWSD03(){ NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID(0xebe0ccb0,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6); - static BLEUUID charUUID(0xebe0ccc1,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6); + static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"); + static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"); pSvc = MI32Client->getService(serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(charUUID); } - if (pChr){ - if(pChr->canNotify()) { - if(pChr->registerForNotify(MI32notifyCB)) { - return true; - } + if(pChr->canNotify()) { + if(!pChr->registerForNotify(MI32notifyCB)) { + MI32.mode.willConnect = 0; + MI32Client->disconnect(); + return; } } - return false; } void MI32StartTimeTask(){ @@ -570,57 +544,123 @@ void MI32TimeTask(void *pvParameters){ MI32.mode.shallSetTime = 0; vTaskDelete( NULL ); } + MI32ConnectActiveSensor(); - if(MI32ConnectActiveSensor()){ - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ - break; - } - timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + break; } - - NimBLERemoteService* pSvc = nullptr; - NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); - static BLEUUID charUUID(0xEBE0CCB7,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); - pSvc = MI32Client->getService(serviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(charUUID); - + timer++; + vTaskDelay(10); } - if (pChr){ - if(pChr->canWrite()) { - union { - uint8_t buf[5]; - uint32_t time; - } _utc; - _utc.time = Rtc.utc_time; - _utc.buf[4] = Rtc.time_timezone / 60; - if(!pChr->writeValue(_utc.buf,sizeof(_utc.buf),true)) { // true is important ! - MI32.mode.willConnect = 0; - MI32Client->disconnect(); - } - else { - MI32.mode.shallSetTime = 0; - MI32.mode.willSetTime = 0; - } - } - } - MI32Client->disconnect(); + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID charUUID("EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6"); + pSvc = MI32Client->getService(serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(charUUID); } - vTaskDelay(500/ portTICK_PERIOD_MS); + if(pChr->canWrite()) { + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + + if(!pChr->writeValue(_utc.buf,sizeof(_utc.buf),true)) { // true is important ! + MI32.mode.willConnect = 0; + MI32Client->disconnect(); + } + else { + MI32.mode.shallSetTime = 0; + MI32.mode.willSetTime = 0; + } + } + MI32Client->disconnect(); + NimBLEDevice::deleteClient(MI32Client); + vTaskDelay(500); MI32.mode.connected = 0; vTaskDelete( NULL ); } +void MI32StartUnitTask(){ + MI32.mode.willConnect = 1; + xTaskCreatePinnedToCore( + MI32UnitTask, /* Function to implement the task */ + "MI32UnitTask", /* Name of the task */ + 8912, /* Stack size in words */ + NULL, /* Task input parameter */ + 15, /* Priority of the task */ + NULL, /* Task handle. */ + 0); /* Core where the task should run */ + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: Start unit set"),D_CMND_MI32); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: with sensor: %u"),D_CMND_MI32, MI32.state.sensor); +} + +void MI32UnitTask(void *pvParameters){ + if (MIBLEsensors[MI32.state.sensor].type != LYWSD02) { + MI32.mode.shallSetUnit = 0; + vTaskDelete( NULL ); + } + MI32ConnectActiveSensor(); + + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + break; + } + timer++; + vTaskDelay(10); + } + + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID charUUID("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"); + pSvc = MI32Client->getService(serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(charUUID); + } + + uint8_t curUnit; + + if(pChr->canRead()) { + const char *buf = pChr->readValue().c_str(); + if( buf[0] != 0 && buf[0]<101 ){ + curUnit = buf[0]; + } + } + else { + return; + } + + if(pChr->canWrite()) { + curUnit = curUnit == 0x01?0xFF:0x01; // C/F + + if(!pChr->writeValue(&curUnit,sizeof(curUnit),true)) { // true is important ! + MI32.mode.willConnect = 0; + MI32Client->disconnect(); + } + else { + MI32.mode.shallSetUnit = 0; + MI32.mode.willSetUnit = 0; + } + } + MI32Client->disconnect(); + NimBLEDevice::deleteClient(MI32Client); + vTaskDelay(500); + MI32.mode.connected = 0; + vTaskDelete( NULL ); +} + + void MI32StartBatteryTask(){ if (MI32.mode.connected) return; MI32.mode.willReadBatt = 1; - MI32.mode.willConnect = 1; - MI32.mode.canScan = 0; xTaskCreatePinnedToCore( MI32BatteryTask, /* Function to implement the task */ "MI32BatteryTask", /* Name of the task */ @@ -644,32 +684,31 @@ void MI32BatteryTask(void *pvParameters){ } MI32.mode.connected = 0; - if(MI32ConnectActiveSensor()){ - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ - break; - } - timer++; - vTaskDelay(30/ portTICK_PERIOD_MS); - } - - switch(MIBLEsensors[MI32.state.sensor].type){ - case FLORA: - MI32batteryFLORA(); - break; - case LYWSD02: - MI32batteryLYWSD02(); - break; - case CGD1: - MI32batteryCGD1(); + MI32ConnectActiveSensor(); + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ break; } - MI32Client->disconnect(); - } + timer++; + vTaskDelay(10); + } + + switch(MIBLEsensors[MI32.state.sensor].type){ + case FLORA: + MI32batteryFLORA(); + break; + case LYWSD02: + MI32batteryLYWSD02(); + break; + case CGD1: + MI32batteryCGD1(); + break; + } + MI32Client->disconnect(); MI32.mode.willReadBatt = 0; - // Wifi.counter = 0; // Now check it - vTaskDelay(500/ portTICK_PERIOD_MS); + NimBLEDevice::deleteClient(MI32Client); + vTaskDelay(500); MI32.mode.connected = 0; vTaskDelete( NULL ); } @@ -681,26 +720,27 @@ void MI32batteryFLORA(){ break; } timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); + vTaskDelay(10); } - DEBUG_SENSOR_LOG(PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID FLserviceUUID(0x00001204,0x0000,0x1000,0x800000805f9b34fb); - static BLEUUID FLcharUUID(0x00001a02,0x0000,0x1000,0x800000805f9b34fb); + static BLEUUID FLserviceUUID("00001204-0000-1000-8000-00805f9b34fb"); + static BLEUUID FLcharUUID("00001a02-0000-1000-8000-00805f9b34fb"); pSvc = MI32Client->getService(FLserviceUUID); - if(pSvc) { + if(pSvc) { /** make sure it's not null */ pChr = pSvc->getCharacteristic(FLcharUUID); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - if (pChr){ - DEBUG_SENSOR_LOG(PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); - } + else { + MI32.mode.readingDone = 1; + return; + } + if(pChr->canRead()) { + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); } - MI32.mode.readingDone = 1; } void MI32batteryLYWSD02(){ @@ -710,27 +750,27 @@ void MI32batteryLYWSD02(){ break; } timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); + vTaskDelay(10); } NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID LY2serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); - static BLEUUID LY2charUUID(0xEBE0CCC4,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + static BLEUUID LY2serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID LY2charUUID("EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6"); pSvc = MI32Client->getService(LY2serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(LY2charUUID); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - if (pChr){ - DEBUG_SENSOR_LOG( PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); - if(pChr->canRead()) { - DEBUG_SENSOR_LOG(PSTR("LYWSD02 char")); - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); - } + else { + return; + } + if(pChr->canRead()) { + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("LYWSD02 char")); + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); } - MI32.mode.readingDone = 1; } void MI32batteryCGD1(){ @@ -740,26 +780,26 @@ void MI32batteryCGD1(){ break; } timer++; - vTaskDelay(10/ portTICK_PERIOD_MS); + vTaskDelay(10); } NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID CGD1serviceUUID((uint16_t)0x180F); - static BLEUUID CGD1charUUID((uint16_t)0x2A19); + static BLEUUID CGD1serviceUUID("180F"); + static BLEUUID CGD1charUUID("2A19"); pSvc = MI32Client->getService(CGD1serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(CGD1charUUID); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - if (pChr){ - DEBUG_SENSOR_LOG(PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); - } + else { + return; + } + if(pChr->canRead()) { + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); } - MI32.mode.readingDone = 1; } @@ -779,14 +819,14 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ } MI32_ReverseMAC(_beacon.Mac); - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); + DEBUG_SENSOR_LOG(PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); + DEBUG_SENSOR_LOG(PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); if(MIBLEsensors[_slot].type==4 || MIBLEsensors[_slot].type==6){ DEBUG_SENSOR_LOG(PSTR("LYWSD03 and CGD1 no support for MiBeacon, type %u"),MIBLEsensors[_slot].type); return; } - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot); + DEBUG_SENSOR_LOG(PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot); switch(_beacon.type){ case 0x04: _tempFloat=(float)(_beacon.temp)/10.0f; @@ -794,7 +834,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].temp=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 4: temp updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + DEBUG_SENSOR_LOG(PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); break; case 0x06: _tempFloat=(float)(_beacon.hum)/10.0f; @@ -802,11 +842,11 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].hum=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 6: hum updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + DEBUG_SENSOR_LOG(PSTR("Mode 6: U16: %u Hum"), _beacon.hum); break; case 0x07: MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff; - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + DEBUG_SENSOR_LOG(PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); break; case 0x08: _tempFloat =(float)_beacon.moist; @@ -814,7 +854,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].moisture=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 8: moisture updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + DEBUG_SENSOR_LOG(PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); break; case 0x09: _tempFloat=(float)(_beacon.fert); @@ -822,14 +862,14 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].fertility=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 9: fertility updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + DEBUG_SENSOR_LOG(PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); break; case 0x0a: if(_beacon.bat<101){ MIBLEsensors[_slot].bat = _beacon.bat; DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); + DEBUG_SENSOR_LOG(PSTR("Mode a: U8: %u %%"), _beacon.bat); break; case 0x0d: _tempFloat=(float)(_beacon.HT.temp)/10.0f; @@ -842,7 +882,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].hum = _tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode d: hum updated")); } - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + DEBUG_SENSOR_LOG(PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); break; } } @@ -950,8 +990,6 @@ void MI32EverySecond(bool restart){ _counter = 0; MI32.mode.canScan = 0; MI32.mode.canConnect = 1; - MI32.mode.willReadBatt = 0; - MI32.mode.willConnect = 0; return; } @@ -964,6 +1002,15 @@ void MI32EverySecond(bool restart){ } } + if (MI32.mode.shallSetUnit) { + MI32.mode.canScan = 0; + MI32.mode.canConnect = 0; + if (MI32.mode.willSetUnit == 0){ + MI32.mode.willSetUnit = 1; + MI32StartTask(MI32_TASK_UNIT); + } + } + if (MI32.mode.willReadBatt) return; if (_counter>MI32.period) { @@ -981,9 +1028,9 @@ void MI32EverySecond(bool restart){ if(_counter==0) { MI32.state.sensor = _nextSensorSlot; - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u of %u"),D_CMND_MI32, MI32.state.sensor, MIBLEsensors.size()-1); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u"),D_CMND_MI32, MI32.state.sensor); MI32.mode.canScan = 0; - if (MI32.mode.runningScan|| MI32.mode.connected || MI32.mode.willConnect) return; + if (MI32.mode.runningScan == 1 || MI32.mode.connected == 1) return; _nextSensorSlot++; MI32.mode.canConnect = 1; if(MI32.mode.connected == 0) { @@ -997,7 +1044,7 @@ void MI32EverySecond(bool restart){ } } - if (_nextSensorSlot>(MIBLEsensors.size()-1)) { + if (MI32.state.sensor==MIBLEsensors.size()-1) { _nextSensorSlot= 0; _counter++; if (MI32.mode.shallReadBatt){ @@ -1057,6 +1104,21 @@ bool MI32Cmd(void) { } Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload); break; + case CMND_MI32_UNIT: + if (XdrvMailbox.data_len > 0) { + if(MIBLEsensors.size()>XdrvMailbox.payload){ + if(MIBLEsensors[XdrvMailbox.payload].type == LYWSD02){ + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will set Unit"),D_CMND_MI32); + MI32.state.sensor = XdrvMailbox.payload; + MI32.mode.canScan = 0; + MI32.mode.canConnect = 0; + MI32.mode.shallSetUnit = 1; + MI32.mode.willSetUnit = 0; + } + } + } + Response_P(S_JSON_MI32_COMMAND_NVALUE, command, XdrvMailbox.payload); + break; case CMND_MI32_PAGE: if (XdrvMailbox.data_len > 0) { if (XdrvMailbox.payload == 0) XdrvMailbox.payload = MI32.perPage; // ignore 0 @@ -1097,7 +1159,6 @@ const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}"; void MI32Show(bool json) { - if (json) { for (uint32_t i = 0; i < MIBLEsensors.size(); i++) { /* @@ -1111,7 +1172,7 @@ void MI32Show(bool json) MIBLEsensors[i].serial[3], MIBLEsensors[i].serial[4], MIBLEsensors[i].serial[5]); if (MIBLEsensors[i].type == FLORA) { - if (!isnan(MIBLEsensors[i].temp)) { + if (!isnan(MIBLEsensors[i].temp)) { // this is the error code -> no temperature char temperature[FLOATSZ]; // all sensors have temperature dtostrfd(MIBLEsensors[i].temp, Settings.flag2.temperature_resolution, temperature); ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%s"), temperature); @@ -1238,4 +1299,4 @@ bool Xsns62(uint8_t function) return result; } #endif // USE_MI_ESP32 -#endif // ESP32 \ No newline at end of file +#endif // ESP32 From 671942aa60a241868ca48c98593ed836eb33b2f8 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Fri, 29 May 2020 20:54:15 +0200 Subject: [PATCH 09/48] faster ili9488 fill --- lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp b/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp index e507fdf1b..495c18ad8 100644 --- a/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp +++ b/lib/JaretBurkett_ILI9488-gemu-1.0/ILI9488.cpp @@ -74,6 +74,9 @@ uint8_t ili9488_start; // ESP8266 #include "spi_register.h" #define SWSPI_OPTMODE +// this enables the 27 bit packed mode +#define RGB_PACK_MODE + uint32_t ili9488_clock; uint32_t ili9488_usr; uint32_t ili9488_usr1; @@ -259,6 +262,8 @@ void ILI9488::writecommand(uint8_t c) { #include "esp32-hal.h" #include "soc/spi_struct.h" +#define RGB_PACK_MODE + // since ardunio transferBits ia completely disfunctional // we use our own hardware driver for 9 bit spi void ILI9488::fastSPIwrite(uint8_t d,uint8_t dc) { @@ -831,8 +836,7 @@ void ILI9488::fillScreen(uint16_t color) { //#define WRITE_SPI_REG -// this enables the 27 bit packed mode -#define RGB_PACK_MODE + // extremely strange => if this code is merged into pack_rgb() the software crashes // swap bytes @@ -1023,17 +1027,39 @@ void ILI9488::fillRect(int16_t x, int16_t y, int16_t w, int16_t h, uint16_t colo g = (g * 255) / 63; b = (b * 255) / 31; +#ifdef RGB_PACK_MODE + // init 27 bit mode + uint32_t data=pack_rgb(r,g,b); + REG_SET_BIT(SPI_USER_REG(3), SPI_USR_MOSI); + REG_WRITE(SPI_MOSI_DLEN_REG(3), 27 - 1); + uint32_t *dp=(uint32_t*)SPI_W0_REG(3); + digitalWrite( _cs, LOW); +#endif + for(y=h; y>0; y--) { for(x=w; x>0; x--) { + #ifndef RGB_PACK_MODE writedata(r); writedata(g); writedata(b); + #else + while (REG_GET_FIELD(SPI_CMD_REG(3), SPI_USR)); + *dp=data; + REG_SET_BIT(SPI_CMD_REG(3), SPI_USR); + #endif } } + +#ifdef RGB_PACK_MODE + while (REG_GET_FIELD(SPI_CMD_REG(3), SPI_USR)); + digitalWrite( _cs, HIGH); +#endif + ILI9488_STOP } #endif + // Pass 8-bit (each) R,G,B, get back 16-bit packed color uint16_t ILI9488::color565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); From b069b89561069246a2a4569e6daf585e04275e62 Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Fri, 29 May 2020 22:52:45 +0200 Subject: [PATCH 10/48] Add Zigbee options to ``ZbSend`` to write and report attributes --- tasmota/CHANGELOG.md | 1 + tasmota/i18n.h | 5 + tasmota/xdrv_10_rules.ino | 32 + tasmota/xdrv_23_zigbee_2_devices.ino | 44 +- tasmota/xdrv_23_zigbee_5_converters.ino | 1066 ++++++++++++--------- tasmota/xdrv_23_zigbee_7_statemachine.ino | 19 +- tasmota/xdrv_23_zigbee_8_parsers.ino | 7 +- tasmota/xdrv_23_zigbee_9_impl.ino | 560 +++++++---- 8 files changed, 1025 insertions(+), 709 deletions(-) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 487c94e64..9fb3490fc 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -11,6 +11,7 @@ - Add Three Phase Export Active Energy to SDM630 driver - Add wildcard pattern ``?`` for JSON matching in rules - Add support for unique MQTTClient (and inherited fallback topic) by full Mac address using ``mqttclient DVES_%12X`` (#8300) +- Add Zigbee options to ``ZbSend`` to write and report attributes ### 8.3.1.1 20200518 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 89d931908..e5f3e395a 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -513,10 +513,15 @@ #define D_CMND_ZIGBEE_FORGET "Forget" #define D_CMND_ZIGBEE_SAVE "Save" #define D_CMND_ZIGBEE_LINKQUALITY "LinkQuality" + #define D_CMND_ZIGBEE_CLUSTER "Cluster" #define D_CMND_ZIGBEE_ENDPOINT "Endpoint" #define D_CMND_ZIGBEE_GROUP "Group" + #define D_CMND_ZIGBEE_MANUF "Manuf" + #define D_CMND_ZIGBEE_DEVICE "Device" #define D_CMND_ZIGBEE_READ "Read" #define D_CMND_ZIGBEE_SEND "Send" +#define D_CMND_ZIGBEE_WRITE "Write" +#define D_CMND_ZIGBEE_REPORT "Report" #define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent" #define D_JSON_ZIGBEE_RECEIVED "ZbReceived" #define D_CMND_ZIGBEE_BIND "Bind" diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino index 1ad618485..8dd185a29 100644 --- a/tasmota/xdrv_10_rules.ino +++ b/tasmota/xdrv_10_rules.ino @@ -458,6 +458,21 @@ bool RulesRuleMatch(uint8_t rule_set, String &event, String &rule) rule_param = String(SunMinutes(1)); } #endif // USE_TIMERS and USE_SUNRISE +// #ifdef USE_ZIGBEE +// if (rule_param.startsWith(F("%ZBDEVICE%"))) { +// snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice()); +// rule_param = String(stemp); +// } +// if (rule_param.startsWith(F("%ZBGROUP%"))) { +// rule_param = String(Z_GetLastGroup()); +// } +// if (rule_param.startsWith(F("%ZBCLUSTER%"))) { +// rule_param = String(Z_GetLastCluster()); +// } +// if (rule_param.startsWith(F("%ZBENDPOINT%"))) { +// rule_param = String(Z_GetLastEndpoint()); +// } +// #endif rule_param.toUpperCase(); strlcpy(rule_svalue, rule_param.c_str(), sizeof(rule_svalue)); @@ -701,6 +716,13 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved) RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0))); RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1))); #endif // USE_TIMERS and USE_SUNRISE +#ifdef USE_ZIGBEE + snprintf_P(stemp, sizeof(stemp), PSTR("0x%04X"), Z_GetLastDevice()); + RulesVarReplace(commands, F("%ZBDEVICE%"), String(stemp)); + RulesVarReplace(commands, F("%ZBGROUP%"), String(Z_GetLastGroup())); + RulesVarReplace(commands, F("%ZBCLUSTER%"), String(Z_GetLastCluster())); + RulesVarReplace(commands, F("%ZBENDPOINT%"), String(Z_GetLastEndpoint())); +#endif char command[commands.length() +1]; strlcpy(command, commands.c_str(), sizeof(command)); @@ -1261,6 +1283,16 @@ bool findNextVariableValue(char * &pVarname, float &value) } else if (sVarName.equals(F("SUNSET"))) { value = SunMinutes(1); #endif +// #ifdef USE_ZIGBEE +// // } else if (sVarName.equals(F("ZBDEVICE"))) { +// // value = Z_GetLastDevice(); +// } else if (sVarName.equals(F("ZBGROUP"))) { +// value = Z_GetLastGroup(); +// } else if (sVarName.equals(F("ZBCLUSTER"))) { +// value = Z_GetLastCluster(); +// } else if (sVarName.equals(F("ZBENDPOINT"))) { +// value = Z_GetLastEndpoint(); +// #endif } else { succeed = false; } diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index c115052f7..60f116002 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -26,6 +26,24 @@ #endif const uint16_t kZigbeeSaveDelaySeconds = ZIGBEE_SAVE_DELAY_SECONDS; // wait for x seconds +/*********************************************************************************************\ + * Structures for Rules variables related to the last received message +\*********************************************************************************************/ + +typedef struct Z_LastMessageVars { + uint16_t device; // device short address + uint16_t groupaddr; // group address + uint16_t cluster; // cluster id + uint8_t endpoint; // source endpoint +} Z_LastMessageVars; + +Z_LastMessageVars gZbLastMessage; + +uint16_t Z_GetLastDevice(void) { return gZbLastMessage.device; } +uint16_t Z_GetLastGroup(void) { return gZbLastMessage.groupaddr; } +uint16_t Z_GetLastCluster(void) { return gZbLastMessage.cluster; } +uint8_t Z_GetLastEndpoint(void) { return gZbLastMessage.endpoint; } + /*********************************************************************************************\ * Structures for device configuration \*********************************************************************************************/ @@ -256,7 +274,7 @@ int32_t Z_Devices::findEndpointInVector(const std::vector & vecOfElements, u // entry with same shortaddr or longaddr exists. // Z_Device & Z_Devices::createDeviceEntry(uint16_t shortaddr, uint64_t longaddr) { - if (!shortaddr && !longaddr) { return *(Z_Device*) nullptr; } // it is not legal to create an enrty with both short/long addr null + if ((BAD_SHORTADDR == shortaddr) && !longaddr) { return *(Z_Device*) nullptr; } // it is not legal to create this entry //Z_Device* device_alloc = (Z_Device*) malloc(sizeof(Z_Device)); Z_Device* device_alloc = new Z_Device{ longaddr, @@ -340,7 +358,7 @@ int32_t Z_Devices::findFriendlyName(const char * name) const { if (name_len) { for (auto &elem : _devices) { if (elem->friendlyName) { - if (strcmp(elem->friendlyName, name) == 0) { return found; } + if (strcasecmp(elem->friendlyName, name) == 0) { return found; } } found++; } @@ -860,21 +878,21 @@ const JsonObject *Z_Devices::jsonGet(uint16_t shortaddr) { void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { Z_Device & device = getShortAddr(shortaddr); if (&device == nullptr) { return; } // don't crash if not found - JsonObject * json = device.json; - if (json == nullptr) { return; } // abort if nothing in buffer + JsonObject & json = *device.json; + if (&json == nullptr) { return; } // abort if nothing in buffer const char * fname = zigbee_devices.getFriendlyName(shortaddr); bool use_fname = (Settings.flag4.zigbee_use_names) && (fname); // should we replace shortaddr with friendlyname? - // Remove redundant "Name" or "Device" - if (use_fname) { - json->remove(F(D_JSON_ZIGBEE_NAME)); - } else { - json->remove(F(D_JSON_ZIGBEE_DEVICE)); - } + // save parameters is global variables to be used by Rules + gZbLastMessage.device = shortaddr; // %zbdevice% + gZbLastMessage.groupaddr = json[F(D_CMND_ZIGBEE_GROUP)]; // %zbgroup% + gZbLastMessage.cluster = json[F(D_CMND_ZIGBEE_CLUSTER)]; // %zbcluster% + gZbLastMessage.endpoint = json[F(D_CMND_ZIGBEE_ENDPOINT)]; // %zbendpoint% + // dump json in string String msg = ""; - json->printTo(msg); + json.printTo(msg); zigbee_devices.jsonClear(shortaddr); if (use_fname) { @@ -889,7 +907,7 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) { } else { MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); } - XdrvRulesProcess(); + XdrvRulesProcess(); // apply rules } void Z_Devices::jsonPublishNow(uint16_t shortaddr, JsonObject & values) { @@ -923,7 +941,7 @@ uint16_t Z_Devices::parseDeviceParam(const char * param, bool short_must_be_know if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload <= 99)) { shortaddr = zigbee_devices.isKnownIndex(XdrvMailbox.payload - 1); } - } else if ((dataBuf[0] == '0') && (dataBuf[1] == 'x')) { + } else if ((dataBuf[0] == '0') && ((dataBuf[1] == 'x') || (dataBuf[1] == 'X'))) { // starts with 0x if (strlen(dataBuf) < 18) { // expect a short address diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index dfb69d4a7..5da74a1ed 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -79,476 +79,6 @@ uint8_t Z_getDatatypeLen(uint8_t t) { } } -typedef union ZCLHeaderFrameControl_t { - struct { - uint8_t frame_type : 2; // 00 = across entire profile, 01 = cluster specific - uint8_t manuf_specific : 1; // Manufacturer Specific Sub-field - uint8_t direction : 1; // 0 = tasmota to zigbee, 1 = zigbee to tasmota - uint8_t disable_def_resp : 1; // don't send back default response - uint8_t reserved : 3; - } b; - uint32_t d8; // raw 8 bits field -} ZCLHeaderFrameControl_t; - - -class ZCLFrame { -public: - - ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, - const char *buf, size_t buf_len, uint16_t clusterid, uint16_t groupaddr, - uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, - uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, - uint32_t timestamp): - _manuf_code(manuf_code), _transact_seq(transact_seq), _cmd_id(cmd_id), - _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough - _cluster_id(clusterid), _groupaddr(groupaddr), - _srcaddr(srcaddr), _srcendpoint(srcendpoint), _dstendpoint(dstendpoint), _wasbroadcast(wasbroadcast), - _linkquality(linkquality), _securityuse(securityuse), _seqnumber(seqnumber), - _timestamp(timestamp) - { - _frame_control.d8 = frame_control; - _payload.addBuffer(buf, buf_len); - }; - - - void log(void) { - char hex_char[_payload.len()*2+2]; - ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); - Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" - "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," - "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," - "\"" D_CMND_ZIGBEE_LINKQUALITY "\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," - "\"timestamp\":%d," - "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," - "\"cmdid\":\"0x%02X\",\"payload\":\"%s\"}}"), - _groupaddr, _cluster_id, _srcaddr, - _srcendpoint, _dstendpoint, _wasbroadcast, - _linkquality, _securityuse, _seqnumber, - _timestamp, - _frame_control, _manuf_code, _transact_seq, _cmd_id, - hex_char); - if (Settings.flag3.tuya_serial_mqtt_publish) { - MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); - XdrvRulesProcess(); - } else { - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), mqtt_data); - } - } - - static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid, - uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, - uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, - uint32_t timestamp) { // parse a raw frame and build the ZCL frame object - uint32_t i = offset; - ZCLHeaderFrameControl_t frame_control; - uint16_t manuf_code = 0; - uint8_t transact_seq; - uint8_t cmd_id; - - frame_control.d8 = buf.get8(i++); - if (frame_control.b.manuf_specific) { - manuf_code = buf.get16(i); - i += 2; - } - transact_seq = buf.get8(i++); - cmd_id = buf.get8(i++); - ZCLFrame zcl_frame(frame_control.d8, manuf_code, transact_seq, cmd_id, - (const char *)(buf.buf() + i), len + offset - i, - clusterid, groupid, - srcaddr, srcendpoint, dstendpoint, wasbroadcast, - linkquality, securityuse, seqnumber, - timestamp); - return zcl_frame; - } - - bool isClusterSpecificCommand(void) { - return _frame_control.b.frame_type & 1; - } - - static void generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len); - void parseRawAttributes(JsonObject& json, uint8_t offset = 0); - void parseReadAttributes(JsonObject& json, uint8_t offset = 0); - void parseResponse(void); - void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); - void postProcessAttributes(uint16_t shortaddr, JsonObject& json); - - inline void setGroupId(uint16_t groupid) { - _groupaddr = groupid; - } - - inline void setClusterId(uint16_t clusterid) { - _cluster_id = clusterid; - } - - inline uint8_t getCmdId(void) const { - return _cmd_id; - } - - inline uint16_t getClusterId(void) const { - return _cluster_id; - } - - inline uint16_t getSrcEndpoint(void) const { - return _srcendpoint; - } - - const SBuffer &getPayload(void) const { - return _payload; - } - - uint16_t getManufCode(void) const { - return _manuf_code; - } - -private: - ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; - uint16_t _manuf_code = 0; // optional - uint8_t _transact_seq = 0; // transaction sequence number - uint8_t _cmd_id = 0; - SBuffer _payload; - uint16_t _cluster_id = 0; - uint16_t _groupaddr = 0; - // information from decoded ZCL frame - uint16_t _srcaddr; - uint8_t _srcendpoint; - uint8_t _dstendpoint; - uint8_t _wasbroadcast; - uint8_t _linkquality; - uint8_t _securityuse; - uint8_t _seqnumber; - uint32_t _timestamp; -}; - -// Zigbee ZCL converters - -// from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55 -// Input voltage in mV, i.e. 3000 = 3.000V -// Output percentage from 0 to 100 as int -uint8_t toPercentageCR2032(uint32_t voltage) { - uint32_t percentage; - if (voltage < 2100) { - percentage = 0; - } else if (voltage < 2440) { - percentage = 6 - ((2440 - voltage) * 6) / 340; - } else if (voltage < 2740) { - percentage = 18 - ((2740 - voltage) * 12) / 300; - } else if (voltage < 2900) { - percentage = 42 - ((2900 - voltage) * 24) / 160; - } else if (voltage < 3000) { - percentage = 100 - ((3000 - voltage) * 58) / 100; - } else if (voltage >= 3000) { - percentage = 100; - } - return percentage; -} - - -uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, - uint32_t offset, uint32_t buflen) { - - uint32_t i = offset; - uint32_t attrtype = buf.get8(i++); - - // fallback - enter a null value - json[attrid_str] = (char*) nullptr; - - uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes - - // now parse accordingly to attr type - switch (attrtype) { - // case Znodata: // nodata - // case Zunk: // unk - // break; - case Zbool: // bool - case Zuint8: // uint8 - case Zenum8: // enum8 - { - uint8_t uint8_val = buf.get8(i); - // i += 1; - if (0xFF != uint8_val) { - json[attrid_str] = uint8_val; - } - } - break; - case Zuint16: // uint16 - case Zenum16: // enum16 - { - uint16_t uint16_val = buf.get16(i); - // i += 2; - if (0xFFFF != uint16_val) { - json[attrid_str] = uint16_val; - } - } - break; - case Zuint32: // uint32 - { - uint32_t uint32_val = buf.get32(i); - // i += 4; - if (0xFFFFFFFF != uint32_val) { - json[attrid_str] = uint32_val; - } - } - break; - // Note: uint40, uint48, uint56, uint64 are displayed as Hex - // Note: int40, int48, int56, int64 are displayed as Hex - case Zuint40: // uint40 - case Zuint48: // uint48 - case Zuint56: // uint56 - case Zuint64: // uint64 - case Zint40: // int40 - case Zint48: // int48 - case Zint56: // int56 - case Zint64: // int64 - { - // uint8_t len = attrtype - 0x27; // 5 - 8 - // print as HEX - char hex[2*len+1]; - ToHex_P(buf.buf(i), len, hex, sizeof(hex)); - json[attrid_str] = hex; - // i += len; - } - break; - case Zint8: // int8 - { - int8_t int8_val = buf.get8(i); - // i += 1; - if (0x80 != int8_val) { - json[attrid_str] = int8_val; - } - } - break; - case Zint16: // int16 - { - int16_t int16_val = buf.get16(i); - // i += 2; - if (0x8000 != int16_val) { - json[attrid_str] = int16_val; - } - } - break; - case Zint32: // int32 - { - int32_t int32_val = buf.get32(i); - // i += 4; - if (0x80000000 != int32_val) { - json[attrid_str] = int32_val; - } - } - break; - - case Zoctstr: // octet string, 1 byte len - case Zstring: // char string, 1 byte len - case Zoctstr16: // octet string, 2 bytes len - case Zstring16: // char string, 2 bytes len - // For strings, default is to try to do a real string, but reverts to octet stream if null char is present or on some exceptions - { - bool parse_as_string = true; - len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits - i += (attrtype <= 0x42) ? 1 : 2; // increment pointer - if (i + len > buf.len()) { // make sure we don't get past the buffer - len = buf.len() - i; - } - - // check if we can safely use a string - if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; } - - if (parse_as_string) { - char str[len+1]; - strncpy(str, buf.charptr(i), len); - str[len] = 0x00; - json[attrid_str] = str; - } else { - // print as HEX - char hex[2*len+1]; - ToHex_P(buf.buf(i), len, hex, sizeof(hex)); - json[attrid_str] = hex; - } - - // i += len; - // break; - } - // i += buf.get8(i) + 1; - break; - - case Zdata8: // data8 - case Zmap8: // map8 - { - uint8_t uint8_val = buf.get8(i); - // i += 1; - json[attrid_str] = uint8_val; - } - break; - case Zdata16: // data16 - case Zmap16: // map16 - { - uint16_t uint16_val = buf.get16(i); - // i += 2; - json[attrid_str] = uint16_val; - } - break; - case Zdata32: // data32 - case Zmap32: // map32 - { - uint32_t uint32_val = buf.get32(i); - // i += 4; - json[attrid_str] = uint32_val; - } - break; - - case Zsingle: // float - { - uint32_t uint32_val = buf.get32(i); - float * float_val = (float*) &uint32_val; - // i += 4; - json[attrid_str] = *float_val; - } - break; - - // TODO - case ZToD: // ToD - case Zdate: // date - case ZUTC: // UTC - case ZclusterId: // clusterId - case ZattribId: // attribId - case ZbacOID: // bacOID - case ZEUI64: // EUI64 - case Zkey128: // key128 - case Zsemi: // semi (float on 2 bytes) - break; - - // Other un-implemented data types - case Zdata24: // data24 - case Zdata40: // data40 - case Zdata48: // data48 - case Zdata56: // data56 - case Zdata64: // data64 - break; - // map - case Zmap24: // map24 - case Zmap40: // map40 - case Zmap48: // map48 - case Zmap56: // map56 - case Zmap64: // map64 - break; - case Zdouble: // double precision - { - uint64_t uint64_val = buf.get64(i); - double * double_val = (double*) &uint64_val; - // i += 8; - json[attrid_str] = *double_val; - } - break; - } - i += len; - - // String pp; // pretty print - // json[attrid_str].prettyPrintTo(pp); - // // now store the attribute - // AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZCL attribute decoded, id %s, type 0x%02X, val=%s"), - // attrid_str, attrtype, pp.c_str()); - return i - offset; // how much have we increased the index -} - -// Generate an attribute name based on cluster number, attribute, and suffix if duplicates -void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len) { - uint32_t suffix = 1; - - snprintf_P(key, key_len, PSTR("%04X/%04X"), cluster, attr); - while (json.containsKey(key)) { - suffix++; - snprintf_P(key, key_len, PSTR("%04X/%04X+%d"), cluster, attr, suffix); // add "0008/0001+2" suffix if duplicate - } -} - -// First pass, parse all attributes in their native format -void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; - uint32_t len = _payload.len(); - - while (len >= i + 3) { - uint16_t attrid = _payload.get16(i); - i += 2; - - char key[16]; - generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); - - // exception for Xiaomi lumi.weather - specific field to be treated as octet and not char - if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) { - if (0x42 == _payload.get8(i)) { - _payload.set8(i, 0x41); // change type from 0x42 to 0x41 - } - } - i += parseSingleAttribute(json, key, _payload, i, len); - } -} - -// ZCL_READ_ATTRIBUTES_RESPONSE -void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { - uint32_t i = offset; - uint32_t len = _payload.len(); - - while (len - i >= 4) { - uint16_t attrid = _payload.get16(i); - i += 2; - uint8_t status = _payload.get8(i++); - - if (0 == status) { - char key[16]; - generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); - - i += parseSingleAttribute(json, key, _payload, i, len); - } - } -} - -// ZCL_DEFAULT_RESPONSE -void ZCLFrame::parseResponse(void) { - if (_payload.len() < 2) { return; } // wrong format - uint8_t cmd = _payload.get8(0); - uint8_t status = _payload.get8(1); - - DynamicJsonBuffer jsonBuffer; - JsonObject& json = jsonBuffer.createObject(); - - // "Device" - char s[12]; - snprintf_P(s, sizeof(s), PSTR("0x%04X"), _srcaddr); - json[F(D_JSON_ZIGBEE_DEVICE)] = s; - // "Name" - const char * friendlyName = zigbee_devices.getFriendlyName(_srcaddr); - if (friendlyName) { - json[F(D_JSON_ZIGBEE_NAME)] = (char*) friendlyName; - } - // "Command" - snprintf_P(s, sizeof(s), PSTR("%04X!%02X"), _cluster_id, cmd); - json[F(D_JSON_ZIGBEE_CMD)] = s; - // "Status" - json[F(D_JSON_ZIGBEE_STATUS)] = status; - // "StatusMessage" - json[F(D_JSON_ZIGBEE_STATUS_MSG)] = getZigbeeStatusMessage(status); - // Add Endpoint - json[F(D_CMND_ZIGBEE_ENDPOINT)] = _srcendpoint; - // Add Group if non-zero - if (_groupaddr) { - json[F(D_CMND_ZIGBEE_GROUP)] = _groupaddr; - } - // Add linkquality - json[F(D_CMND_ZIGBEE_LINKQUALITY)] = _linkquality; - - String msg(""); - msg.reserve(100); - json.printTo(msg); - Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), msg.c_str()); - MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); - XdrvRulesProcess(); -} - - -// Parse non-normalized attributes -void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { - convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _payload); - sendHueUpdate(_srcaddr, _groupaddr, _cluster_id, _cmd_id, _frame_control.b.direction); -} // return value: // 0 = keep initial value @@ -684,13 +214,13 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zuint8, Cx0000, 0x0002, Z(StackVersion), &Z_Copy }, { Zuint8, Cx0000, 0x0003, Z(HWVersion), &Z_Copy }, { Zstring, Cx0000, 0x0004, Z(Manufacturer), &Z_ManufKeep }, // record Manufacturer - { Zstring, Cx0000, 0x0005, Z(ModelId), &Z_ModelKeep }, // record Model + { Zstring, Cx0000, 0x0005, Z(ModelId), &Z_ModelKeep }, // record Model { Zstring, Cx0000, 0x0006, Z(DateCode), &Z_Copy }, { Zenum8, Cx0000, 0x0007, Z(PowerSource), &Z_Copy }, { Zstring, Cx0000, 0x4000, Z(SWBuildID), &Z_Copy }, - { Zunk, Cx0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zunk, Cx0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { Zmap8, Cx0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) + { Zmap8, Cx0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) // Power Configuration cluster { Zuint16, Cx0001, 0x0000, Z(MainsVoltage), &Z_Copy }, @@ -712,8 +242,8 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { //{ Zmap8, Cx0005, 0x0004, Z(NameSupport), &Z_Copy }, // On/off cluster - { Zbool, Cx0006, 0x0000, Z(Power), &Z_Copy }, - { Zbool, Cx0006, 0x8000, Z(Power), &Z_Copy }, // See 7280 + { Zbool, Cx0006, 0x0000, Z(Power), &Z_Copy }, + { Zbool, Cx0006, 0x8000, Z(Power), &Z_Copy }, // See 7280 // On/Off Switch Configuration cluster { Zenum8, Cx0007, 0x0000, Z(SwitchType), &Z_Copy }, @@ -735,7 +265,7 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { { Zmap8, Cx000A, 0x0001, Z(TimeStatus), &Z_Copy }, { Zint32, Cx000A, 0x0002, Z(TimeZone), &Z_Copy }, { Zuint32, Cx000A, 0x0003, Z(DstStart), &Z_Copy }, - { Zuint32, Cx000A, 0x0004, Z(DstEnd), &Z_Copy }, + { Zuint32, Cx000A, 0x0004, Z(DstEnd), &Z_Copy }, { Zint32, Cx000A, 0x0005, Z(DstShift), &Z_Copy }, { Zuint32, Cx000A, 0x0006, Z(StandardTime), &Z_Copy }, { Zuint32, Cx000A, 0x0007, Z(LocalTime), &Z_Copy }, @@ -1032,6 +562,590 @@ const Z_AttributeConverter Z_PostProcess[] PROGMEM = { }; + +typedef union ZCLHeaderFrameControl_t { + struct { + uint8_t frame_type : 2; // 00 = across entire profile, 01 = cluster specific + uint8_t manuf_specific : 1; // Manufacturer Specific Sub-field + uint8_t direction : 1; // 0 = tasmota to zigbee, 1 = zigbee to tasmota + uint8_t disable_def_resp : 1; // don't send back default response + uint8_t reserved : 3; + } b; + uint32_t d8; // raw 8 bits field +} ZCLHeaderFrameControl_t; + + +class ZCLFrame { +public: + + ZCLFrame(uint8_t frame_control, uint16_t manuf_code, uint8_t transact_seq, uint8_t cmd_id, + const char *buf, size_t buf_len, uint16_t clusterid, uint16_t groupaddr, + uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, + uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, + uint32_t timestamp): + _manuf_code(manuf_code), _transact_seq(transact_seq), _cmd_id(cmd_id), + _payload(buf_len ? buf_len : 250), // allocate the data frame from source or preallocate big enough + _cluster_id(clusterid), _groupaddr(groupaddr), + _srcaddr(srcaddr), _srcendpoint(srcendpoint), _dstendpoint(dstendpoint), _wasbroadcast(wasbroadcast), + _linkquality(linkquality), _securityuse(securityuse), _seqnumber(seqnumber), + _timestamp(timestamp) + { + _frame_control.d8 = frame_control; + _payload.addBuffer(buf, buf_len); + }; + + + void log(void) { + char hex_char[_payload.len()*2+2]; + ToHex_P((unsigned char*)_payload.getBuffer(), _payload.len(), hex_char, sizeof(hex_char)); + Response_P(PSTR("{\"" D_JSON_ZIGBEEZCL_RECEIVED "\":{" + "\"groupid\":%d," "\"clusterid\":%d," "\"srcaddr\":\"0x%04X\"," + "\"srcendpoint\":%d," "\"dstendpoint\":%d," "\"wasbroadcast\":%d," + "\"" D_CMND_ZIGBEE_LINKQUALITY "\":%d," "\"securityuse\":%d," "\"seqnumber\":%d," + "\"timestamp\":%d," + "\"fc\":\"0x%02X\",\"manuf\":\"0x%04X\",\"transact\":%d," + "\"cmdid\":\"0x%02X\",\"payload\":\"%s\"}}"), + _groupaddr, _cluster_id, _srcaddr, + _srcendpoint, _dstendpoint, _wasbroadcast, + _linkquality, _securityuse, _seqnumber, + _timestamp, + _frame_control, _manuf_code, _transact_seq, _cmd_id, + hex_char); + if (Settings.flag3.tuya_serial_mqtt_publish) { + MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR)); + XdrvRulesProcess(); + } else { + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE "%s"), mqtt_data); + } + } + + static ZCLFrame parseRawFrame(const SBuffer &buf, uint8_t offset, uint8_t len, uint16_t clusterid, uint16_t groupid, + uint16_t srcaddr, uint8_t srcendpoint, uint8_t dstendpoint, uint8_t wasbroadcast, + uint8_t linkquality, uint8_t securityuse, uint8_t seqnumber, + uint32_t timestamp) { // parse a raw frame and build the ZCL frame object + uint32_t i = offset; + ZCLHeaderFrameControl_t frame_control; + uint16_t manuf_code = 0; + uint8_t transact_seq; + uint8_t cmd_id; + + frame_control.d8 = buf.get8(i++); + if (frame_control.b.manuf_specific) { + manuf_code = buf.get16(i); + i += 2; + } + transact_seq = buf.get8(i++); + cmd_id = buf.get8(i++); + ZCLFrame zcl_frame(frame_control.d8, manuf_code, transact_seq, cmd_id, + (const char *)(buf.buf() + i), len + offset - i, + clusterid, groupid, + srcaddr, srcendpoint, dstendpoint, wasbroadcast, + linkquality, securityuse, seqnumber, + timestamp); + return zcl_frame; + } + + bool isClusterSpecificCommand(void) { + return _frame_control.b.frame_type & 1; + } + + static void generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len); + void parseRawAttributes(JsonObject& json, uint8_t offset = 0); + void parseReadAttributes(JsonObject& json, uint8_t offset = 0); + void parseReadAttributesResponse(JsonObject& json, uint8_t offset = 0); + void parseResponse(void); + void parseClusterSpecificCommand(JsonObject& json, uint8_t offset = 0); + void postProcessAttributes(uint16_t shortaddr, JsonObject& json); + + inline void setGroupId(uint16_t groupid) { + _groupaddr = groupid; + } + + inline void setClusterId(uint16_t clusterid) { + _cluster_id = clusterid; + } + + inline uint8_t getCmdId(void) const { + return _cmd_id; + } + + inline uint16_t getClusterId(void) const { + return _cluster_id; + } + + inline uint16_t getSrcEndpoint(void) const { + return _srcendpoint; + } + + const SBuffer &getPayload(void) const { + return _payload; + } + + uint16_t getManufCode(void) const { + return _manuf_code; + } + +private: + ZCLHeaderFrameControl_t _frame_control = { .d8 = 0 }; + uint16_t _manuf_code = 0; // optional + uint8_t _transact_seq = 0; // transaction sequence number + uint8_t _cmd_id = 0; + SBuffer _payload; + uint16_t _cluster_id = 0; + uint16_t _groupaddr = 0; + // information from decoded ZCL frame + uint16_t _srcaddr; + uint8_t _srcendpoint; + uint8_t _dstendpoint; + uint8_t _wasbroadcast; + uint8_t _linkquality; + uint8_t _securityuse; + uint8_t _seqnumber; + uint32_t _timestamp; +}; + +// Zigbee ZCL converters + +// from https://github.com/Koenkk/zigbee-shepherd-converters/blob/638d29f0cace6343052b9a4e7fd60980fa785479/converters/fromZigbee.js#L55 +// Input voltage in mV, i.e. 3000 = 3.000V +// Output percentage from 0 to 100 as int +uint8_t toPercentageCR2032(uint32_t voltage) { + uint32_t percentage; + if (voltage < 2100) { + percentage = 0; + } else if (voltage < 2440) { + percentage = 6 - ((2440 - voltage) * 6) / 340; + } else if (voltage < 2740) { + percentage = 18 - ((2740 - voltage) * 12) / 300; + } else if (voltage < 2900) { + percentage = 42 - ((2900 - voltage) * 24) / 160; + } else if (voltage < 3000) { + percentage = 100 - ((3000 - voltage) * 58) / 100; + } else if (voltage >= 3000) { + percentage = 100; + } + return percentage; +} + +// +// Appends the attribute value to Write or to Report +// Adds to buf: +// - 2 bytes: attribute identigier +// - 1 byte: attribute type +// - n bytes: value (typically between 1 and 4 bytes, or bigger for strings) +// returns number of bytes of attribute, or <0 if error +int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, uint16_t attr, uint8_t attrtype) { + uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes + + uint32_t u32 = val.as(); + int32_t i32 = val.as(); + float f32 = val.as(); + + buf.add16(attr); // prepend with attribute identifier + buf.add8(attrtype); // prepend with attribute type + + switch (attrtype) { + // unsigned 8 + case Zbool: // bool + case Zuint8: // uint8 + case Zenum8: // enum8 + case Zdata8: // data8 + case Zmap8: // map8 + buf.add8(u32); + break; + // unsigned 16 + case Zuint16: // uint16 + case Zenum16: // enum16 + case Zdata16: // data16 + case Zmap16: // map16 + buf.add16(u32); + break; + // unisgned 32 + case Zuint32: // uint32 + case Zdata32: // data32 + case Zmap32: // map32 + buf.add32(u32); + break; + + // signed 8 + case Zint8: // int8 + buf.add8(i32); + break; + case Zint16: // int16 + buf.add16(i32); + break; + case Zint32: // int32 + buf.add32(i32); + break; + + case Zsingle: // float + uint32_t *f_ptr; + buf.add32( *((uint32_t*)&f32) ); // cast float as uint32_t + break; + + case Zstring: + case Zstring16: + { + const char * val_str = val.as(); + if (nullptr == val_str) { return -2; } + size_t val_len = strlen(val_str); + if (val_len > 32) { val_len = 32; } + len = val_len + 1; + buf.add8(val_len); + if (Zstring16 == attrtype) { + buf.add8(0); // len is on 2 bytes + len++; + } + for (uint32_t i = 0; i < val_len; i++) { + buf.add8(val_str[i]); + } + } + break; + + default: + // remove the attribute type we just added + buf.setLen(buf.len() - 3); + return -1; + } + return len + 3; +} + +uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, + uint32_t offset, uint32_t buflen) { + + uint32_t i = offset; + uint32_t attrtype = buf.get8(i++); + + // fallback - enter a null value + json[attrid_str] = (char*) nullptr; + + uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes + + // now parse accordingly to attr type + switch (attrtype) { + // case Znodata: // nodata + // case Zunk: // unk + // break; + case Zbool: // bool + case Zuint8: // uint8 + case Zenum8: // enum8 + { + uint8_t uint8_val = buf.get8(i); + // i += 1; + if (0xFF != uint8_val) { + json[attrid_str] = uint8_val; + } + } + break; + case Zuint16: // uint16 + case Zenum16: // enum16 + { + uint16_t uint16_val = buf.get16(i); + // i += 2; + if (0xFFFF != uint16_val) { + json[attrid_str] = uint16_val; + } + } + break; + case Zuint32: // uint32 + { + uint32_t uint32_val = buf.get32(i); + // i += 4; + if (0xFFFFFFFF != uint32_val) { + json[attrid_str] = uint32_val; + } + } + break; + // Note: uint40, uint48, uint56, uint64 are displayed as Hex + // Note: int40, int48, int56, int64 are displayed as Hex + case Zuint40: // uint40 + case Zuint48: // uint48 + case Zuint56: // uint56 + case Zuint64: // uint64 + case Zint40: // int40 + case Zint48: // int48 + case Zint56: // int56 + case Zint64: // int64 + { + // uint8_t len = attrtype - 0x27; // 5 - 8 + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + // i += len; + } + break; + case Zint8: // int8 + { + int8_t int8_val = buf.get8(i); + // i += 1; + if (0x80 != int8_val) { + json[attrid_str] = int8_val; + } + } + break; + case Zint16: // int16 + { + int16_t int16_val = buf.get16(i); + // i += 2; + if (0x8000 != int16_val) { + json[attrid_str] = int16_val; + } + } + break; + case Zint32: // int32 + { + int32_t int32_val = buf.get32(i); + // i += 4; + if (0x80000000 != int32_val) { + json[attrid_str] = int32_val; + } + } + break; + + case Zoctstr: // octet string, 1 byte len + case Zstring: // char string, 1 byte len + case Zoctstr16: // octet string, 2 bytes len + case Zstring16: // char string, 2 bytes len + // For strings, default is to try to do a real string, but reverts to octet stream if null char is present or on some exceptions + { + bool parse_as_string = true; + len = (attrtype <= 0x42) ? buf.get8(i) : buf.get16(i); // len is 8 or 16 bits + i += (attrtype <= 0x42) ? 1 : 2; // increment pointer + if (i + len > buf.len()) { // make sure we don't get past the buffer + len = buf.len() - i; + } + + // check if we can safely use a string + if ((0x41 == attrtype) || (0x43 == attrtype)) { parse_as_string = false; } + + if (parse_as_string) { + char str[len+1]; + strncpy(str, buf.charptr(i), len); + str[len] = 0x00; + json[attrid_str] = str; + } else { + // print as HEX + char hex[2*len+1]; + ToHex_P(buf.buf(i), len, hex, sizeof(hex)); + json[attrid_str] = hex; + } + + // i += len; + // break; + } + // i += buf.get8(i) + 1; + break; + + case Zdata8: // data8 + case Zmap8: // map8 + { + uint8_t uint8_val = buf.get8(i); + // i += 1; + json[attrid_str] = uint8_val; + } + break; + case Zdata16: // data16 + case Zmap16: // map16 + { + uint16_t uint16_val = buf.get16(i); + // i += 2; + json[attrid_str] = uint16_val; + } + break; + case Zdata32: // data32 + case Zmap32: // map32 + { + uint32_t uint32_val = buf.get32(i); + // i += 4; + json[attrid_str] = uint32_val; + } + break; + + case Zsingle: // float + { + uint32_t uint32_val = buf.get32(i); + float * float_val = (float*) &uint32_val; + // i += 4; + json[attrid_str] = *float_val; + } + break; + + // TODO + case ZToD: // ToD + case Zdate: // date + case ZUTC: // UTC + case ZclusterId: // clusterId + case ZattribId: // attribId + case ZbacOID: // bacOID + case ZEUI64: // EUI64 + case Zkey128: // key128 + case Zsemi: // semi (float on 2 bytes) + break; + + // Other un-implemented data types + case Zdata24: // data24 + case Zdata40: // data40 + case Zdata48: // data48 + case Zdata56: // data56 + case Zdata64: // data64 + break; + // map + case Zmap24: // map24 + case Zmap40: // map40 + case Zmap48: // map48 + case Zmap56: // map56 + case Zmap64: // map64 + break; + case Zdouble: // double precision + { + uint64_t uint64_val = buf.get64(i); + double * double_val = (double*) &uint64_val; + // i += 8; + json[attrid_str] = *double_val; + } + break; + } + i += len; + + // String pp; // pretty print + // json[attrid_str].prettyPrintTo(pp); + // // now store the attribute + // AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_ZIGBEE "ZCL attribute decoded, id %s, type 0x%02X, val=%s"), + // attrid_str, attrtype, pp.c_str()); + return i - offset; // how much have we increased the index +} + +// Generate an attribute name based on cluster number, attribute, and suffix if duplicates +void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len) { + uint32_t suffix = 1; + + snprintf_P(key, key_len, PSTR("%04X/%04X"), cluster, attr); + while (json.containsKey(key)) { + suffix++; + snprintf_P(key, key_len, PSTR("%04X/%04X+%d"), cluster, attr, suffix); // add "0008/0001+2" suffix if duplicate + } +} + +// First pass, parse all attributes in their native format +void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + while (len >= i + 3) { + uint16_t attrid = _payload.get16(i); + i += 2; + + char key[16]; + generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); + + // exception for Xiaomi lumi.weather - specific field to be treated as octet and not char + if ((0x0000 == _cluster_id) && (0xFF01 == attrid)) { + if (0x42 == _payload.get8(i)) { + _payload.set8(i, 0x41); // change type from 0x42 to 0x41 + } + } + i += parseSingleAttribute(json, key, _payload, i, len); + } +} + +// ZCL_READ_ATTRIBUTES +// TODO +void ZCLFrame::parseReadAttributes(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + json[F(D_CMND_ZIGBEE_CLUSTER)] = _cluster_id; + + JsonArray &attr_list = json.createNestedArray(F("Read")); + JsonObject &attr_names = json.createNestedObject(F("ReadNames")); + while (len - i >= 2) { + uint16_t attrid = _payload.get16(i); + attr_list.add(attrid); + + // find the attribute name + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); + uint16_t conv_attribute = pgm_read_word(&converter->attribute); + + if ((conv_cluster == _cluster_id) && (conv_attribute == attrid)) { + attr_names[(const __FlashStringHelper*) converter->name] = true; + break; + } + } + i += 2; + } +} + +// ZCL_READ_ATTRIBUTES_RESPONSE +void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { + uint32_t i = offset; + uint32_t len = _payload.len(); + + while (len >= 4 + i) { + uint16_t attrid = _payload.get16(i); + i += 2; + uint8_t status = _payload.get8(i++); + + if (0 == status) { + char key[16]; + generateAttributeName(json, _cluster_id, attrid, key, sizeof(key)); + + i += parseSingleAttribute(json, key, _payload, i, len); + } + } +} + +// ZCL_DEFAULT_RESPONSE +void ZCLFrame::parseResponse(void) { + if (_payload.len() < 2) { return; } // wrong format + uint8_t cmd = _payload.get8(0); + uint8_t status = _payload.get8(1); + + DynamicJsonBuffer jsonBuffer; + JsonObject& json = jsonBuffer.createObject(); + + // "Device" + char s[12]; + snprintf_P(s, sizeof(s), PSTR("0x%04X"), _srcaddr); + json[F(D_JSON_ZIGBEE_DEVICE)] = s; + // "Name" + const char * friendlyName = zigbee_devices.getFriendlyName(_srcaddr); + if (friendlyName) { + json[F(D_JSON_ZIGBEE_NAME)] = (char*) friendlyName; + } + // "Command" + snprintf_P(s, sizeof(s), PSTR("%04X!%02X"), _cluster_id, cmd); + json[F(D_JSON_ZIGBEE_CMD)] = s; + // "Status" + json[F(D_JSON_ZIGBEE_STATUS)] = status; + // "StatusMessage" + json[F(D_JSON_ZIGBEE_STATUS_MSG)] = getZigbeeStatusMessage(status); + // Add Endpoint + json[F(D_CMND_ZIGBEE_ENDPOINT)] = _srcendpoint; + // Add Group if non-zero + if (_groupaddr) { + json[F(D_CMND_ZIGBEE_GROUP)] = _groupaddr; + } + // Add linkquality + json[F(D_CMND_ZIGBEE_LINKQUALITY)] = _linkquality; + + String msg(""); + msg.reserve(100); + json.printTo(msg); + Response_P(PSTR("{\"" D_JSON_ZIGBEE_RESPONSE "\":%s}"), msg.c_str()); + MqttPublishPrefixTopic_P(RESULT_OR_TELE, PSTR(D_JSON_ZIGBEEZCL_RECEIVED)); + XdrvRulesProcess(); +} + + +// Parse non-normalized attributes +void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { + convertClusterSpecific(json, _cluster_id, _cmd_id, _frame_control.b.direction, _payload); + sendHueUpdate(_srcaddr, _groupaddr, _cluster_id, _cmd_id, _frame_control.b.direction); +} + // ====================================================================== // Record Manuf int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { diff --git a/tasmota/xdrv_23_zigbee_7_statemachine.ino b/tasmota/xdrv_23_zigbee_7_statemachine.ino index 590c795ac..4297a4375 100644 --- a/tasmota/xdrv_23_zigbee_7_statemachine.ino +++ b/tasmota/xdrv_23_zigbee_7_statemachine.ino @@ -106,10 +106,11 @@ enum Zigbee_StateMachine_Instruction_Set { // Labels used in the State Machine -- internal only const uint8_t ZIGBEE_LABEL_INIT_COORD = 10; // Start ZNP as coordinator const uint8_t ZIGBEE_LABEL_START_COORD = 11; // Start ZNP as coordinator -const uint8_t ZIGBEE_LABEL_INIT_ROUTER = 12; // Start ZNP as router +const uint8_t ZIGBEE_LABEL_INIT_ROUTER = 12; // Init ZNP as router const uint8_t ZIGBEE_LABEL_START_ROUTER = 13; // Start ZNP as router -const uint8_t ZIGBEE_LABEL_INIT_DEVICE = 14; // Start ZNP as end-device -// const uint8_t ZIGBEE_LABEL_START_DEVICE = 15; // Start ZNP as end-device - same as ZIGBEE_LABEL_START_ROUTER +const uint8_t ZIGBEE_LABEL_INIT_DEVICE = 14; // Init ZNP as end-device +const uint8_t ZIGBEE_LABEL_START_DEVICE = 15; // Start ZNP as end-device +const uint8_t ZIGBEE_LABEL_START_ROUTER_DEVICE = 16; // Start common to router and device const uint8_t ZIGBEE_LABEL_FACT_RESET_ROUTER_DEVICE_POST = 19; // common post configuration for router and device const uint8_t ZIGBEE_LABEL_READY = 20; // goto label 20 for main loop const uint8_t ZIGBEE_LABEL_MAIN_LOOP = 21; // main loop @@ -400,7 +401,7 @@ void Z_UpdateConfig(uint8_t zb_channel, uint16_t zb_pan_id, uint64_t zb_ext_pani const char kCheckingDeviceConfiguration[] PROGMEM = D_LOG_ZIGBEE "checking device configuration"; const char kConfiguredCoord[] PROGMEM = "Configured, starting coordinator"; const char kConfiguredRouter[] PROGMEM = "Configured, starting router"; -const char kConfiguredDevice[] PROGMEM = "Configured, starting end-device"; +const char kConfiguredDevice[] PROGMEM = "Configured, starting device"; const char kStarted[] PROGMEM = "Started"; const char kZigbeeStarted[] PROGMEM = D_LOG_ZIGBEE "Zigbee started"; const char kResetting[] PROGMEM = "Resetting configuration"; @@ -426,7 +427,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_WAIT_RECV_FUNC(2000, ZBR_VERSION, &Z_ReceiveCheckVersion) // Check if version is valid // Dispatching whether coordinator, router or end-device - ZI_CALL(&Z_SwitchDeviceType, 0) // goto ZIGBEE_LABEL_START_ROUTER, ZIGBEE_LABEL_START_DEVICE or continue if coordinator + ZI_CALL(&Z_SwitchDeviceType, 0) // goto ZIGBEE_LABEL_INIT_ROUTER, ZIGBEE_LABEL_INIT_DEVICE or continue if coordinator // ====================================================================== // Start as Zigbee Coordinator @@ -537,8 +538,9 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_LOGTYPE) // check the logical type ZI_WAIT_RECV(1000, ZBS_LOGTYPE_ROUTER) // it should be coordinator - ZI_LABEL(ZIGBEE_LABEL_START_ROUTER) // Init as a router + // ZI_LABEL(ZIGBEE_LABEL_START_ROUTER) // Init as a router ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredRouter) + ZI_LABEL(ZIGBEE_LABEL_START_ROUTER_DEVICE) ZI_ON_ERROR_GOTO(ZIGBEE_LABEL_ABORT) ZI_SEND(ZBS_AF_REGISTER_ALL) // Z_AF register for endpoint 01, profile 0x0104 Home Automation ZI_WAIT_RECV(1000, ZBR_AF_REGISTER) @@ -570,7 +572,7 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_WNV_ZNPHC) // Write NV ZNP Has Configured ZI_WAIT_RECV(1000, ZBR_WNV_OK) - ZI_GOTO(ZIGBEE_LABEL_START_ROUTER) + ZI_GOTO(ZIGBEE_LABEL_START_ROUTER_DEVICE) // ====================================================================== // Start as Zigbee Device @@ -583,7 +585,8 @@ static const Zigbee_Instruction zb_prog[] PROGMEM = { ZI_SEND(ZBS_LOGTYPE) // check the logical type ZI_WAIT_RECV(1000, ZBS_LOGTYPE_DEVICE) // it should be coordinator - ZI_GOTO(ZIGBEE_LABEL_START_ROUTER) + ZI_MQTT_STATE(ZIGBEE_STATUS_STARTING, kConfiguredDevice) + ZI_GOTO(ZIGBEE_LABEL_START_ROUTER_DEVICE) ZI_LABEL(ZIGBEE_LABEL_FACT_RESET_DEVICE) // Factory reset for router ZI_MQTT_STATE(ZIGBEE_STATUS_RESET_CONF, kResetting) diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index ffe108c5a..0b1144eb5 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -653,13 +653,16 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { JsonObject& json = jsonBuffer.createObject(); if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_DEFAULT_RESPONSE == zcl_received.getCmdId())) { - zcl_received.parseResponse(); + zcl_received.parseResponse(); // Zigbee general "Degault Response", publish ZbResponse message } else { // Build the ZbReceive json if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_REPORT_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseRawAttributes(json); + zcl_received.parseRawAttributes(json); // Zigbee report attributes from sensors if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES_RESPONSE == zcl_received.getCmdId())) { + zcl_received.parseReadAttributesResponse(json); + if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages + } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) { zcl_received.parseReadAttributes(json); if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if (zcl_received.isClusterSpecificCommand()) { diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index 5c5181b6d..38627f91f 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -32,7 +32,7 @@ TasmotaSerial *ZigbeeSerial = nullptr; const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix D_CMND_ZIGBEEZNPSEND "|" D_CMND_ZIGBEE_PERMITJOIN "|" D_CMND_ZIGBEE_STATUS "|" D_CMND_ZIGBEE_RESET "|" D_CMND_ZIGBEE_SEND "|" - D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEE_READ "|" D_CMND_ZIGBEEZNPRECEIVE "|" + D_CMND_ZIGBEE_PROBE "|" D_CMND_ZIGBEEZNPRECEIVE "|" D_CMND_ZIGBEE_FORGET "|" D_CMND_ZIGBEE_SAVE "|" D_CMND_ZIGBEE_NAME "|" D_CMND_ZIGBEE_BIND "|" D_CMND_ZIGBEE_UNBIND "|" D_CMND_ZIGBEE_PING "|" D_CMND_ZIGBEE_MODELID "|" D_CMND_ZIGBEE_LIGHT "|" D_CMND_ZIGBEE_RESTORE "|" D_CMND_ZIGBEE_BIND_STATE "|" @@ -42,7 +42,7 @@ const char kZbCommands[] PROGMEM = D_PRFX_ZB "|" // prefix void (* const ZigbeeCommand[])(void) PROGMEM = { &CmndZbZNPSend, &CmndZbPermitJoin, &CmndZbStatus, &CmndZbReset, &CmndZbSend, - &CmndZbProbe, &CmndZbRead, &CmndZbZNPReceive, + &CmndZbProbe, &CmndZbZNPReceive, &CmndZbForget, &CmndZbSave, &CmndZbName, &CmndZbBind, &CmndZbUnbind, &CmndZbPing, &CmndZbModelId, &CmndZbLight, &CmndZbRestore, &CmndZbBindState, @@ -393,9 +393,301 @@ void zigbeeZCLSendStr(uint16_t shortaddr, uint16_t groupaddr, uint8_t endpoint, } } +// Parse "Report" or "Write" attribute +void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, bool write) { + SBuffer buf(200); // buffer to store the binary output of attibutes + + const JsonObject &attrs = val_pubwrite.as(); + // iterate on keys + for (JsonObject::const_iterator it=attrs.begin(); it!=attrs.end(); ++it) { + const char *key = it->key; + const JsonVariant &value = it->value; + + uint16_t attr_id = 0xFFFF; + uint16_t cluster_id = 0xFFFF; + uint8_t type_id = Znodata; + + // check if the name has the format "XXXX/YYYY" where XXXX is the cluster, YYYY the attribute id + // alternative "XXXX/YYYY%ZZ" where ZZ is the type (for unregistered attributes) + char * delimiter = strchr(key, '/'); + char * delimiter2 = strchr(key, '%'); + if (delimiter) { + cluster_id = strtoul(key, &delimiter, 16); + if (!delimiter2) { + attr_id = strtoul(delimiter+1, nullptr, 16); + } else { + attr_id = strtoul(delimiter+1, &delimiter2, 16); + type_id = strtoul(delimiter2+1, nullptr, 16); + } + } + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X"), cluster_id, attr_id); + + // do we already know the type, i.e. attribute and cluster are also known + if (Znodata == type_id) { + // scan attributes to find by name, and retrieve type + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + bool match = false; + uint16_t local_attr_id = pgm_read_word(&converter->attribute); + uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); + uint8_t local_type_id = pgm_read_byte(&converter->type); + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id); + + if (delimiter) { + if ((cluster_id == local_cluster_id) && (attr_id == local_attr_id)) { + type_id = local_type_id; + break; + } + } else if (converter->name) { + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Comparing '%s' with '%s'"), attr_name, converter->name); + if (0 == strcasecmp_P(key, converter->name)) { + // match + cluster_id = local_cluster_id; + attr_id = local_attr_id; + type_id = local_type_id; + break; + } + } + } + } + + // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X, type_id = 0x%02X"), cluster_id, attr_id, type_id); + if ((0xFFFF == attr_id) || (0xFFFF == cluster_id)) { + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR("Unknown attribute "), key); + return; + } + if (Znodata == type_id) { + Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR("Unknown attribute type for attribute "), key); + return; + } + + if (0xFFFF == cluster) { + cluster = cluster_id; // set the cluster for this packet + } else if (cluster != cluster_id) { + ResponseCmndChar_P(PSTR("No more than one cluster id per command")); + return; + } + // push the value in the buffer + int32_t res = encodeSingleAttribute(buf, value, attr_id, type_id); + if (res < 0) { + Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); + return; + } + } + + // did we have any attribute? + if (0 == buf.len()) { + ResponseCmndChar_P(PSTR("No attribute in list")); + return; + } + + // all good, send the packet + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, write ? ZCL_WRITE_ATTRIBUTES : ZCL_REPORT_ATTRIBUTES, false /* not cluster specific */, manuf, buf.getBuffer(), buf.len(), false /* noresponse */, zigbee_devices.getNextSeqNumber(device)); + ResponseCmndDone(); +} + +// Parse the "Send" attribute and send the command +void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { + uint8_t cmd = 0; + String cmd_str = ""; // the actual low-level command, either specified or computed + const char *cmd_s; // pointer to payload string + bool clusterSpecific = true; + + static char delim[] = ", "; // delimiters for parameters + // probe the type of the argument + // If JSON object, it's high level commands + // If String, it's a low level command + if (val_cmd.is()) { + // we have a high-level command + const JsonObject &cmd_obj = val_cmd.as(); + int32_t cmd_size = cmd_obj.size(); + if (cmd_size > 1) { + Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); + return; + } else if (1 == cmd_size) { + // We have exactly 1 command, parse it + JsonObject::const_iterator it = cmd_obj.begin(); // just get the first key/value + String key = it->key; + const JsonVariant& value = it->value; + uint32_t x = 0, y = 0, z = 0; + uint16_t cmd_var; + + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); + if (tasmota_cmd) { + cmd_str = tasmota_cmd; + } else { + Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); + return; + } + + // parse the JSON value, depending on its type fill in x,y,z + if (value.is()) { + x = value.as() ? 1 : 0; + } else if (value.is()) { + x = value.as(); + } else { + // if non-bool or non-int, trying char* + const char *s_const = value.as(); + if (s_const != nullptr) { + char s[strlen(s_const)+1]; + strcpy(s, s_const); + if ((nullptr != s) && (0x00 != *s)) { // ignore any null or empty string, could represent 'null' json value + char *sval = strtok(s, delim); + if (sval) { + x = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + y = ZigbeeAliasOrNumber(sval); + sval = strtok(nullptr, delim); + if (sval) { + z = ZigbeeAliasOrNumber(sval); + } + } + } + } + } + } + + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_template = %s"), cmd_str.c_str()); + if (0xFF == cmd_var) { // if command number is a variable, replace it with x + cmd = x; + x = y; // and shift other variables + y = z; + } else { + cmd = cmd_var; // or simply copy the cmd number + } + cmd_str = zigbeeCmdAddParams(cmd_str.c_str(), x, y, z); // fill in parameters + //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_final = %s"), cmd_str.c_str()); + cmd_s = cmd_str.c_str(); + } else { + // we have zero command, pass through until last error for missing command + } + } else if (val_cmd.is()) { + // low-level command + cmd_str = val_cmd.as(); + // Now parse the string to extract cluster, command, and payload + // Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC" + // where AA is the cluster number, BBBB the command number, CCCC... the payload + // First delimiter is '_' for a global command, or '!' for a cluster specific command + const char * data = cmd_str.c_str(); + cluster = parseHex(&data, 4); + + // delimiter + if (('_' == *data) || ('!' == *data)) { + if ('_' == *data) { clusterSpecific = false; } + data++; + } else { + ResponseCmndChar_P(PSTR("Wrong delimiter for payload")); + return; + } + // parse cmd number + cmd = parseHex(&data, 2); + + // move to end of payload + // delimiter is optional + if ('/' == *data) { data++; } // skip delimiter + + cmd_s = data; + } else { + // we have an unsupported command type, just ignore it and fallback to missing command + } + + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeZCLSend device: 0x%04X, group: 0x%04X, endpoint:%d, cluster:0x%04X, cmd:0x%02X, send:\"%s\""), + device, groupaddr, endpoint, cluster, cmd, cmd_s); + zigbeeZCLSendStr(device, groupaddr, endpoint, clusterSpecific, manuf, cluster, cmd, cmd_s); + ResponseCmndDone(); +} + + +// Parse the "Send" attribute and send the command +void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf) { + // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":5} + // ZbSend {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Read":"0x0005"} + // ZbSend {"Device":"0xF289","Cluster":0,"Endpoint":3,"Read":[5,6,7,4]} + // ZbSend {"Device":"0xF289","Endpoint":3,"Read":{"ModelId":true}} + // ZbSend {"Device":"0xF289","Read":{"ModelId":true}} + + // params + size_t attrs_len = 0; + uint8_t* attrs = nullptr; // empty string is valid + + uint16_t val = strToUInt(val_attr); + if (val_attr.is()) { + const JsonArray& attr_arr = val_attr.as(); + attrs_len = attr_arr.size() * 2; + attrs = new uint8_t[attrs_len]; + + uint32_t i = 0; + for (auto value : attr_arr) { + uint16_t val = strToUInt(value); + attrs[i++] = val & 0xFF; + attrs[i++] = val >> 8; + } + } else if (val_attr.is()) { + const JsonObject& attr_obj = val_attr.as(); + attrs_len = attr_obj.size() * 2; + attrs = new uint8_t[attrs_len]; + uint32_t actual_attr_len = 0; + + // iterate on keys + for (JsonObject::const_iterator it=attr_obj.begin(); it!=attr_obj.end(); ++it) { + const char *key = it->key; + // const JsonVariant &value = it->value; // we don't need the value here, only keys are relevant + + bool found = false; + // scan attributes to find by name, and retrieve type + for (uint32_t i = 0; i < ARRAY_SIZE(Z_PostProcess); i++) { + const Z_AttributeConverter *converter = &Z_PostProcess[i]; + bool match = false; + uint16_t local_attr_id = pgm_read_word(&converter->attribute); + uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); + // uint8_t local_type_id = pgm_read_byte(&converter->type); + + if ((converter->name) && (0 == strcasecmp_P(key, converter->name))) { + // match name + // check if there is a conflict with cluster + // TODO + attrs[actual_attr_len++] = local_attr_id & 0xFF; + attrs[actual_attr_len++] = local_attr_id >> 8; + found = true; + break; // found, exit loop + } + } + if (!found) { + AddLog_P2(LOG_LEVEL_INFO, PSTR("ZIG: Unknown attribute name (ignored): %s"), key); + } + } + + attrs_len = actual_attr_len; + } else { + attrs_len = 2; + attrs = new uint8_t[attrs_len]; + attrs[0] = val & 0xFF; // little endian + attrs[1] = val >> 8; + } + + if (attrs_len > 0) { + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, manuf, attrs, attrs_len, true /* we do want a response */, zigbee_devices.getNextSeqNumber(device)); + ResponseCmndDone(); + } else { + ResponseCmndChar_P(PSTR("Missing parameters")); + } + + if (attrs) { delete[] attrs; } +} + // // Command `ZbSend` // +// Examples: +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"0006/0000":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"Power":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AqaraRotate":0}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AqaraRotate":12.5}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"006/0000%39":12.5}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"AnalogInApplicationType":1000000}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"TimeZone":-1000000}} +// ZbSend {"Device":"0x0000","Endpoint":1,"Write":{"Manufacturer":"Tasmota","ModelId":"Tasmota Z2T Router"}} void CmndZbSend(void) { // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":1} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"3"} } @@ -405,7 +697,7 @@ void CmndZbSend(void) { // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":true} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":"true"} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"ShutterClose":null} } - // ZbSend { "devicse":"0x1234", "endpoint":"0x03", "send":{"Power":1} } + // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Power":1} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"1,2"} } // ZbSend { "device":"0x1234", "endpoint":"0x03", "send":{"Color":"0x1122,0xFFEE"} } if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } @@ -414,26 +706,21 @@ void CmndZbSend(void) { if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params - static char delim[] = ", "; // delimiters for parameters - uint16_t device = BAD_SHORTADDR; // 0x0000 is local, so considered invalid - uint16_t groupaddr = 0x0000; // group address - uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint - uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - // Command elements - uint16_t cluster = 0; - uint8_t cmd = 0; - String cmd_str = ""; // the actual low-level command, either specified or computed - const char *cmd_s; // pointer to payload string - bool clusterSpecific = true; + uint16_t device = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid + uint16_t groupaddr = 0x0000; // group address valid only if device == BAD_SHORTADDR + uint16_t cluster = 0xFFFF; // no default + uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint + uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - // parse JSON - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); + + // parse "Device" and "Group" + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); if (nullptr != &val_device) { device = zigbee_devices.parseDeviceParam(val_device.as()); if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } } if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); + const JsonVariant &val_group = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_GROUP)); if (nullptr != &val_group) { groupaddr = strToUInt(val_group); } else { // no device nor group @@ -442,116 +729,54 @@ void CmndZbSend(void) { } } - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); + // read other parameters + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); + if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); + const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_MANUF)); if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR("Send")); + + // infer endpoint + if (BAD_SHORTADDR == device) { + endpoint = 0xFF; // endpoint not used for group addresses + } else if (0 == endpoint) { + endpoint = zigbee_devices.findFirstEndpoint(device); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: guessing endpoint %d"), endpoint); + } + + const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_SEND)); + const JsonVariant &val_read = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ)); + const JsonVariant &val_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE)); + const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); + uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish); + if (multi_cmd > 1) { + ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write' or 'Report'")); + return; + } + if (nullptr != &val_cmd) { - // probe the type of the argument - // If JSON object, it's high level commands - // If String, it's a low level command - if (val_cmd.is()) { - // we have a high-level command - const JsonObject &cmd_obj = val_cmd.as(); - int32_t cmd_size = cmd_obj.size(); - if (cmd_size > 1) { - Response_P(PSTR("Only 1 command allowed (%d)"), cmd_size); - return; - } else if (1 == cmd_size) { - // We have exactly 1 command, parse it - JsonObject::const_iterator it = cmd_obj.begin(); // just get the first key/value - String key = it->key; - const JsonVariant& value = it->value; - uint32_t x = 0, y = 0, z = 0; - uint16_t cmd_var; - - const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); - if (tasmota_cmd) { - cmd_str = tasmota_cmd; - } else { - Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); - return; - } - - // parse the JSON value, depending on its type fill in x,y,z - if (value.is()) { - x = value.as() ? 1 : 0; - } else if (value.is()) { - x = value.as(); - } else { - // if non-bool or non-int, trying char* - const char *s_const = value.as(); - if (s_const != nullptr) { - char s[strlen(s_const)+1]; - strcpy(s, s_const); - if ((nullptr != s) && (0x00 != *s)) { // ignore any null or empty string, could represent 'null' json value - char *sval = strtok(s, delim); - if (sval) { - x = ZigbeeAliasOrNumber(sval); - sval = strtok(nullptr, delim); - if (sval) { - y = ZigbeeAliasOrNumber(sval); - sval = strtok(nullptr, delim); - if (sval) { - z = ZigbeeAliasOrNumber(sval); - } - } - } - } - } - } - - //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_template = %s"), cmd_str.c_str()); - if (0xFF == cmd_var) { // if command number is a variable, replace it with x - cmd = x; - x = y; // and shift other variables - y = z; - } else { - cmd = cmd_var; // or simply copy the cmd number - } - cmd_str = zigbeeCmdAddParams(cmd_str.c_str(), x, y, z); // fill in parameters - //AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbSend: command_final = %s"), cmd_str.c_str()); - cmd_s = cmd_str.c_str(); - } else { - // we have zero command, pass through until last error for missing command - } - } else if (val_cmd.is()) { - // low-level command - cmd_str = val_cmd.as(); - // Now parse the string to extract cluster, command, and payload - // Parse 'cmd' in the form "AAAA_BB/CCCCCCCC" or "AAAA!BB/CCCCCCCC" - // where AA is the cluster number, BBBB the command number, CCCC... the payload - // First delimiter is '_' for a global command, or '!' for a cluster specific command - const char * data = cmd_str.c_str(); - cluster = parseHex(&data, 4); - - // delimiter - if (('_' == *data) || ('!' == *data)) { - if ('_' == *data) { clusterSpecific = false; } - data++; - } else { - ResponseCmndChar_P(PSTR("Wrong delimiter for payload")); - return; - } - // parse cmd number - cmd = parseHex(&data, 2); - - // move to end of payload - // delimiter is optional - if ('/' == *data) { data++; } // skip delimiter - - cmd_s = data; - } else { - // we have an unsupported command type, just ignore it and fallback to missing command + // "Send":{...commands...} + ZbSendSend(val_cmd, device, groupaddr, cluster, endpoint, manuf); + } else if (nullptr != &val_read) { + // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] + ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf); + } else if (nullptr != &val_write) { + if ((0 == endpoint) || (!val_write.is())) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; } - - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZigbeeZCLSend device: 0x%04X, group: 0x%04X, endpoint:%d, cluster:0x%04X, cmd:0x%02X, send:\"%s\""), - device, groupaddr, endpoint, cluster, cmd, cmd_s); - zigbeeZCLSendStr(device, groupaddr, endpoint, clusterSpecific, manuf, cluster, cmd, cmd_s); - ResponseCmndDone(); + // "Write":{...attributes...} + ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, true /* write */); + } else if (nullptr != &val_publish) { + if ((0 == endpoint) || (!val_publish.is())) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; + } + // "Report":{...attributes...} + ZbSendReportWrite(val_publish, device, groupaddr, cluster, endpoint, manuf, false /* report */); } else { - Response_P(PSTR("Missing zigbee 'Send'")); + Response_P(PSTR("Missing zigbee 'Send', 'Write' or 'Report'")); return; } } @@ -570,7 +795,6 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } // params - // static char delim[] = ", "; // delimiters for parameters uint16_t srcDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint16_t dstDevice = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid uint64_t dstLongAddr = 0; @@ -582,7 +806,7 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind // Information about source device: "Device", "Endpoint", "Cluster" // - the source endpoint must have a known IEEE address - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); + const JsonVariant &val_device = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_DEVICE)); if (nullptr != &val_device) { srcDevice = zigbee_devices.parseDeviceParam(val_device.as()); } @@ -591,10 +815,10 @@ void ZbBindUnbind(bool unbind) { // false = bind, true = unbind uint64_t srcLongAddr = zigbee_devices.getDeviceLongAddr(srcDevice); if (0 == srcLongAddr) { ResponseCmndChar_P(PSTR("Unknown source IEEE address")); return; } // look for source endpoint - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); + const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_ENDPOINT)); if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } // look for source cluster - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); + const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } // Either Device address @@ -885,90 +1109,6 @@ void CmndZbRestore(void) { ResponseCmndDone(); } -// -// Command `ZbRead` -// Send an attribute read command to a device, specifying cluster and list of attributes -// -void CmndZbRead(void) { - // ZbRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":5} - // ZbRead {"Device":"0xF289","Cluster":"0x0000","Endpoint":"0x0003","Attr":"0x0005"} - // ZbRead {"Device":"0xF289","Cluster":0,"Endpoint":3,"Attr":[5,6,7,4]} - if (zigbee.init_phase) { ResponseCmndChar_P(PSTR(D_ZIGBEE_NOT_STARTED)); return; } - DynamicJsonBuffer jsonBuf; - JsonObject &json = jsonBuf.parseObject((const char*) XdrvMailbox.data); - if (!json.success()) { ResponseCmndChar_P(PSTR(D_JSON_INVALID_JSON)); return; } - - // params - uint16_t device = BAD_SHORTADDR; // BAD_SHORTADDR is broadcast, so considered invalid - uint16_t groupaddr = 0x0000; // if 0x0000 ignore group adress - uint16_t cluster = 0x0000; // default to general cluster - uint8_t endpoint = 0x00; // 0x00 is invalid for the dst endpoint - uint16_t manuf = 0x0000; // Manuf Id in ZCL frame - size_t attrs_len = 0; - uint8_t* attrs = nullptr; // empty string is valid - - const JsonVariant &val_device = GetCaseInsensitive(json, PSTR("Device")); - if (nullptr != &val_device) { - device = zigbee_devices.parseDeviceParam(val_device.as()); - if (BAD_SHORTADDR == device) { ResponseCmndChar_P(PSTR("Invalid parameter")); return; } - } - if (BAD_SHORTADDR == device) { // if not found, check if we have a group - const JsonVariant &val_group = GetCaseInsensitive(json, PSTR("Group")); - if (nullptr != &val_group) { - groupaddr = strToUInt(val_group); - } else { // no device nor group - ResponseCmndChar_P(PSTR("Unknown device")); - return; - } - } - - const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR("Cluster")); - if (nullptr != &val_cluster) { cluster = strToUInt(val_cluster); } - const JsonVariant &val_endpoint = GetCaseInsensitive(json, PSTR("Endpoint")); - if (nullptr != &val_endpoint) { endpoint = strToUInt(val_endpoint); } - const JsonVariant &val_manuf = GetCaseInsensitive(json, PSTR("Manuf")); - if (nullptr != &val_manuf) { manuf = strToUInt(val_manuf); } - - const JsonVariant &val_attr = GetCaseInsensitive(json, PSTR("Read")); - if (nullptr != &val_attr) { - uint16_t val = strToUInt(val_attr); - if (val_attr.is()) { - const JsonArray& attr_arr = val_attr.as(); - attrs_len = attr_arr.size() * 2; - attrs = new uint8_t[attrs_len]; - - uint32_t i = 0; - for (auto value : attr_arr) { - uint16_t val = strToUInt(value); - attrs[i++] = val & 0xFF; - attrs[i++] = val >> 8; - } - } else { - attrs_len = 2; - attrs = new uint8_t[attrs_len]; - attrs[0] = val & 0xFF; // little endian - attrs[1] = val >> 8; - } - } - - if ((0 == endpoint) && (device)) { // try to compute the endpoint - endpoint = zigbee_devices.findFirstEndpoint(device); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZbRead: guessing endpoint 0x%02X"), endpoint); - } - if (BAD_SHORTADDR == device) { - endpoint = 0xFF; // endpoint not used for group addresses - } - - if ((0 != endpoint) && (attrs_len > 0)) { - ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, ZCL_READ_ATTRIBUTES, false, manuf, attrs, attrs_len, true /* we do want a response */, zigbee_devices.getNextSeqNumber(device)); - ResponseCmndDone(); - } else { - ResponseCmndChar_P(PSTR("Missing parameters")); - } - - if (attrs) { delete[] attrs; } -} - // // Command `ZbPermitJoin` // Allow or Deny pairing of new Zigbee devices From ce3dfd1066de1ae6ee63276c41b619ce652148ff Mon Sep 17 00:00:00 2001 From: rando-calrissian <37273799+rando-calrissian@users.noreply.github.com> Date: Fri, 29 May 2020 15:41:49 -0700 Subject: [PATCH 11/48] Add files via upload Added ESP32 support for changing the displayed temperature unit on LYWSD02 BLE device --- tasmota/xsns_62_MI_ESP32.ino | 435 +++++++++++++++++++---------------- 1 file changed, 236 insertions(+), 199 deletions(-) diff --git a/tasmota/xsns_62_MI_ESP32.ino b/tasmota/xsns_62_MI_ESP32.ino index 1844edc2c..264ffbb52 100644 --- a/tasmota/xsns_62_MI_ESP32.ino +++ b/tasmota/xsns_62_MI_ESP32.ino @@ -229,18 +229,26 @@ class MI32SensorCallback : public NimBLEClientCallbacks { class MI32AdvCallbacks: public NimBLEAdvertisedDeviceCallbacks { void onResult(NimBLEAdvertisedDevice* advertisedDevice) { - // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice.getAddress().toString().c_str(),advertisedDevice.getServiceData().length()); - if (advertisedDevice->getServiceData().length() == 0) return; + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); + if (advertisedDevice->getServiceData().length() == 0) { + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); + MI32Scan->erase(advertisedDevice->getAddress()); + return; + } uint16_t uuid = advertisedDevice->getServiceDataUUID().getNative()->u16.value; - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%x"),uuid); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("UUID: %x"),uuid); uint8_t addr[6]; memcpy(addr,advertisedDevice->getAddress().getNative(),6); MI32_ReverseMAC(addr); if(uuid==0xfe95) { - MI32ParseResponse((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr); + MI32ParseResponse((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); } else if(uuid==0xfdcd) { - MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().c_str(),advertisedDevice->getServiceData().length(), addr); + MI32parseCGD1Packet((char*)advertisedDevice->getServiceData().data(),advertisedDevice->getServiceData().length(), addr); + } + else { + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("No Xiaomi Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData().length()); + MI32Scan->erase(advertisedDevice->getAddress()); } }; }; @@ -399,47 +407,57 @@ void MI32StartTask(uint32_t task){ } } -void MI32ConnectActiveSensor(){ // only use inside a task !! +bool MI32ConnectActiveSensor(){ // only use inside a task !! + MI32.mode.connected = 0; MI32Client = nullptr; - esp_bd_addr_t address; - memcpy(address,MIBLEsensors[MI32.state.sensor].serial,sizeof(address)); + Wifi.counter = Wifi.counter + 20; // hopefully less interference + NimBLEAddress _address = NimBLEAddress(MIBLEsensors[MI32.state.sensor].serial); if(NimBLEDevice::getClientListSize()) { - MI32Client = NimBLEDevice::getClientByPeerAddress(NimBLEAddress(address)); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: found any clients in the cList"),D_CMND_MI32); + MI32Client = NimBLEDevice::getClientByPeerAddress(_address); if(MI32Client){ - if(!MI32Client->connect(NimBLEAddress(address), 0,false)) { - MI32.mode.willConnect = 0; - vTaskDelete( NULL ); - } + // Should be impossible + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: got connected client"),D_CMND_MI32); } else { + // Should be the norm after the first iteration MI32Client = NimBLEDevice::getDisconnectedClient(); + DEBUG_SENSOR_LOG(PSTR("%s: got disconnected client"),D_CMND_MI32); } } + + if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) { + MI32.mode.willConnect = 0; + DEBUG_SENSOR_LOG(PSTR("%s: max connection already reached"),D_CMND_MI32); + return false; + } if(!MI32Client) { - if(NimBLEDevice::getClientListSize() >= NIMBLE_MAX_CONNECTIONS) { - MI32.mode.willConnect = 0; - vTaskDelete( NULL ); - } + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: will create client"),D_CMND_MI32); MI32Client = NimBLEDevice::createClient(); MI32Client->setClientCallbacks(&MI32SensorCB , false); - MI32Client->setConnectionParams(12,12,0,51); - MI32Client->setConnectTimeout(10); - if (!MI32Client->connect(NimBLEAddress(address),0,false)) { - MI32.mode.willConnect = 0; - NimBLEDevice::deleteClient(MI32Client); - vTaskDelete( NULL ); - } + MI32Client->setConnectionParams(12,12,0,48); + MI32Client->setConnectTimeout(30); } + if (!MI32Client->connect(_address,false)) { + MI32.mode.willConnect = 0; + NimBLEDevice::deleteClient(MI32Client); + DEBUG_SENSOR_LOG(PSTR("%s: did not connect client"),D_CMND_MI32); + return false; + } + DEBUG_SENSOR_LOG(PSTR("%s: did create new client"),D_CMND_MI32); + return true; + // } } void MI32StartScanTask(){ if (MI32.mode.connected) return; MI32.mode.runningScan = 1; + // Wifi.counter = Wifi.counter + 3; xTaskCreatePinnedToCore( MI32ScanTask, /* Function to implement the task */ "MI32ScanTask", /* Name of the task */ - 4096, /* Stack size in words */ + 8192, /* Stack size in words */ NULL, /* Task input parameter */ 0, /* Priority of the task */ NULL, /* Task handle. */ @@ -448,17 +466,18 @@ void MI32StartScanTask(){ } void MI32ScanTask(void *pvParameters){ - NimBLEScan* pScan = NimBLEDevice::getScan(); - pScan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks); - pScan->setActiveScan(false); - pScan->start(5, MI32scanEndedCB); // hard coded duration + if (MI32Scan == nullptr) MI32Scan = NimBLEDevice::getScan(); + DEBUG_SENSOR_LOG(PSTR("%s: Scan Cache Length: %u"),D_CMND_MI32, MI32Scan->getResults().getCount()); + MI32Scan->setAdvertisedDeviceCallbacks(&MI32ScanCallbacks); + MI32Scan->setActiveScan(false); + MI32Scan->start(5, MI32scanEndedCB, true); // hard coded duration uint32_t timer = 0; while (MI32.mode.runningScan){ if (timer>15){ vTaskDelete( NULL ); } timer++; - vTaskDelay(1000); + vTaskDelay(1000/ portTICK_PERIOD_MS); } vTaskDelete( NULL ); } @@ -482,47 +501,62 @@ void MI32SensorTask(void *pvParameters){ MI32.mode.willConnect = 0; vTaskDelete( NULL ); } - MI32ConnectActiveSensor(); - MI32.mode.readingDone = 1; - switch(MIBLEsensors[MI32.state.sensor].type){ - case LYWSD03MMC: - MI32.mode.readingDone = 0; - MI32connectLYWSD03(); - break; - default: - break; - } - uint32_t timer = 0; - while (!MI32.mode.readingDone){ - if (timer>150){ + if (MI32ConnectActiveSensor()){ + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + MI32Client->disconnect(); + NimBLEDevice::deleteClient(MI32Client); + MI32.mode.willConnect = 0; + vTaskDelay(100/ portTICK_PERIOD_MS); + vTaskDelete( NULL ); + } + timer++; + vTaskDelay(10/ portTICK_PERIOD_MS); + } + + timer = 150; + switch(MIBLEsensors[MI32.state.sensor].type){ + case LYWSD03MMC: + MI32.mode.readingDone = 0; + if(MI32connectLYWSD03forNotification()) timer=0; + break; + default: break; } - timer++; - vTaskDelay(100); + + while (!MI32.mode.readingDone){ + if (timer>150){ + break; + } + timer++; + vTaskDelay(100/ portTICK_PERIOD_MS); + } + MI32Client->disconnect(); + DEBUG_SENSOR_LOG(PSTR("%s: requested disconnect"),D_CMND_MI32); } - MI32Client->disconnect(); - NimBLEDevice::deleteClient(MI32Client); - vTaskDelay(500); + vTaskDelay(500/ portTICK_PERIOD_MS); MI32.mode.connected = 0; vTaskDelete( NULL ); } -void MI32connectLYWSD03(){ +bool MI32connectLYWSD03forNotification(){ NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6"); - static BLEUUID charUUID("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6"); + static BLEUUID serviceUUID(0xebe0ccb0,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6); + static BLEUUID charUUID(0xebe0ccc1,0x7a0a,0x4b0c,0x8a1a6ff2997da3a6); pSvc = MI32Client->getService(serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(charUUID); } - if(pChr->canNotify()) { - if(!pChr->registerForNotify(MI32notifyCB)) { - MI32.mode.willConnect = 0; - MI32Client->disconnect(); - return; + if (pChr){ + if(pChr->canNotify()) { + if(pChr->registerForNotify(MI32notifyCB)) { + return true; + } } } + return false; } void MI32StartTimeTask(){ @@ -544,45 +578,48 @@ void MI32TimeTask(void *pvParameters){ MI32.mode.shallSetTime = 0; vTaskDelete( NULL ); } - MI32ConnectActiveSensor(); - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ - break; + if(MI32ConnectActiveSensor()){ + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + break; + } + timer++; + vTaskDelay(10/ portTICK_PERIOD_MS); } - timer++; - vTaskDelay(10); - } - NimBLERemoteService* pSvc = nullptr; - NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); - static BLEUUID charUUID("EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6"); - pSvc = MI32Client->getService(serviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(charUUID); - } - if(pChr->canWrite()) { - union { - uint8_t buf[5]; - uint32_t time; - } _utc; - _utc.time = Rtc.utc_time; - _utc.buf[4] = Rtc.time_timezone / 60; + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + static BLEUUID serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + static BLEUUID charUUID(0xEBE0CCB7,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + pSvc = MI32Client->getService(serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(charUUID); - if(!pChr->writeValue(_utc.buf,sizeof(_utc.buf),true)) { // true is important ! - MI32.mode.willConnect = 0; - MI32Client->disconnect(); } - else { - MI32.mode.shallSetTime = 0; - MI32.mode.willSetTime = 0; + if (pChr){ + if(pChr->canWrite()) { + union { + uint8_t buf[5]; + uint32_t time; + } _utc; + _utc.time = Rtc.utc_time; + _utc.buf[4] = Rtc.time_timezone / 60; + + if(!pChr->writeValue(_utc.buf,sizeof(_utc.buf),true)) { // true is important ! + MI32.mode.willConnect = 0; + MI32Client->disconnect(); + } + else { + MI32.mode.shallSetTime = 0; + MI32.mode.willSetTime = 0; + } + } } + MI32Client->disconnect(); } - MI32Client->disconnect(); - NimBLEDevice::deleteClient(MI32Client); - vTaskDelay(500); + vTaskDelay(500/ portTICK_PERIOD_MS); MI32.mode.connected = 0; vTaskDelete( NULL ); } @@ -606,61 +643,58 @@ void MI32UnitTask(void *pvParameters){ MI32.mode.shallSetUnit = 0; vTaskDelete( NULL ); } - MI32ConnectActiveSensor(); - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ - break; + if(MI32ConnectActiveSensor()){ + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + break; + } + timer++; + vTaskDelay(10/ portTICK_PERIOD_MS); } - timer++; - vTaskDelay(10); + + NimBLERemoteService* pSvc = nullptr; + NimBLERemoteCharacteristic* pChr = nullptr; + static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID charUUID("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"); + pSvc = MI32Client->getService(serviceUUID); + if(pSvc) { + pChr = pSvc->getCharacteristic(charUUID); } - NimBLERemoteService* pSvc = nullptr; - NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); - static BLEUUID charUUID("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"); - pSvc = MI32Client->getService(serviceUUID); - if(pSvc) { - pChr = pSvc->getCharacteristic(charUUID); - } + if(pChr->canRead()){ + uint8_t curUnit; + const char *buf = pChr->readValue().c_str(); + if( buf[0] != 0 && buf[0]<101 ){ + curUnit = buf[0]; + } - uint8_t curUnit; + if(pChr->canWrite()) { + curUnit = curUnit == 0x01?0xFF:0x01; // C/F - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - if( buf[0] != 0 && buf[0]<101 ){ - curUnit = buf[0]; + if(!pChr->writeValue(&curUnit,sizeof(curUnit),true)) { // true is important ! + MI32.mode.willConnect = 0; + MI32Client->disconnect(); + } + else { + MI32.mode.shallSetUnit = 0; + MI32.mode.willSetUnit = 0; + } + } } + MI32Client->disconnect(); } - else { - return; - } - - if(pChr->canWrite()) { - curUnit = curUnit == 0x01?0xFF:0x01; // C/F - - if(!pChr->writeValue(&curUnit,sizeof(curUnit),true)) { // true is important ! - MI32.mode.willConnect = 0; - MI32Client->disconnect(); - } - else { - MI32.mode.shallSetUnit = 0; - MI32.mode.willSetUnit = 0; - } - } - MI32Client->disconnect(); - NimBLEDevice::deleteClient(MI32Client); - vTaskDelay(500); + vTaskDelay(500/ portTICK_PERIOD_MS); MI32.mode.connected = 0; vTaskDelete( NULL ); } - void MI32StartBatteryTask(){ if (MI32.mode.connected) return; MI32.mode.willReadBatt = 1; + MI32.mode.willConnect = 1; + MI32.mode.canScan = 0; xTaskCreatePinnedToCore( MI32BatteryTask, /* Function to implement the task */ "MI32BatteryTask", /* Name of the task */ @@ -684,31 +718,32 @@ void MI32BatteryTask(void *pvParameters){ } MI32.mode.connected = 0; - MI32ConnectActiveSensor(); - uint32_t timer = 0; - while (MI32.mode.connected == 0){ - if (timer>1000){ + if(MI32ConnectActiveSensor()){ + uint32_t timer = 0; + while (MI32.mode.connected == 0){ + if (timer>1000){ + break; + } + timer++; + vTaskDelay(30/ portTICK_PERIOD_MS); + } + + switch(MIBLEsensors[MI32.state.sensor].type){ + case FLORA: + MI32batteryFLORA(); + break; + case LYWSD02: + MI32batteryLYWSD02(); + break; + case CGD1: + MI32batteryCGD1(); break; } - timer++; - vTaskDelay(10); - } - - switch(MIBLEsensors[MI32.state.sensor].type){ - case FLORA: - MI32batteryFLORA(); - break; - case LYWSD02: - MI32batteryLYWSD02(); - break; - case CGD1: - MI32batteryCGD1(); - break; - } - MI32Client->disconnect(); + MI32Client->disconnect(); + } MI32.mode.willReadBatt = 0; - NimBLEDevice::deleteClient(MI32Client); - vTaskDelay(500); + // Wifi.counter = 0; // Now check it + vTaskDelay(500/ portTICK_PERIOD_MS); MI32.mode.connected = 0; vTaskDelete( NULL ); } @@ -720,27 +755,26 @@ void MI32batteryFLORA(){ break; } timer++; - vTaskDelay(10); + vTaskDelay(10/ portTICK_PERIOD_MS); } - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); + DEBUG_SENSOR_LOG(PSTR("%s connected for battery"),kMI32SlaveType[MIBLEsensors[MI32.state.sensor].type-1] ); NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID FLserviceUUID("00001204-0000-1000-8000-00805f9b34fb"); - static BLEUUID FLcharUUID("00001a02-0000-1000-8000-00805f9b34fb"); + static BLEUUID FLserviceUUID(0x00001204,0x0000,0x1000,0x800000805f9b34fb); + static BLEUUID FLcharUUID(0x00001a02,0x0000,0x1000,0x800000805f9b34fb); pSvc = MI32Client->getService(FLserviceUUID); - if(pSvc) { /** make sure it's not null */ + if(pSvc) { pChr = pSvc->getCharacteristic(FLcharUUID); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - else { - MI32.mode.readingDone = 1; - return; - } - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); + if (pChr){ + DEBUG_SENSOR_LOG(PSTR("%s: got Flora char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); + if(pChr->canRead()) { + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); + } } + MI32.mode.readingDone = 1; } void MI32batteryLYWSD02(){ @@ -750,27 +784,27 @@ void MI32batteryLYWSD02(){ break; } timer++; - vTaskDelay(10); + vTaskDelay(10/ portTICK_PERIOD_MS); } NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID LY2serviceUUID("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"); - static BLEUUID LY2charUUID("EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6"); + static BLEUUID LY2serviceUUID(0xEBE0CCB0,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); + static BLEUUID LY2charUUID(0xEBE0CCC4,0x7A0A,0x4B0C,0x8A1A6FF2997DA3A6); pSvc = MI32Client->getService(LY2serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(LY2charUUID); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - else { - return; - } - if(pChr->canRead()) { - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("LYWSD02 char")); - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); + if (pChr){ + DEBUG_SENSOR_LOG( PSTR("%s: got LYWSD02 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); + if(pChr->canRead()) { + DEBUG_SENSOR_LOG(PSTR("LYWSD02 char")); + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); + } } + MI32.mode.readingDone = 1; } void MI32batteryCGD1(){ @@ -780,26 +814,26 @@ void MI32batteryCGD1(){ break; } timer++; - vTaskDelay(10); + vTaskDelay(10/ portTICK_PERIOD_MS); } NimBLERemoteService* pSvc = nullptr; NimBLERemoteCharacteristic* pChr = nullptr; - static BLEUUID CGD1serviceUUID("180F"); - static BLEUUID CGD1charUUID("2A19"); + static BLEUUID CGD1serviceUUID((uint16_t)0x180F); + static BLEUUID CGD1charUUID((uint16_t)0x2A19); pSvc = MI32Client->getService(CGD1serviceUUID); if(pSvc) { pChr = pSvc->getCharacteristic(CGD1charUUID); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); } - else { - return; - } - if(pChr->canRead()) { - const char *buf = pChr->readValue().c_str(); - MI32readBat((char*)buf); + if (pChr){ + DEBUG_SENSOR_LOG(PSTR("%s: got CGD1 char %s"),D_CMND_MI32, pChr->getUUID().toString().c_str()); + if(pChr->canRead()) { + const char *buf = pChr->readValue().c_str(); + MI32readBat((char*)buf); + } } + MI32.mode.readingDone = 1; } @@ -819,14 +853,14 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ } MI32_ReverseMAC(_beacon.Mac); - DEBUG_SENSOR_LOG(PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); - DEBUG_SENSOR_LOG(PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MiBeacon type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[0],(uint8_t)_buf[1],(uint8_t)_buf[2],(uint8_t)_buf[3],(uint8_t)_buf[4],(uint8_t)_buf[5],(uint8_t)_buf[6],(uint8_t)_buf[7]); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR(" type:%02x: %02x %02x %02x %02x %02x %02x %02x %02x"),_beacon.type, (uint8_t)_buf[8],(uint8_t)_buf[9],(uint8_t)_buf[10],(uint8_t)_buf[11],(uint8_t)_buf[12],(uint8_t)_buf[13],(uint8_t)_buf[14],(uint8_t)_buf[15]); if(MIBLEsensors[_slot].type==4 || MIBLEsensors[_slot].type==6){ DEBUG_SENSOR_LOG(PSTR("LYWSD03 and CGD1 no support for MiBeacon, type %u"),MIBLEsensors[_slot].type); return; } - DEBUG_SENSOR_LOG(PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s at slot %u"), kMI32SlaveType[MIBLEsensors[_slot].type-1],_slot); switch(_beacon.type){ case 0x04: _tempFloat=(float)(_beacon.temp)/10.0f; @@ -834,7 +868,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].temp=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 4: temp updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 4: U16: %u Temp"), _beacon.temp ); break; case 0x06: _tempFloat=(float)(_beacon.hum)/10.0f; @@ -842,11 +876,11 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].hum=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 6: hum updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode 6: U16: %u Hum"), _beacon.hum); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 6: U16: %u Hum"), _beacon.hum); break; case 0x07: MIBLEsensors[_slot].lux=_beacon.lux & 0x00ffffff; - DEBUG_SENSOR_LOG(PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); break; case 0x08: _tempFloat =(float)_beacon.moist; @@ -854,7 +888,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].moisture=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 8: moisture updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 8: U8: %u Moisture"), _beacon.moist); break; case 0x09: _tempFloat=(float)(_beacon.fert); @@ -862,14 +896,14 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].fertility=_tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode 9: fertility updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode 9: U16: %u Fertility"), _beacon.fert); break; case 0x0a: if(_beacon.bat<101){ MIBLEsensors[_slot].bat = _beacon.bat; DEBUG_SENSOR_LOG(PSTR("Mode a: bat updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode a: U8: %u %%"), _beacon.bat); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode a: U8: %u %%"), _beacon.bat); break; case 0x0d: _tempFloat=(float)(_beacon.HT.temp)/10.0f; @@ -882,7 +916,7 @@ void MI32parseMiBeacon(char * _buf, uint32_t _slot){ MIBLEsensors[_slot].hum = _tempFloat; DEBUG_SENSOR_LOG(PSTR("Mode d: hum updated")); } - DEBUG_SENSOR_LOG(PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); + // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); break; } } @@ -990,6 +1024,8 @@ void MI32EverySecond(bool restart){ _counter = 0; MI32.mode.canScan = 0; MI32.mode.canConnect = 1; + MI32.mode.willReadBatt = 0; + MI32.mode.willConnect = 0; return; } @@ -1028,9 +1064,9 @@ void MI32EverySecond(bool restart){ if(_counter==0) { MI32.state.sensor = _nextSensorSlot; - AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u"),D_CMND_MI32, MI32.state.sensor); + AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s: active sensor now: %u of %u"),D_CMND_MI32, MI32.state.sensor, MIBLEsensors.size()-1); MI32.mode.canScan = 0; - if (MI32.mode.runningScan == 1 || MI32.mode.connected == 1) return; + if (MI32.mode.runningScan|| MI32.mode.connected || MI32.mode.willConnect) return; _nextSensorSlot++; MI32.mode.canConnect = 1; if(MI32.mode.connected == 0) { @@ -1044,7 +1080,7 @@ void MI32EverySecond(bool restart){ } } - if (MI32.state.sensor==MIBLEsensors.size()-1) { + if (_nextSensorSlot>(MIBLEsensors.size()-1)) { _nextSensorSlot= 0; _counter++; if (MI32.mode.shallReadBatt){ @@ -1159,6 +1195,7 @@ const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}"; void MI32Show(bool json) { + if (json) { for (uint32_t i = 0; i < MIBLEsensors.size(); i++) { /* @@ -1172,7 +1209,7 @@ void MI32Show(bool json) MIBLEsensors[i].serial[3], MIBLEsensors[i].serial[4], MIBLEsensors[i].serial[5]); if (MIBLEsensors[i].type == FLORA) { - if (!isnan(MIBLEsensors[i].temp)) { // this is the error code -> no temperature + if (!isnan(MIBLEsensors[i].temp)) { char temperature[FLOATSZ]; // all sensors have temperature dtostrfd(MIBLEsensors[i].temp, Settings.flag2.temperature_resolution, temperature); ResponseAppend_P(PSTR("\"" D_JSON_TEMPERATURE "\":%s"), temperature); @@ -1299,4 +1336,4 @@ bool Xsns62(uint8_t function) return result; } #endif // USE_MI_ESP32 -#endif // ESP32 +#endif // ESP32 \ No newline at end of file From 16aa38be4943cb1f4edadf89f30feeb84de70763 Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Sat, 30 May 2020 01:05:10 +0200 Subject: [PATCH 12/48] fix Min stage settings --- tasmota/xsns_67_as3935.ino | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tasmota/xsns_67_as3935.ino b/tasmota/xsns_67_as3935.ino index 59618c32a..2b73a2749 100644 --- a/tasmota/xsns_67_as3935.ino +++ b/tasmota/xsns_67_as3935.ino @@ -465,8 +465,18 @@ bool AS3935SetDefault() { void AS3935InitSettings() { if(Settings.as3935_functions.nf_autotune){ - AS3935SetGain(INDOORS); - AS3935SetNoiseFloor(0); + if(Settings.as3935_parameter.nf_autotune_min) { + if (Settings.as3935_parameter.nf_autotune_min > 7) { + AS3935SetGain(OUTDOORS); + AS3935SetNoiseFloor(Settings.as3935_parameter.nf_autotune_min - 8); + } else { + AS3935SetGain(INDOORS); + AS3935SetNoiseFloor(Settings.as3935_parameter.nf_autotune_min); + } + } else { + AS3935SetGain(INDOORS); + AS3935SetNoiseFloor(0); + } } I2cWrite8(AS3935_ADDR, 0x00, Settings.as3935_sensor_cfg[0]); I2cWrite8(AS3935_ADDR, 0x01, Settings.as3935_sensor_cfg[1]); From 3d42fae8bdd07b3e7fab4c17f1e6d70eef398650 Mon Sep 17 00:00:00 2001 From: Paul C Diem Date: Fri, 29 May 2020 18:55:33 -0500 Subject: [PATCH 13/48] Fix SO88 incoming power state check --- tasmota/support_device_groups.ino | 3 +-- tasmota/support_tasmota.ino | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tasmota/support_device_groups.ino b/tasmota/support_device_groups.ino index 9572c9a6a..3e40f189f 100644 --- a/tasmota/support_device_groups.ino +++ b/tasmota/support_device_groups.ino @@ -387,7 +387,7 @@ void SendReceiveDeviceGroupMessage(struct device_group * device_group, struct de case DGR_ITEM_POWER: if (Settings.flag4.remote_device_mode) { // SetOption88 - Enable relays in separate device groups bool on = (value & 1); - if (on != (power & 1)) ExecuteCommandPower(device_group_index + 1, (on ? POWER_ON : POWER_OFF), SRC_REMOTE); + if (on != (power & (1 << device_group_index))) ExecuteCommandPower(device_group_index + 1, (on ? POWER_ON : POWER_OFF), SRC_REMOTE); } else if (device_group->local) { uint8_t mask_devices = value >> 24; @@ -396,7 +396,6 @@ void SendReceiveDeviceGroupMessage(struct device_group * device_group, struct de uint32_t mask = 1 << i; bool on = (value & mask); if (on != (power & mask)) ExecuteCommandPower(i + 1, (on ? POWER_ON : POWER_OFF), SRC_REMOTE); - if (Settings.flag4.remote_device_mode) break; // SetOption88 - Enable relays in separate device groups } } break; diff --git a/tasmota/support_tasmota.ino b/tasmota/support_tasmota.ino index 26341b416..3cbfb497c 100644 --- a/tasmota/support_tasmota.ino +++ b/tasmota/support_tasmota.ino @@ -573,7 +573,7 @@ void ExecuteCommandPower(uint32_t device, uint32_t state, uint32_t source) #ifdef USE_DEVICE_GROUPS if (SRC_REMOTE != source && SRC_RETRY != source) { if (Settings.flag4.remote_device_mode) // SetOption88 - Enable relays in separate device groups - SendDeviceGroupMessage(device - 1, DGR_MSGTYP_UPDATE, DGR_ITEM_POWER, (power >> device - 1) & 1 | 0x01000000); // Explicitly set number of relays to one + SendDeviceGroupMessage(device - 1, DGR_MSGTYP_UPDATE, DGR_ITEM_POWER, (power >> (device - 1)) & 1 | 0x01000000); // Explicitly set number of relays to one else SendLocalDeviceGroupMessage(DGR_MSGTYP_UPDATE, DGR_ITEM_POWER, power); } From 7f1514e6da269972fabe458ef3b5cdabd40cba2d Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Sat, 30 May 2020 10:48:12 +0200 Subject: [PATCH 14/48] some fixes and optimizations --- tasmota/xdrv_10_scripter.ino | 139 ++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 51 deletions(-) diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index 434028092..a2562a36c 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -1276,6 +1276,7 @@ chknext: #endif //USE_ENERGY_SENSOR break; case 'f': +//#define DEBUG_FS #ifdef USE_SCRIPT_FATFS if (!strncmp(vname,"fo(",3)) { lp+=3; @@ -1304,6 +1305,9 @@ chknext: for (uint8_t cnt=0;cnt=SFS_MAX) ind=SFS_MAX-1; - glob_script_mem.files[ind].close(); - glob_script_mem.file_flags[ind].is_open=0; + if (fvar>=0) { + uint8_t ind=fvar; + if (ind>=SFS_MAX) ind=SFS_MAX-1; +#ifdef DEBUG_FS + AddLog_P2(LOG_LEVEL_INFO,PSTR("closing file %d"),ind); +#endif + glob_script_mem.files[ind].close(); + glob_script_mem.file_flags[ind].is_open=0; + } fvar=0; lp++; len=0; @@ -2951,7 +2966,6 @@ int16_t Run_Scripter(const char *type, int8_t tlen, char *js) { toLogEOL("for error",lp); } } else if (!strncmp(lp,"next",4)) { - lp+=4; lp_next=lp; if (floop>0) { // for next loop @@ -4300,8 +4314,7 @@ void Script_Check_Hue(String *response) { uint8_t hue_script_found=Run_Scripter(">H",-2,0); if (hue_script_found!=99) return; - char line[128]; - char tmp[128]; + char tmp[256]; uint8_t hue_devs=0; uint8_t vindex=0; char *cp; @@ -4316,17 +4329,7 @@ void Script_Check_Hue(String *response) { } if (*lp!=';') { // check this line - memcpy(line,lp,sizeof(line)); - line[sizeof(line)-1]=0; - cp=line; - for (uint32_t i=0; im",-2,0); if (msect==99) { - char line[128]; - char tmp[128]; + char tmp[256]; char *lp=glob_script_mem.section_ptr+2; while (lp) { while (*lp==SCRIPT_EOL) { @@ -5530,17 +5532,7 @@ uint8_t msect=Run_Scripter(">m",-2,0); } if (*lp!=';') { // send this line to smtp - memcpy(line,lp,sizeof(line)); - line[sizeof(line)-1]=0; - char *cp=line; - for (uint32_t i=0; iprintln(tmp); func(tmp); } @@ -5563,8 +5555,7 @@ uint8_t msect=Run_Scripter(">m",-2,0); void ScriptJsonAppend(void) { uint8_t web_script=Run_Scripter(">J",-2,0); if (web_script==99) { - char line[128]; - char tmp[128]; + char tmp[256]; char *lp=glob_script_mem.section_ptr+2; while (lp) { while (*lp==SCRIPT_EOL) { @@ -5575,17 +5566,7 @@ void ScriptJsonAppend(void) { } if (*lp!=';') { // send this line to mqtt - memcpy(line,lp,sizeof(line)); - line[sizeof(line)-1]=0; - char *cp=line; - for (uint32_t i=0; it1",3,0); + } +} + +void script_task2(void *arg) { + //uint32_t lastms=millis(); + //uint32_t time; + while (1) { + //time=millis()-lastms; + //lastms=millis(); + //time=esp32_tasks[1].task_timer-time; + //if (timet2",3,0); + } +} +uint32_t scripter_create_task(uint32_t num, uint32_t time, uint32_t core) { + //return 0; + BaseType_t res = 0; + if (core > 1) { core = 1; } + if (num == 1) { + if (esp32_tasks[0].task_t) { vTaskDelete(esp32_tasks[0].task_t); } + res = xTaskCreatePinnedToCore(script_task1, "T1", STASK_STACK, NULL, STASK_PRIO, &esp32_tasks[0].task_t, core); + esp32_tasks[0].task_timer = time; + } else { + if (esp32_tasks[1].task_t) { vTaskDelete(esp32_tasks[1].task_t); } + res = xTaskCreatePinnedToCore(script_task2, "T2", STASK_STACK, NULL, STASK_PRIO, &esp32_tasks[1].task_t, core); + esp32_tasks[1].task_timer = time; + } + return res; +} +#else + uint16_t task_timer1; uint16_t task_timer2; TaskHandle_t task_t1; @@ -5625,13 +5667,6 @@ void script_task2(void *arg) { Run_Scripter(">t2",3,0); } } -#ifndef STASK_STACK -#define STASK_STACK 4096 -#endif - -#ifndef STASK_PRIO -#define STASK_PRIO 5 -#endif uint32_t scripter_create_task(uint32_t num, uint32_t time, uint32_t core) { //return 0; @@ -5648,6 +5683,8 @@ uint32_t scripter_create_task(uint32_t num, uint32_t time, uint32_t core) { } return res; } +#endif + #endif // USE_SCRIPT_TASK #endif // ESP32 /*********************************************************************************************\ From 7e28e03d7814c4a07ac31f706235517e24ee5fdf Mon Sep 17 00:00:00 2001 From: Staars Date: Sat, 30 May 2020 13:50:22 +0200 Subject: [PATCH 15/48] add commands to touch pin button on ESP32 --- tasmota/i18n.h | 5 ++++ tasmota/support_button.ino | 16 +++++++++--- tasmota/support_command.ino | 50 +++++++++++++++++++++++++++++++++++-- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 89d931908..1f0b00807 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -319,6 +319,11 @@ #define D_CMND_HUMOFFSET "HumOffset" #define D_CMND_GLOBAL_TEMP "GlobalTemp" #define D_CMND_GLOBAL_HUM "GlobalHum" +#ifdef ESP32 +#define D_CMND_TOUCH_CAL "TouchCal" +#define D_CMND_TOUCH_THRES "TouchThres" +#define D_CMND_TOUCH_NUM "TouchNum" +#endif //ESP32 // Commands xdrv_01_mqtt.ino #define D_CMND_MQTTLOG "MqttLog" diff --git a/tasmota/support_button.ino b/tasmota/support_button.ino index 96dfaf73a..a92a56be5 100644 --- a/tasmota/support_button.ino +++ b/tasmota/support_button.ino @@ -52,6 +52,14 @@ struct BUTTON { uint8_t adc = 99; // ADC0 button number } Button; +#ifdef ESP32 +struct TOUCH_BUTTON { + uint8_t pin_threshold = TOUCH_PIN_THRESHOLD; + uint8_t hit_threshold = TOUCH_HIT_THRESHOLD; + uint8_t calibration = 0; // Bitfield +} TOUCH_BUTTON; +#endif // ESP32 + /********************************************************************************************/ void ButtonPullupFlag(uint8 button_bit) @@ -155,15 +163,15 @@ void ButtonHandler(void) uint32_t _value = touchRead(Pin(GPIO_KEY1, button_index)); button = NOT_PRESSED; if (_value != 0){ // probably read-error - if(_value < TOUCH_PIN_THRESHOLD){ - if(++Button.touch_hits[button_index]>TOUCH_HIT_THRESHOLD){ - button = PRESSED; - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Touch value: %u hits: %u"), _value, Button.touch_hits[button_index]); + if(_value < TOUCH_BUTTON.pin_threshold){ + if(++Button.touch_hits[button_index]>TOUCH_BUTTON.hit_threshold){ + if (!bitRead(TOUCH_BUTTON.calibration, button_index+1)) button = PRESSED; } } else Button.touch_hits[button_index] = 0; } else Button.touch_hits[button_index] = 0; + if (bitRead(TOUCH_BUTTON.calibration, button_index+1)) AddLog_P2(LOG_LEVEL_INFO, PSTR("PLOT: %u, %u, %u,"),button_index+1, _value, Button.touch_hits[button_index]); // button number (1..4) , value, continuous hits under threshold } else{ // Normal button button = (digitalRead(Pin(GPIO_KEY1, button_index)) != bitRead(Button.inverted_mask, button_index)); diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 9d9fbfa19..6c44391a9 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -38,7 +38,11 @@ const char kTasmotaCommands[] PROGMEM = "|" // No prefix #endif // USE_DEVICE_GROUPS_SEND D_CMND_DEVGROUP_SHARE "|" D_CMND_DEVGROUPSTATUS "|" #endif // USE_DEVICE_GROUPS - D_CMND_SENSOR "|" D_CMND_DRIVER; + D_CMND_SENSOR "|" D_CMND_DRIVER +#ifdef ESP32 + "|" D_CMND_TOUCH_CAL "|" D_CMND_TOUCH_THRES "|" D_CMND_TOUCH_NUM +#endif //ESP32 + ; void (* const TasmotaCommand[])(void) PROGMEM = { &CmndBacklog, &CmndDelay, &CmndPower, &CmndStatus, &CmndState, &CmndSleep, &CmndUpgrade, &CmndUpgrade, &CmndOtaUrl, @@ -61,7 +65,11 @@ void (* const TasmotaCommand[])(void) PROGMEM = { #endif // USE_DEVICE_GROUPS_SEND &CmndDevGroupShare, &CmndDevGroupStatus, #endif // USE_DEVICE_GROUPS - &CmndSensor, &CmndDriver }; + &CmndSensor, &CmndDriver +#ifdef ESP32 + ,&CmndTouchCal, &CmndTouchThres, &CmndTouchNum +#endif //ESP32 + }; const char kWifiConfig[] PROGMEM = D_WCFG_0_RESTART "||" D_WCFG_2_WIFIMANAGER "||" D_WCFG_4_RETRY "|" D_WCFG_5_WAIT "|" D_WCFG_6_SERIAL "|" D_WCFG_7_WIFIMANAGER_RESET_ONLY; @@ -1946,3 +1954,41 @@ void CmndDriver(void) { XdrvCall(FUNC_COMMAND_DRIVER); } + +#ifdef ESP32 +void CmndTouchCal(void) +{ + if (XdrvMailbox.payload >= 0) { + if (XdrvMailbox.payload < MAX_KEYS + 1) TOUCH_BUTTON.calibration = bitSet(TOUCH_BUTTON.calibration, XdrvMailbox.payload); + if (XdrvMailbox.payload == 0) TOUCH_BUTTON.calibration = 0; + if (XdrvMailbox.payload == 255) TOUCH_BUTTON.calibration = 255; // all pinss + } + Response_P(PSTR("{\"" D_CMND_TOUCH_CAL "\": %u"), TOUCH_BUTTON.calibration); + ResponseJsonEnd(); + AddLog_P2(LOG_LEVEL_INFO, PSTR("Button Touchvalue Hits,")); +} + +void CmndTouchThres(void) +{ + if (XdrvMailbox.payload >= 0) { + if (XdrvMailbox.payload<256){ + TOUCH_BUTTON.pin_threshold = XdrvMailbox.payload; + } + } + Response_P(PSTR("{\"" D_CMND_TOUCH_THRES "\": %u"), TOUCH_BUTTON.pin_threshold); + ResponseJsonEnd(); +} + +void CmndTouchNum(void) +{ + if (XdrvMailbox.payload >= 0) { + if (XdrvMailbox.payload<32){ + TOUCH_BUTTON.hit_threshold = XdrvMailbox.payload; + } + } + Response_P(PSTR("{\"" D_CMND_TOUCH_NUM "\": %u"), TOUCH_BUTTON.hit_threshold); + ResponseJsonEnd(); + +} + +#endif //ESP32 \ No newline at end of file From bf73728224c3a778d1f5a6a9055e81486392f4da Mon Sep 17 00:00:00 2001 From: Staars Date: Sat, 30 May 2020 13:50:57 +0200 Subject: [PATCH 16/48] add semi-generic serial plotter to PIO --- pio/serial-plotter.py | 188 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100755 pio/serial-plotter.py diff --git a/pio/serial-plotter.py b/pio/serial-plotter.py new file mode 100755 index 000000000..5abde9c72 --- /dev/null +++ b/pio/serial-plotter.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +""" + serial-plotter.py - for Tasmota + + Copyright (C) 2020 Christian Baars + + 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 . + +Requirements: + - Python + - pip3 matplotlib + - a Tasmotadriver that plots + +Instructions: + expects serial data in the format: + 'PLOT: graphnumber value' + graph (1-4) + integer value + Code snippet example: (last value will be ignored) + AddLog_P2(LOG_LEVEL_INFO, PSTR("PLOT: %u, %u, %u,"),button_index+1, _value, Button.touch_hits[button_index]); + +Usage: + set serial config in code + ./serial-plotter.py + set output in tasmota, e.g.; TouchCal 1..4 (via Textbox) + +""" +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.widgets import TextBox +import time +import serial +import argparse + +#default values +port = '/dev/cu.SLAB_USBtoUART' +baud = 115200 + +#command line input +parser = argparse.ArgumentParser() +parser.add_argument("--port", "-p", help="change serial port, default: " + port) +parser.add_argument("--baud", "-b", help="change baud rate, default: " + str(baud)) +args = parser.parse_args() +if args.port: + print("change serial port to %s" % args.port) + port = args.port +if args.baud: + print("change baud rate to %s" % args.baud) + baud = args.baud + + +#time range +dt = 0.01 +t = np.arange(0.0, 100, dt) + +#lists for the data +xs = [0] #counting up x +ys = [[0],[0],[0],[0]] #4 fixed graphs for now +max_y = 1 +# min_y = 0 + +fig = plt.figure('Tasmota Serial Plotter') +ax = fig.add_subplot(111, autoscale_on=True, xlim=(0, 200), ylim=(0, 20)) #fixed x scale for now, y will adapt +ax.grid() + +line1, = ax.plot([], [], color = "r", label='G 1') +line2, = ax.plot([], [], color = "g", label='G 2') +line3, = ax.plot([], [], color = "b", label='G 3') +line4, = ax.plot([], [], color = "y", label='G 4') + +time_template = 'time = %.1fs' +time_text = ax.text(0.05, 0.9, '', transform=ax.transAxes) + +ser = serial.Serial() +ser.port = port +ser.baudrate = baud +ser.timeout = 0 #return immediately +try: + ser.open() +except: + print("Could not connect to serial with settings: " + str(ser.port) + ' at ' + str(ser.baudrate) + 'baud') + print("port available?") + exit() + +if ser.is_open==True: + print("Serial Plotter started ...:") + plt.title('connected to ' + str(ser.port) + ' at ' + str(ser.baudrate) + 'baud') +else: + print("Could not connect to serial: " + str(ser.port) + ' at ' + str(ser.baudrate) + 'baud') + plt.title('NOT connected to ' + str(ser.port) + ' at ' + str(ser.baudrate) + 'baud') + +def init(): + line1.set_data([], []) + line2.set_data([], []) + line3.set_data([], []) + line4.set_data([], []) + time_text.set_text('') + return [line1,line2,line3,line4,time_text ] #was line + + +def parse_line(data_line): + pos = data_line.find("PLOT:", 10) + if pos<0: + # print("wrong format") + return 0,0 + + raw_data = data_line[pos+6:] + val_list = raw_data.split(',') + try: + g = int(val_list[0]) + v = int(val_list[1]) + return g, v + except: + return 0,0 + +def update(num, line1, line2): + global xs, ys, max_y + + time_text.set_text(time_template % (num*dt) ) + + receive_data = str(ser.readline()) #string + + g, v = parse_line(receive_data) + if (g in range(1,5)): + # print(v,g) + if v>max_y: + max_y = v + print(max_y) + ax.set_ylim([0, max_y * 1.2]) + + idx = 0 + for y in ys: + y.append(y[-1]) + if idx == g-1: + y[-1] = v + idx = idx +1 + xs.append(xs[-1]+1) + + if len(ys[0])>200: + xs.pop() + for y in ys: + y.pop(0) + line1.set_data(xs, ys[0]) + line2.set_data(xs, ys[1]) + line3.set_data(xs, ys[2]) + line4.set_data(xs, ys[3]) + return [line1,line2,line3,line4, time_text] + +def handle_close(evt): + print('Closing serial connection') + ser.close() + print('Closed serial plotter') + +ani = animation.FuncAnimation(fig, update, None, fargs=[line1, line2], + interval=10, blit=True, init_func=init) + +ax.set_xlabel('Last 200 Samples') +ax.set_ylabel('Values') + +fig.canvas.mpl_connect('close_event', handle_close) + +def submit(text): + print (text) + ser.write(text.encode() + "\n".encode()) + +plt.subplots_adjust(bottom=0.25) +axbox = plt.axes([0.15, 0.05, 0.7, 0.075]) +text_box = TextBox(axbox, 'Send:', initial='') +text_box.on_submit(submit) + +ax.legend(loc='lower right', ncol=2) + +if ser.is_open==True: + plt.show() + From 86fc34e612e3b234faf010dffd281ef8abc93589 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sat, 30 May 2020 14:27:07 +0200 Subject: [PATCH 17/48] Move serial-plotter --- {pio => tools}/serial-plotter.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {pio => tools}/serial-plotter.py (100%) mode change 100755 => 100644 diff --git a/pio/serial-plotter.py b/tools/serial-plotter.py old mode 100755 new mode 100644 similarity index 100% rename from pio/serial-plotter.py rename to tools/serial-plotter.py From 29070f1136e3f269da03df8b4799659533b256bd Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sat, 30 May 2020 14:59:52 +0200 Subject: [PATCH 18/48] Add ``CpuFrequency`` and ``FlashFrequency`` to status 0 - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` --- RELEASENOTES.md | 3 +++ tasmota/CHANGELOG.md | 2 ++ tasmota/support_command.ino | 16 ++++++++-------- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7fb0366a3..bf2983136 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -68,3 +68,6 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add support for VEML6075 UVA/UVB/UVINDEX Sensor by device111 (#8432) - Add support for VEML7700 Ambient light intensity Sensor by device111 (#8432) - Add Three Phase Export Active Energy to SDM630 driver +- Add Zigbee options to ``ZbSend`` to write and report attributes +- Add ``CpuFrequency`` to ``status 2`` +- Add ``FlashFrequency`` to ``status 4`` diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 9fb3490fc..8e48a6302 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -12,6 +12,8 @@ - Add wildcard pattern ``?`` for JSON matching in rules - Add support for unique MQTTClient (and inherited fallback topic) by full Mac address using ``mqttclient DVES_%12X`` (#8300) - Add Zigbee options to ``ZbSend`` to write and report attributes +- Add ``CpuFrequency`` to ``status 2`` +- Add ``FlashFrequency`` to ``status 4`` ### 8.3.1.1 20200518 diff --git a/tasmota/support_command.ino b/tasmota/support_command.ino index 6c44391a9..f143a9316 100644 --- a/tasmota/support_command.ino +++ b/tasmota/support_command.ino @@ -41,7 +41,7 @@ const char kTasmotaCommands[] PROGMEM = "|" // No prefix D_CMND_SENSOR "|" D_CMND_DRIVER #ifdef ESP32 "|" D_CMND_TOUCH_CAL "|" D_CMND_TOUCH_THRES "|" D_CMND_TOUCH_NUM -#endif //ESP32 +#endif //ESP32 ; void (* const TasmotaCommand[])(void) PROGMEM = { @@ -65,10 +65,10 @@ void (* const TasmotaCommand[])(void) PROGMEM = { #endif // USE_DEVICE_GROUPS_SEND &CmndDevGroupShare, &CmndDevGroupStatus, #endif // USE_DEVICE_GROUPS - &CmndSensor, &CmndDriver + &CmndSensor, &CmndDriver #ifdef ESP32 ,&CmndTouchCal, &CmndTouchThres, &CmndTouchNum -#endif //ESP32 +#endif //ESP32 }; const char kWifiConfig[] PROGMEM = @@ -444,14 +444,14 @@ void CmndStatus(void) ",\"" D_JSON_BOOTVERSION "\":%d" #endif ",\"" D_JSON_COREVERSION "\":\"" ARDUINO_CORE_RELEASE "\",\"" D_JSON_SDKVERSION "\":\"%s\"," - "\"Hardware\":\"%s\"" + "\"CpuFrequency\":%d,\"Hardware\":\"%s\"" "%s}}"), my_version, my_image, GetBuildDateAndTime().c_str() #ifdef ESP8266 , ESP.getBootVersion() #endif , ESP.getSdkVersion(), - GetDeviceHardware().c_str(), + ESP.getCpuFreqMHz(), GetDeviceHardware().c_str(), GetStatistics().c_str()); MqttPublishPrefixTopic_P(option, PSTR(D_CMND_STATUS "2")); } @@ -476,7 +476,7 @@ void CmndStatus(void) #ifdef ESP8266 ",\"" D_JSON_FLASHCHIPID "\":\"%06X\"" #endif - ",\"" D_JSON_FLASHMODE "\":%d,\"" + ",\"FlashFrequency\":%d,\"" D_JSON_FLASHMODE "\":%d,\"" D_JSON_FEATURES "\":[\"%08X\",\"%08X\",\"%08X\",\"%08X\",\"%08X\",\"%08X\",\"%08X\"]"), ESP_getSketchSize()/1024, ESP.getFreeSketchSpace()/1024, ESP_getFreeHeap()/1024, #ifdef ESP32 @@ -486,7 +486,7 @@ void CmndStatus(void) #ifdef ESP8266 , ESP.getFlashChipId() #endif - , ESP.getFlashChipMode(), + , ESP.getFlashChipSpeed()/1000000, ESP.getFlashChipMode(), LANGUAGE_LCID, feature_drv1, feature_drv2, feature_sns1, feature_sns2, feature5, feature6); XsnsDriverState(); ResponseAppend_P(PSTR(",\"Sensors\":")); @@ -1980,7 +1980,7 @@ void CmndTouchThres(void) } void CmndTouchNum(void) -{ +{ if (XdrvMailbox.payload >= 0) { if (XdrvMailbox.payload<32){ TOUCH_BUTTON.hit_threshold = XdrvMailbox.payload; From 54806fd527357603e6f60477d73ebf04a6f3b48b Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Sat, 30 May 2020 15:29:47 +0200 Subject: [PATCH 19/48] files system update --- tasmota/xdrv_10_scripter.ino | 145 +++++++++++++++++++++++------------ tasmota/xdrv_13_display.ino | 21 +++-- 2 files changed, 110 insertions(+), 56 deletions(-) diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index a2562a36c..0edfad516 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -64,7 +64,6 @@ keywords if then else endif, or, and are better readable for beginners (others m #define MAX_SCRIPT_SIZE MAX_RULE_SIZE*MAX_RULE_SETS - uint32_t EncodeLightId(uint8_t relay_id); uint32_t DecodeLightId(uint32_t hue_id); @@ -79,30 +78,40 @@ uint32_t DecodeLightId(uint32_t hue_id); #endif #endif // USE_SCRIPT_COMPRESSION -#if defined(ESP32) && defined(ESP32_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS) +#if (defined(LITTLEFS_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS)) || (USE_SCRIPT_FATFS==-1) + +#ifdef ESP32 #include "FS.h" #include "SPIFFS.h" +#else +#include +#endif +FS *fsp; void SaveFile(const char *name,const uint8_t *buf,uint32_t len) { - File file = SPIFFS.open(name, FILE_WRITE); + File file = fsp->open(name, "w"); if (!file) return; file.write(buf, len); file.close(); } #define FORMAT_SPIFFS_IF_FAILED true -uint8_t spiffs_mounted=0; +uint8_t fs_mounted=0; void LoadFile(const char *name,uint8_t *buf,uint32_t len) { - if (!spiffs_mounted) { + if (!fs_mounted) { +#ifdef ESP32 if(!SPIFFS.begin(FORMAT_SPIFFS_IF_FAILED)){ +#else + if(!fsp->begin()){ +#endif //Serial.println("SPIFFS Mount Failed"); return; } - spiffs_mounted=1; + fs_mounted=1; } - File file = SPIFFS.open(name); + File file = fsp->open(name, "r"); if (!file) return; file.read(buf, len); file.close(); @@ -116,29 +125,43 @@ enum {OPER_EQU=1,OPER_PLS,OPER_MIN,OPER_MUL,OPER_DIV,OPER_PLSEQU,OPER_MINEQU,OPE enum {SCRIPT_LOGLEVEL=1,SCRIPT_TELEPERIOD}; #ifdef USE_SCRIPT_FATFS + +#if USE_SCRIPT_FATFS>=0 #include - -//#define USE_MMC - -#ifdef USE_MMC -#include -#undef FS_USED -#define FS_USED SD_MMC -#else #include -#undef FS_USED -#define FS_USED SD +#ifdef ESP32 +FS *fsp; +#else +SDClass *fsp; +#endif #endif #ifndef ESP32 +// esp8266 + +#if USE_SCRIPT_FATFS>=0 +// old fs #undef FILE_WRITE #define FILE_WRITE (sdfat::O_READ | sdfat::O_WRITE | sdfat::O_CREAT) #define FILE_APPEND (sdfat::O_READ | sdfat::O_WRITE | sdfat::O_CREAT | sdfat::O_APPEND) + +#else +// new fs +#undef FILE_WRITE +#define FILE_WRITE "w" +#undef FILE_READ +#define FILE_READ "r" +#undef FILE_APPEND +#define FILE_APPEND "a" #endif +#endif // USE_SCRIPT_FATFS>=0 + + #ifndef FAT_SCRIPT_SIZE #define FAT_SCRIPT_SIZE 4096 #endif + #ifdef ESP32 #undef FAT_SCRIPT_NAME #define FAT_SCRIPT_NAME "/script.txt" @@ -150,7 +173,8 @@ enum {SCRIPT_LOGLEVEL=1,SCRIPT_TELEPERIOD}; #if USE_STANDARD_SPI_LIBRARY==0 #warning ("FATFS standard spi should be used"); #endif -#endif + +#endif // USE_SCRIPT_FATFS #ifdef SUPPORT_MQTT_EVENT #include // Import LinkedList library @@ -624,10 +648,18 @@ char *script; #ifdef USE_SCRIPT_FATFS if (!glob_script_mem.script_sd_found) { + +#if USE_SCRIPT_FATFS>=0 + fsp=&SD; + #ifdef USE_MMC - if (FS_USED.begin()) { + if (fsp->begin()) { #else - if (FS_USED.begin(USE_SCRIPT_FATFS)) { + if (SD.begin(USE_SCRIPT_FATFS)) { +#endif + +#else + if (fsp->begin()) { #endif glob_script_mem.script_sd_found=1; } else { @@ -1308,7 +1340,7 @@ chknext: #ifdef DEBUG_FS AddLog_P2(LOG_LEVEL_INFO,PSTR("open file for read %d"),cnt); #endif - glob_script_mem.files[cnt]=FS_USED.open(str,FILE_READ); + glob_script_mem.files[cnt]=fsp->open(str,FILE_READ); if (glob_script_mem.files[cnt].isDirectory()) { glob_script_mem.files[cnt].rewindDirectory(); glob_script_mem.file_flags[cnt].is_dir=1; @@ -1318,12 +1350,12 @@ chknext: } else { if (mode==1) { - glob_script_mem.files[cnt]=FS_USED.open(str,FILE_WRITE); + glob_script_mem.files[cnt]=fsp->open(str,FILE_WRITE); #ifdef DEBUG_FS AddLog_P2(LOG_LEVEL_INFO,PSTR("open file for write %d"),cnt); #endif } else { - glob_script_mem.files[cnt]=FS_USED.open(str,FILE_APPEND); + glob_script_mem.files[cnt]=fsp->open(str,FILE_APPEND); #ifdef DEBUG_FS AddLog_P2(LOG_LEVEL_INFO,PSTR("open file for append %d"),cnt); #endif @@ -1465,7 +1497,7 @@ chknext: lp+=3; char str[glob_script_mem.max_ssize+1]; lp=GetStringResult(lp,OPER_EQU,str,0); - FS_USED.remove(str); + fsp->remove(str); lp++; len=0; goto exit; @@ -1505,7 +1537,7 @@ chknext: char str[glob_script_mem.max_ssize+1]; lp=GetStringResult(lp,OPER_EQU,str,0); // execute script - File ef=FS_USED.open(str); + File ef=fsp->open(str,FILE_READ); if (ef) { uint16_t fsiz=ef.size(); if (fsiz<2048) { @@ -1527,7 +1559,7 @@ chknext: lp+=4; char str[glob_script_mem.max_ssize+1]; lp=GetStringResult(lp,OPER_EQU,str,0); - fvar=FS_USED.mkdir(str); + fvar=fsp->mkdir(str); lp++; len=0; goto exit; @@ -1536,7 +1568,7 @@ chknext: lp+=4; char str[glob_script_mem.max_ssize+1]; lp=GetStringResult(lp,OPER_EQU,str,0); - fvar=FS_USED.rmdir(str); + fvar=fsp->rmdir(str); lp++; len=0; goto exit; @@ -1545,7 +1577,7 @@ chknext: lp+=3; char str[glob_script_mem.max_ssize+1]; lp=GetStringResult(lp,OPER_EQU,str,0); - if (FS_USED.exists(str)) fvar=1; + if (fsp->exists(str)) fvar=1; else fvar=0; lp++; len=0; @@ -3714,7 +3746,7 @@ void ListDir(char *path, uint8_t depth) { char format[12]; sprintf(format,"%%-%ds",24-depth); - File dir=FS_USED.open(path); + File dir=fsp->open(path, FILE_READ); if (dir) { dir.rewindDirectory(); if (strlen(path)>1) { @@ -3836,8 +3868,8 @@ void script_upload(void) { if (upload.status == UPLOAD_FILE_START) { char npath[48]; sprintf(npath,"%s/%s",path,upload.filename.c_str()); - FS_USED.remove(npath); - upload_file=FS_USED.open(npath,FILE_WRITE); + fsp->remove(npath); + upload_file=fsp->open(npath,FILE_WRITE); if (!upload_file) Web.upload_error=1; } else if(upload.status == UPLOAD_FILE_WRITE) { if (upload_file) upload_file.write(upload.buf,upload.currentSize); @@ -3856,12 +3888,12 @@ uint8_t DownloadFile(char *file) { File download_file; WiFiClient download_Client; - if (!FS_USED.exists(file)) { + if (!fsp->exists(file)) { AddLog_P(LOG_LEVEL_INFO,PSTR("file not found")); return 0; } - download_file=FS_USED.open(file,FILE_READ); + download_file=fsp->open(file,FILE_READ); if (!download_file) { AddLog_P(LOG_LEVEL_INFO,PSTR("could not open file")); return 0; @@ -4041,16 +4073,16 @@ void ScriptSaveSettings(void) { #if !defined(USE_24C256) && defined(USE_SCRIPT_FATFS) if (glob_script_mem.flags&1) { - FS_USED.remove(FAT_SCRIPT_NAME); - File file=FS_USED.open(FAT_SCRIPT_NAME,FILE_WRITE); + fsp->remove(FAT_SCRIPT_NAME); + File file=fsp->open(FAT_SCRIPT_NAME,FILE_WRITE); file.write((const uint8_t*)glob_script_mem.script_ram,FAT_SCRIPT_SIZE); file.close(); } #endif -#if defined(ESP32) && defined(ESP32_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS) +#if defined(LITTLEFS_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS) if (glob_script_mem.flags&1) { - SaveFile("/script.txt",(uint8_t*)glob_script_mem.script_ram,ESP32_SCRIPT_SIZE); + SaveFile("/script.txt",(uint8_t*)glob_script_mem.script_ram,LITTLEFS_SCRIPT_SIZE); } #endif } @@ -4065,7 +4097,7 @@ void ScriptSaveSettings(void) { #ifdef USE_SCRIPT_COMPRESSION #ifndef USE_24C256 #ifndef USE_SCRIPT_FATFS -#ifndef ESP32_SCRIPT_SIZE +#ifndef LITTLEFS_SCRIPT_SIZE //AddLog_P2(LOG_LEVEL_INFO,PSTR("in string: %s len = %d"),glob_script_mem.script_ram,strlen(glob_script_mem.script_ram)); uint32_t len_compressed = SCRIPT_COMPRESS(glob_script_mem.script_ram, strlen(glob_script_mem.script_ram), Settings.rules[0], MAX_SCRIPT_SIZE-1); @@ -5710,7 +5742,7 @@ bool Xdrv10(uint8_t function) #ifdef USE_SCRIPT_COMPRESSION #ifndef USE_24C256 #ifndef USE_SCRIPT_FATFS -#ifndef ESP32_SCRIPT_SIZE +#ifndef LITTLEFS_SCRIPT_SIZE int32_t len_decompressed; sprt=(char*)calloc(UNISHOXRSIZE+8,1); if (!sprt) { break; } @@ -5756,10 +5788,14 @@ bool Xdrv10(uint8_t function) #endif #endif + #ifdef USE_SCRIPT_FATFS +#if USE_SCRIPT_FATFS>=0 + fsp = &SD; + #ifdef USE_MMC - if (FS_USED.begin()) { + if (fsp->begin()) { #else #ifdef ESP32 @@ -5767,10 +5803,15 @@ bool Xdrv10(uint8_t function) SPI.begin(Pin(GPIO_SPI_CLK),Pin(GPIO_SPI_MISO),Pin(GPIO_SPI_MOSI), -1); } #endif - if (FS_USED.begin(USE_SCRIPT_FATFS)) { + if (SD.begin(USE_SCRIPT_FATFS)) { #endif - //FS_USED.dateTimeCallback(dateTime); +#else + fsp = &LittleFS; + if (fsp->begin()) { +#endif + + //fsp->dateTimeCallback(dateTime); glob_script_mem.script_sd_found=1; char *script; @@ -5778,8 +5819,8 @@ bool Xdrv10(uint8_t function) if (!script) break; glob_script_mem.script_ram=script; glob_script_mem.script_size=FAT_SCRIPT_SIZE; - if (FS_USED.exists(FAT_SCRIPT_NAME)) { - File file=FS_USED.open(FAT_SCRIPT_NAME,FILE_READ); + if (fsp->exists(FAT_SCRIPT_NAME)) { + File file=fsp->open(FAT_SCRIPT_NAME,FILE_READ); file.read((uint8_t*)script,FAT_SCRIPT_SIZE); file.close(); } @@ -5796,15 +5837,21 @@ bool Xdrv10(uint8_t function) #endif -#if defined(ESP32) && defined(ESP32_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS) +#if defined(LITTLEFS_SCRIPT_SIZE) && !defined(USE_24C256) && !defined(USE_SCRIPT_FATFS) + +#ifdef ESP32 + fsp = &SPIFFS; +#else + fsp = &LittleFS; +#endif char *script; - script=(char*)calloc(ESP32_SCRIPT_SIZE+4,1); + script=(char*)calloc(LITTLEFS_SCRIPT_SIZE+4,1); if (!script) break; - LoadFile("/script.txt",(uint8_t*)script,ESP32_SCRIPT_SIZE); + LoadFile("/script.txt",(uint8_t*)script,LITTLEFS_SCRIPT_SIZE); glob_script_mem.script_ram=script; - glob_script_mem.script_size=ESP32_SCRIPT_SIZE; - script[ESP32_SCRIPT_SIZE-1]=0; + glob_script_mem.script_size=LITTLEFS_SCRIPT_SIZE; + script[LITTLEFS_SCRIPT_SIZE-1]=0; // use rules storage for permanent vars glob_script_mem.script_pram=(uint8_t*)Settings.rules[0]; glob_script_mem.script_pram_size=MAX_SCRIPT_SIZE; diff --git a/tasmota/xdrv_13_display.ino b/tasmota/xdrv_13_display.ino index d2466d051..43245c794 100644 --- a/tasmota/xdrv_13_display.ino +++ b/tasmota/xdrv_13_display.ino @@ -505,7 +505,7 @@ void DisplayText(void) cp += var; linebuf[fill] = 0; break; -#if defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT) +#if defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT) && USE_SCRIPT_FATFS>=0 case 'P': { char *ep=strchr(cp,':'); if (ep) { @@ -1510,8 +1510,13 @@ void rgb888_to_565(uint8_t *in, uint16_t *out, uint32_t len); #endif #endif +#if defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT) && USE_SCRIPT_FATFS>=0 -#if defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT) +#ifdef ESP32 +extern FS *fsp; +#else +extern SDClass *fsp; +#endif #define XBUFF_LEN 128 void Draw_RGB_Bitmap(char *file,uint16_t xp, uint16_t yp) { if (!renderer) return; @@ -1527,7 +1532,7 @@ void Draw_RGB_Bitmap(char *file,uint16_t xp, uint16_t yp) { if (!strcmp(estr,"rgb")) { // special rgb format - fp=SD.open(file,FILE_READ); + fp=fsp->open(file,FILE_READ); if (!fp) return; uint16_t xsize; fp.read((uint8_t*)&xsize,2); @@ -1564,7 +1569,7 @@ void Draw_RGB_Bitmap(char *file,uint16_t xp, uint16_t yp) { #ifdef ESP32 #ifdef JPEG_PICTS if (psramFound()) { - fp=SD.open(file,FILE_READ); + fp=fsp->open(file,FILE_READ); if (!fp) return; uint32_t size = fp.size(); uint8_t *mem = (uint8_t *)heap_caps_malloc(size+4, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); @@ -1850,7 +1855,9 @@ void DisplayCheckGraph() { #if defined(USE_SCRIPT_FATFS) && defined(USE_SCRIPT) +#ifdef ESP32 #include +#endif void Save_graph(uint8_t num, char *path) { if (!renderer) return; @@ -1858,8 +1865,8 @@ void Save_graph(uint8_t num, char *path) { struct GRAPH *gp=graph[index]; if (!gp) return; File fp; - SD.remove(path); - fp=SD.open(path,FILE_WRITE); + fsp->remove(path); + fp=fsp->open(path,FILE_WRITE); if (!fp) return; char str[32]; sprintf_P(str,PSTR("%d\t%d\t%d\t"),gp->xcnt,gp->xs,gp->ys); @@ -1884,7 +1891,7 @@ void Restore_graph(uint8_t num, char *path) { struct GRAPH *gp=graph[index]; if (!gp) return; File fp; - fp=SD.open(path,FILE_READ); + fp=fsp->open(path,FILE_READ); if (!fp) return; char vbuff[32]; char *cp=vbuff; From 8447a3a703a1d556e7a41369dcc2265fb673c971 Mon Sep 17 00:00:00 2001 From: Staars Date: Sun, 31 May 2020 10:37:52 +0200 Subject: [PATCH 20/48] Update serial-plotter.py, more infos in the code, small refactorings --- tools/serial-plotter.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/tools/serial-plotter.py b/tools/serial-plotter.py index 5abde9c72..83ec6dba6 100644 --- a/tools/serial-plotter.py +++ b/tools/serial-plotter.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 """ serial-plotter.py - for Tasmota @@ -20,7 +20,8 @@ Requirements: - Python - - pip3 matplotlib + - pip3 install matplotlib pyserial + - for Windows: Full python install including tkinter - a Tasmotadriver that plots Instructions: @@ -32,8 +33,7 @@ Instructions: AddLog_P2(LOG_LEVEL_INFO, PSTR("PLOT: %u, %u, %u,"),button_index+1, _value, Button.touch_hits[button_index]); Usage: - set serial config in code - ./serial-plotter.py + ./serial-plotter.py --port /dev/PORT --baud BAUD (or change defaults in the script) set output in tasmota, e.g.; TouchCal 1..4 (via Textbox) """ @@ -44,6 +44,10 @@ from matplotlib.widgets import TextBox import time import serial import argparse +import sys + +print("Python version") +print (sys.version) #default values port = '/dev/cu.SLAB_USBtoUART' @@ -164,25 +168,25 @@ def handle_close(evt): ser.close() print('Closed serial plotter') +def submit(text): + print (text) + ser.write(text.encode() + "\n".encode()) + + ani = animation.FuncAnimation(fig, update, None, fargs=[line1, line2], interval=10, blit=True, init_func=init) ax.set_xlabel('Last 200 Samples') ax.set_ylabel('Values') +plt.subplots_adjust(bottom=0.25) +ax.legend(loc='lower right', ncol=2) fig.canvas.mpl_connect('close_event', handle_close) -def submit(text): - print (text) - ser.write(text.encode() + "\n".encode()) - -plt.subplots_adjust(bottom=0.25) axbox = plt.axes([0.15, 0.05, 0.7, 0.075]) text_box = TextBox(axbox, 'Send:', initial='') text_box.on_submit(submit) -ax.legend(loc='lower right', ncol=2) - if ser.is_open==True: plt.show() - + \ No newline at end of file From 72014b777314fad588bb09e0f38b6ed8e3f6d520 Mon Sep 17 00:00:00 2001 From: halfbakery Date: Sun, 31 May 2020 12:46:15 +0200 Subject: [PATCH 21/48] Make a previously undocumented switch debouncing feature clean and official --- tasmota/support_switch.ino | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasmota/support_switch.ino b/tasmota/support_switch.ino index 46b642e5b..c13243c0e 100644 --- a/tasmota/support_switch.ino +++ b/tasmota/support_switch.ino @@ -82,8 +82,8 @@ void SwitchProbe(void) if (uptime < 4) { return; } // Block GPIO for 4 seconds after poweron to workaround Wemos D1 / Obi RTS circuit uint8_t state_filter = Settings.switch_debounce / SWITCH_PROBE_INTERVAL; // 5, 10, 15 - uint8_t force_high = (Settings.switch_debounce % 50) &1; // 51, 101, 151 etc - uint8_t force_low = (Settings.switch_debounce % 50) &2; // 52, 102, 152 etc + uint8_t force_high = (Settings.switch_debounce % 10) &1; // 51, 101, 151 etc + uint8_t force_low = (Settings.switch_debounce % 10) &2; // 52, 102, 152 etc for (uint32_t i = 0; i < MAX_SWITCHES; i++) { if (PinUsed(GPIO_SWT1, i)) { From b97e7cd1e475e228d0b47ba735e045d0f1b00766 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Sun, 31 May 2020 16:28:02 +0200 Subject: [PATCH 22/48] Fix Sonoff Dual Buttons Fix Sonoff Dual Buttons (#8560) --- tasmota/support_button.ino | 71 +++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/tasmota/support_button.ino b/tasmota/support_button.ino index a92a56be5..bf058db4e 100644 --- a/tasmota/support_button.ino +++ b/tasmota/support_button.ino @@ -44,7 +44,7 @@ struct BUTTON { uint8_t dual_receive_count = 0; // Sonoff dual input flag uint8_t no_pullup_mask = 0; // key no pullup flag (1 = no pullup) uint8_t inverted_mask = 0; // Key inverted flag (1 = inverted) -#ifdef ESP32 +#ifdef ESP32 uint8_t touch_mask = 0; // Touch flag (1 = inverted) uint8_t touch_hits[MAX_KEYS] = { 0 }; // Hits in a row to filter out noise #endif // ESP32 @@ -80,6 +80,11 @@ void ButtonTouchFlag(uint8 button_bit) void ButtonInit(void) { Button.present = 0; +#ifdef ESP8266 + if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { + Button.present++; + } +#endif // ESP8266 for (uint32_t i = 0; i < MAX_KEYS; i++) { if (PinUsed(GPIO_KEY1, i)) { Button.present++; @@ -151,29 +156,35 @@ void ButtonHandler(void) Button.dual_code = 0; } } - else - if (PinUsed(GPIO_KEY1, button_index)) { - button_present = 1; - button = (digitalRead(Pin(GPIO_KEY1, button_index)) != bitRead(Button.inverted_mask, button_index)); + else { + if (PinUsed(GPIO_KEY1, button_index)) { + button_present = 1; + button = (digitalRead(Pin(GPIO_KEY1, button_index)) != bitRead(Button.inverted_mask, button_index)); + } } #else if (PinUsed(GPIO_KEY1, button_index)) { button_present = 1; - if (bitRead(Button.touch_mask, button_index)){ // Touch + if (bitRead(Button.touch_mask, button_index)) { // Touch uint32_t _value = touchRead(Pin(GPIO_KEY1, button_index)); button = NOT_PRESSED; - if (_value != 0){ // probably read-error - if(_value < TOUCH_BUTTON.pin_threshold){ - if(++Button.touch_hits[button_index]>TOUCH_BUTTON.hit_threshold){ - if (!bitRead(TOUCH_BUTTON.calibration, button_index+1)) button = PRESSED; + if (_value != 0) { // Probably read-error + if (_value < TOUCH_BUTTON.pin_threshold) { + if (++Button.touch_hits[button_index] > TOUCH_BUTTON.hit_threshold) { + if (!bitRead(TOUCH_BUTTON.calibration, button_index+1)) { + button = PRESSED; + } } + } else { + Button.touch_hits[button_index] = 0; } - else Button.touch_hits[button_index] = 0; + } else { + Button.touch_hits[button_index] = 0; } - else Button.touch_hits[button_index] = 0; - if (bitRead(TOUCH_BUTTON.calibration, button_index+1)) AddLog_P2(LOG_LEVEL_INFO, PSTR("PLOT: %u, %u, %u,"),button_index+1, _value, Button.touch_hits[button_index]); // button number (1..4) , value, continuous hits under threshold - } - else{ // Normal button + if (bitRead(TOUCH_BUTTON.calibration, button_index+1)) { + AddLog_P2(LOG_LEVEL_INFO, PSTR("PLOT: %u, %u, %u,"), button_index+1, _value, Button.touch_hits[button_index]); // Button number (1..4), value, continuous hits under threshold + } + } else { // Normal button button = (digitalRead(Pin(GPIO_KEY1, button_index)) != bitRead(Button.inverted_mask, button_index)); } } @@ -210,12 +221,12 @@ void ButtonHandler(void) if (!Button.hold_timer[button_index]) { button_pressed = true; } // Do not allow within 1 second } if (button_pressed) { - if (!Settings.flag3.mqtt_buttons) { + if (!Settings.flag3.mqtt_buttons) { // SetOption73 (0) - Decouple button from relay and send just mqtt topic if (!SendKey(KEY_BUTTON, button_index +1, POWER_TOGGLE)) { // Execute Toggle command via MQTT if ButtonTopic is set ExecuteCommandPower(button_index +1, POWER_TOGGLE, SRC_BUTTON); // Execute Toggle command internally } } else { - MqttButtonTopic(button_index +1, 1, 0); // SetOption73 (0) - Decouple button from relay and send just mqtt topic + MqttButtonTopic(button_index +1, 1, 0); // SetOption73 (0) - Decouple button from relay and send just mqtt topic } } } @@ -244,7 +255,7 @@ void ButtonHandler(void) Button.hold_timer[button_index] = 0; } else { Button.hold_timer[button_index]++; - if (Settings.flag.button_single) { // SetOption13 (0) - Allow only single button press for immediate action + if (Settings.flag.button_single) { // SetOption13 (0) - Allow only single button press for immediate action if (Button.hold_timer[button_index] == loops_per_second * hold_time_extent * Settings.param[P_HOLD_TIME] / 10) { // SetOption32 (40) - Button held for factor times longer snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_SETOPTION "13 0")); // Disable single press only ExecuteCommand(scmnd, SRC_BUTTON); @@ -252,13 +263,13 @@ void ButtonHandler(void) } else { if (Button.hold_timer[button_index] == loops_per_second * Settings.param[P_HOLD_TIME] / 10) { // SetOption32 (40) - Button hold Button.press_counter[button_index] = 0; - if (Settings.flag3.mqtt_buttons) { // SetOption73 (0) - Decouple button from relay and send just mqtt topic + if (Settings.flag3.mqtt_buttons) { // SetOption73 (0) - Decouple button from relay and send just mqtt topic MqttButtonTopic(button_index +1, 3, 1); } else { SendKey(KEY_BUTTON, button_index +1, POWER_HOLD); // Execute Hold command via MQTT if ButtonTopic is set } } else { - if (!Settings.flag.button_restrict) { + if (!Settings.flag.button_restrict) { // SetOption1 - Control button multipress if ((Button.hold_timer[button_index] == loops_per_second * hold_time_extent * Settings.param[P_HOLD_TIME] / 10)) { // SetOption32 (40) - Button held for factor times longer Button.press_counter[button_index] = 0; snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_RESET " 1")); @@ -269,14 +280,14 @@ void ButtonHandler(void) } } - if (!Settings.flag.button_single) { // SetOption13 (0) - Allow multi-press + if (!Settings.flag.button_single) { // SetOption13 (0) - Allow multi-press if (Button.window_timer[button_index]) { Button.window_timer[button_index]--; } else { if (!restart_flag && !Button.hold_timer[button_index] && (Button.press_counter[button_index] > 0) && (Button.press_counter[button_index] < 7)) { bool single_press = false; - if (Button.press_counter[button_index] < 3) { // Single or Double press + if (Button.press_counter[button_index] < 3) { // Single or Double press #ifdef ESP8266 if ((SONOFF_DUAL_R2 == my_module_type) || (SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { single_press = true; @@ -301,15 +312,21 @@ void ButtonHandler(void) if (WifiState() > WIFI_RESTART) { // Wifimanager active restart_flag = 1; } - if (!Settings.flag3.mqtt_buttons) { - if (Button.press_counter[button_index] == 1) { // By default first press always send a TOGGLE (2) + if (!Settings.flag3.mqtt_buttons) { // SetOption73 - Detach buttons from relays and enable MQTT action state for multipress + if (Button.press_counter[button_index] == 1) { // By default first press always send a TOGGLE (2) ExecuteCommandPower(button_index + Button.press_counter[button_index], POWER_TOGGLE, SRC_BUTTON); } else { SendKey(KEY_BUTTON, button_index +1, Button.press_counter[button_index] +9); // 2,3,4 and 5 press send just the key value (11,12,13 and 14) for rules - if (0 == button_index) { // BUTTON1 can toggle up to 5 relays if present. If a relay is not present will send out the key value (2,11,12,13 and 14) for rules - if ((Button.press_counter[button_index] > 1 && PinUsed(GPIO_REL1, Button.press_counter[button_index]-1)) && Button.press_counter[button_index] <= MAX_RELAY_BUTTON1) { + if (0 == button_index) { // BUTTON1 can toggle up to 5 relays if present. If a relay is not present will send out the key value (2,11,12,13 and 14) for rules + bool valid_relay = PinUsed(GPIO_REL1, Button.press_counter[button_index]-1); +#ifdef ESP8266 + if ((SONOFF_DUAL == my_module_type) || (CH4 == my_module_type)) { + valid_relay = (Button.press_counter[button_index] <= devices_present); + } +#endif // ESP8266 + if ((Button.press_counter[button_index] > 1) && valid_relay && (Button.press_counter[button_index] <= MAX_RELAY_BUTTON1)) { ExecuteCommandPower(button_index + Button.press_counter[button_index], POWER_TOGGLE, SRC_BUTTON); // Execute Toggle command internally - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("DBG: Relay%d found on GPIO%d"), Button.press_counter[button_index], Pin(GPIO_REL1, Button.press_counter[button_index]-1)); +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("DBG: Relay%d found on GPIO%d"), Button.press_counter[button_index], Pin(GPIO_REL1, Button.press_counter[button_index]-1)); } } } From 917124af2614ae5ea10531c242983feb9b560f0b Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Mon, 1 Jun 2020 11:51:57 +0200 Subject: [PATCH 23/48] fix start without stop condition --- tasmota/support.ino | 1 + 1 file changed, 1 insertion(+) diff --git a/tasmota/support.ino b/tasmota/support.ino index 9d1ff07f4..839e126f6 100644 --- a/tasmota/support.ino +++ b/tasmota/support.ino @@ -1459,6 +1459,7 @@ bool I2cValidRead(uint8_t addr, uint8_t reg, uint8_t size) } retry--; } + if (!retry) Wire.endTransmission(); return status; } From f1a2fb2b8d30c92b3780d6ac1e20f2784bb443d0 Mon Sep 17 00:00:00 2001 From: gemu2015 Date: Mon, 1 Jun 2020 15:17:32 +0200 Subject: [PATCH 24/48] fix serial buffer issues --- lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp | 1 + tasmota/xsns_52_ibeacon.ino | 10 ++++++---- tasmota/xsns_53_sml.ino | 19 ++++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) mode change 100644 => 100755 tasmota/xsns_52_ibeacon.ino diff --git a/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp b/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp index 6b41d068c..7a09518c2 100644 --- a/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp +++ b/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp @@ -174,6 +174,7 @@ bool TasmotaSerial::begin(long speed, int stop_bits) { m_uart = tasmota_serial_index; tasmota_serial_index--; TSerial = new HardwareSerial(m_uart); + if (TM_SERIAL_BUFFER_SIZE != serial_buffer_size) TSerial->setRxBufferSize(serial_buffer_size); if (2 == m_stop_bits) { TSerial->begin(speed, SERIAL_8N2, m_rx_pin, m_tx_pin); } else { diff --git a/tasmota/xsns_52_ibeacon.ino b/tasmota/xsns_52_ibeacon.ino old mode 100644 new mode 100755 index 88b040e76..5f9c0f3ba --- a/tasmota/xsns_52_ibeacon.ino +++ b/tasmota/xsns_52_ibeacon.ino @@ -25,6 +25,8 @@ #include +#define TMSBSIZ 256 + #define HM17_BAUDRATE 9600 #define IBEACON_DEBUG @@ -96,7 +98,7 @@ void IBEACON_Init() { // actually doesnt work reliably with software serial if (PinUsed(GPIO_IBEACON_RX) && PinUsed(GPIO_IBEACON_TX)) { - IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1); + IBEACON_Serial = new TasmotaSerial(Pin(GPIO_IBEACON_RX), Pin(GPIO_IBEACON_TX),1,0,TMSBSIZ); if (IBEACON_Serial->begin(HM17_BAUDRATE)) { if (IBEACON_Serial->hardwareSerial()) { ClaimSerial(); @@ -144,7 +146,7 @@ void hm17_every_second(void) { void hm17_sbclr(void) { memset(hm17_sbuffer,0,HM17_BSIZ); hm17_sindex=0; - IBEACON_Serial->flush(); + //IBEACON_Serial->flush(); } void hm17_sendcmd(uint8_t cmd) { @@ -405,7 +407,7 @@ hm17_v110: } } else { #ifdef IBEACON_DEBUG - if (hm17_debug) AddLog_P2(LOG_LEVEL_INFO, PSTR(">>%s"),&hm17_sbuffer[8]); + if (hm17_debug) AddLog_P2(LOG_LEVEL_INFO, PSTR(">->%s"),&hm17_sbuffer[8]); #endif } break; @@ -517,7 +519,7 @@ bool xsns52_cmd(void) { #ifdef IBEACON_DEBUG else if (*cp=='d') { cp++; - if (*cp) hm17_debug=atoi(cp); + hm17_debug=atoi(cp); Response_P(S_JSON_IBEACON, XSNS_52,"debug",hm17_debug); } #endif diff --git a/tasmota/xsns_53_sml.ino b/tasmota/xsns_53_sml.ino index 3b4357b26..cb51bcc82 100755 --- a/tasmota/xsns_53_sml.ino +++ b/tasmota/xsns_53_sml.ino @@ -49,6 +49,8 @@ #define SPECIAL_SS #endif +#define TMSBSIZ 256 + // addresses a bug in meter DWS74 //#define DWS74_BUG @@ -2144,9 +2146,9 @@ init10: // serial input, init #ifdef SPECIAL_SS if (meter_desc_p[meters].type=='m' || meter_desc_p[meters].type=='M' || meter_desc_p[meters].type=='p') { - meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1); + meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1,0,TMSBSIZ); } else { - meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1,1); + meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1,1,TMSBSIZ); } #else #ifdef ESP32 @@ -2154,8 +2156,9 @@ init10: if (uart_index==0) { ClaimSerial(); } uart_index--; if (uart_index<0) uart_index=0; + meter_ss[meters]->setRxBufferSize(TMSBSIZ); #else - meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1); + meter_ss[meters] = new TasmotaSerial(meter_desc_p[meters].srcpin,meter_desc_p[meters].trxpin,1,0,TMSBSIZ); #endif #endif @@ -2188,6 +2191,15 @@ uint32_t SML_SetBaud(uint32_t meter, uint32_t br) { if (meter<1 || meter>meters_used) return 0; meter--; if (!meter_ss[meter]) return 0; + +#ifdef ESP32 + meter_ss[meter]->flush(); + if (meter_desc_p[meter].type=='M') { + meter_ss[meter]->begin(br,SERIAL_8E1,meter_desc_p[meter].srcpin,meter_desc_p[meter].trxpin); + } else { + meter_ss[meter]->begin(br,SERIAL_8N1,meter_desc_p[meter].srcpin,meter_desc_p[meter].trxpin); + } +#else if (meter_ss[meter]->begin(br)) { meter_ss[meter]->flush(); } @@ -2196,6 +2208,7 @@ uint32_t SML_SetBaud(uint32_t meter, uint32_t br) { Serial.begin(br, SERIAL_8E1); } } +#endif return 1; } From 02375d6b28647529c6dc55cd5fba66d17b97ab7b Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Mon, 1 Jun 2020 15:45:22 +0200 Subject: [PATCH 25/48] Update TasmotaSerial.cpp --- lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp b/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp index 7a09518c2..6982779d5 100644 --- a/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp +++ b/lib/TasmotaSerial-3.0.0/src/TasmotaSerial.cpp @@ -174,7 +174,9 @@ bool TasmotaSerial::begin(long speed, int stop_bits) { m_uart = tasmota_serial_index; tasmota_serial_index--; TSerial = new HardwareSerial(m_uart); - if (TM_SERIAL_BUFFER_SIZE != serial_buffer_size) TSerial->setRxBufferSize(serial_buffer_size); + if (serial_buffer_size > 256) { + TSerial->setRxBufferSize(serial_buffer_size); + } if (2 == m_stop_bits) { TSerial->begin(speed, SERIAL_8N2, m_rx_pin, m_tx_pin); } else { From eafaccfcda2515a2c2cc0538c6831694d6790c85 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:00:56 +0200 Subject: [PATCH 26/48] Add support for up to two BH1750 sensors Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) --- RELEASENOTES.md | 1 + tasmota/CHANGELOG.md | 1 + tasmota/settings.h | 5 +- tasmota/xsns_10_bh1750.ino | 177 ++++++++++++++++++++++--------------- 4 files changed, 108 insertions(+), 76 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bf2983136..f0f2c1e69 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -71,3 +71,4 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add Zigbee options to ``ZbSend`` to write and report attributes - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` +- Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 8e48a6302..60563404f 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -14,6 +14,7 @@ - Add Zigbee options to ``ZbSend`` to write and report attributes - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` +- Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) ### 8.3.1.1 20200518 diff --git a/tasmota/settings.h b/tasmota/settings.h index accf21842..0ad7a3533 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -232,9 +232,8 @@ typedef union { struct { uint8_t spare0 : 1; uint8_t spare1 : 1; - uint8_t spare2 : 1; - uint8_t spare3 : 1; - uint8_t bh1750_resolution : 2; // Sensor10 1,2,3 + uint8_t bh1750_2_resolution : 2; + uint8_t bh1750_1_resolution : 2; // Sensor10 1,2,3 uint8_t hx711_json_weight_change : 1; // Sensor34 8,x - Enable JSON message on weight change uint8_t mhz19b_abc_disable : 1; // Disable ABC (Automatic Baseline Correction for MHZ19(B) (0 = Enabled (default), 1 = Disabled with Sensor15 command) }; diff --git a/tasmota/xsns_10_bh1750.ino b/tasmota/xsns_10_bh1750.ino index a098166df..7af8e3960 100644 --- a/tasmota/xsns_10_bh1750.ino +++ b/tasmota/xsns_10_bh1750.ino @@ -22,6 +22,11 @@ /*********************************************************************************************\ * BH1750 - Ambient Light Intensity * + * Bh1750Resolution1 0..2 - Set BH1750 1 resolution mode + * Bh1750Resolution2 0..2 - Set BH1750 2 resolution mode + * Bh1750MTime1 30..255 - Set BH1750 1 MT register + * Bh1750MTime2 30..255 - Set BH1750 2 MT register + * * I2C Address: 0x23 or 0x5C \*********************************************************************************************/ @@ -38,119 +43,148 @@ #define BH1750_MEASUREMENT_TIME_HIGH 0x40 // Measurement Time register high 3 bits #define BH1750_MEASUREMENT_TIME_LOW 0x60 // Measurement Time register low 5 bits -struct BH1750DATA { - uint8_t address; +#define D_PRFX_BH1750 "Bh1750" +#define D_CMND_RESOLUTION "Resolution" +#define D_CMND_MTREG "MTime" + +const char kBh1750Commands[] PROGMEM = D_PRFX_BH1750 "|" // Prefix + D_CMND_RESOLUTION "|" D_CMND_MTREG ; + +void (* const Bh1750Command[])(void) PROGMEM = { + &CmndBh1750Resolution, &CmndBh1750MTime }; + +struct { uint8_t addresses[2] = { BH1750_ADDR1, BH1750_ADDR2 }; uint8_t resolution[3] = { BH1750_CONTINUOUS_HIGH_RES_MODE, BH1750_CONTINUOUS_HIGH_RES_MODE2, BH1750_CONTINUOUS_LOW_RES_MODE }; - uint8_t type = 0; - uint8_t valid = 0; - uint8_t mtreg = 69; // Default Measurement Time - uint16_t illuminance = 0; + uint8_t count = 0; char types[7] = "BH1750"; } Bh1750; +struct { + uint8_t address; + uint8_t valid = 0; + uint8_t mtreg = 69; // Default Measurement Time + uint16_t illuminance = 0; +} Bh1750_sensors[2]; + /*********************************************************************************************/ -bool Bh1750SetResolution(void) -{ - Wire.beginTransmission(Bh1750.address); - Wire.write(Bh1750.resolution[Settings.SensorBits1.bh1750_resolution]); +uint8_t Bh1750Resolution(uint32_t sensor_index) { + uint8_t settings_resolution = Settings.SensorBits1.bh1750_1_resolution; + if (1 == sensor_index) { + settings_resolution = Settings.SensorBits1.bh1750_2_resolution; + } + return settings_resolution; +} + +bool Bh1750SetResolution(uint32_t sensor_index) { + Wire.beginTransmission(Bh1750_sensors[sensor_index].address); + Wire.write(Bh1750.resolution[Bh1750Resolution(sensor_index)]); return (!Wire.endTransmission()); } -bool Bh1750SetMTreg(void) -{ - Wire.beginTransmission(Bh1750.address); - uint8_t data = BH1750_MEASUREMENT_TIME_HIGH | ((Bh1750.mtreg >> 5) & 0x07); +bool Bh1750SetMTreg(uint32_t sensor_index) { + Wire.beginTransmission(Bh1750_sensors[sensor_index].address); + uint8_t data = BH1750_MEASUREMENT_TIME_HIGH | ((Bh1750_sensors[sensor_index].mtreg >> 5) & 0x07); Wire.write(data); if (Wire.endTransmission()) { return false; } - Wire.beginTransmission(Bh1750.address); - data = BH1750_MEASUREMENT_TIME_LOW | (Bh1750.mtreg & 0x1F); + Wire.beginTransmission(Bh1750_sensors[sensor_index].address); + data = BH1750_MEASUREMENT_TIME_LOW | (Bh1750_sensors[sensor_index].mtreg & 0x1F); Wire.write(data); if (Wire.endTransmission()) { return false; } - return Bh1750SetResolution(); + return Bh1750SetResolution(sensor_index); } -bool Bh1750Read(void) -{ - if (Bh1750.valid) { Bh1750.valid--; } +bool Bh1750Read(uint32_t sensor_index) { + if (Bh1750_sensors[sensor_index].valid) { Bh1750_sensors[sensor_index].valid--; } + + if (2 != Wire.requestFrom(Bh1750_sensors[sensor_index].address, (uint8_t)2)) { return false; } - if (2 != Wire.requestFrom(Bh1750.address, (uint8_t)2)) { return false; } float illuminance = (Wire.read() << 8) | Wire.read(); - illuminance /= (1.2 * (69 / (float)Bh1750.mtreg)); - if (1 == Settings.SensorBits1.bh1750_resolution) { + illuminance /= (1.2 * (69 / (float)Bh1750_sensors[sensor_index].mtreg)); + if (1 == Bh1750Resolution(sensor_index)) { illuminance /= 2; } - Bh1750.illuminance = illuminance; + Bh1750_sensors[sensor_index].illuminance = illuminance; - Bh1750.valid = SENSOR_MAX_MISS; + Bh1750_sensors[sensor_index].valid = SENSOR_MAX_MISS; return true; } /********************************************************************************************/ -void Bh1750Detect(void) -{ +void Bh1750Detect(void) { for (uint32_t i = 0; i < sizeof(Bh1750.addresses); i++) { - Bh1750.address = Bh1750.addresses[i]; - if (I2cActive(Bh1750.address)) { continue; } + if (I2cActive(Bh1750.addresses[i])) { continue; } - if (Bh1750SetMTreg()) { - I2cSetActiveFound(Bh1750.address, Bh1750.types); - Bh1750.type = 1; - break; + Bh1750_sensors[Bh1750.count].address = Bh1750.addresses[i]; + if (Bh1750SetMTreg(Bh1750.count)) { + I2cSetActiveFound(Bh1750_sensors[Bh1750.count].address, Bh1750.types); + Bh1750.count++; } } } -void Bh1750EverySecond(void) -{ - // 1mS - if (!Bh1750Read()) { - AddLogMissed(Bh1750.types, Bh1750.valid); +void Bh1750EverySecond(void) { + for (uint32_t i = 0; i < Bh1750.count; i++) { + // 1mS + if (!Bh1750Read(i)) { +// AddLogMissed(Bh1750.types, Bh1750.valid); + } } } /*********************************************************************************************\ - * Command Sensor10 - * - * 0 - High resolution mode (default) - * 1 - High resolution mode 2 - * 2 - Low resolution mode - * 31..254 - Measurement Time value (not persistent, default is 69) + * Commands \*********************************************************************************************/ -bool Bh1750CommandSensor(void) -{ - if (XdrvMailbox.data_len) { +void CmndBh1750Resolution(void) { + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Bh1750.count)) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 2)) { - Settings.SensorBits1.bh1750_resolution = XdrvMailbox.payload; - Bh1750SetResolution(); - } - else if ((XdrvMailbox.payload > 30) && (XdrvMailbox.payload < 255)) { - Bh1750.mtreg = XdrvMailbox.payload; - Bh1750SetMTreg(); + if (1 == XdrvMailbox.index) { + Settings.SensorBits1.bh1750_1_resolution = XdrvMailbox.payload; + } else { + Settings.SensorBits1.bh1750_2_resolution = XdrvMailbox.payload; + } + Bh1750SetResolution(XdrvMailbox.index -1); } + ResponseCmndIdxNumber(Bh1750Resolution(XdrvMailbox.index -1)); } - Response_P(PSTR("{\"" D_CMND_SENSOR "10\":{\"Resolution\":%d,\"MTime\":%d}}"), Settings.SensorBits1.bh1750_resolution, Bh1750.mtreg); - - return true; } -void Bh1750Show(bool json) -{ - if (Bh1750.valid) { - if (json) { - ResponseAppend_P(JSON_SNS_ILLUMINANCE, Bh1750.types, Bh1750.illuminance); -#ifdef USE_DOMOTICZ - if (0 == tele_period) { - DomoticzSensor(DZ_ILLUMINANCE, Bh1750.illuminance); +void CmndBh1750MTime(void) { + if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= Bh1750.count)) { + if ((XdrvMailbox.payload > 30) && (XdrvMailbox.payload < 255)) { + Bh1750_sensors[XdrvMailbox.index -1].mtreg = XdrvMailbox.payload; + Bh1750SetMTreg(XdrvMailbox.index -1); + } + ResponseCmndIdxNumber(Bh1750_sensors[XdrvMailbox.index -1].mtreg); + } +} + +/********************************************************************************************/ + +void Bh1750Show(bool json) { + for (uint32_t sensor_index = 0; sensor_index < Bh1750.count; sensor_index++) { + if (Bh1750_sensors[sensor_index].valid) { + char sensor_name[10]; + strlcpy(sensor_name, Bh1750.types, sizeof(sensor_name)); + if (Bh1750.count > 1) { + snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%d"), sensor_name, IndexSeparator(), sensor_index +1); // BH1750-1 } + + if (json) { + ResponseAppend_P(JSON_SNS_ILLUMINANCE, sensor_name, Bh1750_sensors[sensor_index].illuminance); +#ifdef USE_DOMOTICZ + if ((0 == tele_period) && (0 == sensor_index)) { + DomoticzSensor(DZ_ILLUMINANCE, Bh1750_sensors[sensor_index].illuminance); + } #endif // USE_DOMOTICZ #ifdef USE_WEBSERVER - } else { - WSContentSend_PD(HTTP_SNS_ILLUMINANCE, Bh1750.types, Bh1750.illuminance); + } else { + WSContentSend_PD(HTTP_SNS_ILLUMINANCE, sensor_name, Bh1750_sensors[sensor_index].illuminance); #endif // USE_WEBSERVER + } } } } @@ -159,8 +193,7 @@ void Bh1750Show(bool json) * Interface \*********************************************************************************************/ -bool Xsns10(uint8_t function) -{ +bool Xsns10(uint8_t function) { if (!I2cEnabled(XI2C_11)) { return false; } bool result = false; @@ -168,15 +201,13 @@ bool Xsns10(uint8_t function) if (FUNC_INIT == function) { Bh1750Detect(); } - else if (Bh1750.type) { + else if (Bh1750.count) { switch (function) { case FUNC_EVERY_SECOND: Bh1750EverySecond(); break; - case FUNC_COMMAND_SENSOR: - if (XSNS_10 == XdrvMailbox.index) { - result = Bh1750CommandSensor(); - } + case FUNC_COMMAND: + result = DecodeCommand(kBh1750Commands, Bh1750Command); break; case FUNC_JSON_APPEND: Bh1750Show(1); From 0fd566846bbdc994e157f2ac1117ccd880bac761 Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:43:53 +0200 Subject: [PATCH 27/48] update de language for illuminance for a "more" German Word --- tasmota/language/de_DE.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index 9f676f101..aa83ec0c2 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -107,7 +107,7 @@ #define D_HOST "Host" #define D_HOSTNAME "Hostname" #define D_HUMIDITY "Feuchtigkeit" -#define D_ILLUMINANCE "Beleuchtungsintensität" +#define D_ILLUMINANCE "Beleuchtungsstärke" #define D_IMMEDIATE "direkt" // Button immediate #define D_INDEX "Index" #define D_INFO "Info" From 965c2ae8072ba39c5028e051abb6ab33d5d2fb58 Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Mon, 1 Jun 2020 18:48:08 +0200 Subject: [PATCH 28/48] add 2 commands for VEML7700 Sensor (gain, integration time); value normalizing activated --- tasmota/xsns_71_veml7700.ino | 97 +++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 7 deletions(-) diff --git a/tasmota/xsns_71_veml7700.ino b/tasmota/xsns_71_veml7700.ino index de28661cc..18a57bcf7 100644 --- a/tasmota/xsns_71_veml7700.ino +++ b/tasmota/xsns_71_veml7700.ino @@ -37,12 +37,27 @@ Adafruit_VEML7700 veml7700 = Adafruit_VEML7700(); //create object copy const char HTTP_SNS_WHITE[] PROGMEM = "{s}%s " D_WHITE_CONTENT "{m}%d {e}"; const char JSON_SNS_VEML7700[] PROGMEM = ",\"%s\":{\"" D_JSON_ILLUMINANCE "\":%d,\"" D_JSON_WHITE_CONTENT "\":%d}"; +#define D_CMND_VEML7700_PWR "power" +#define D_CMND_VEML7700_GAIN "gain" +#define D_CMND_VEML7700_INTTIME "inttime" + +const char S_JSON_VEML7700_COMMAND_NVALUE[] PROGMEM = "{\"" D_NAME_VEML7700 "\":{\"%s\":%d}}"; +const char kVEML7700_Commands[] PROGMEM = D_CMND_VEML7700_PWR "|" D_CMND_VEML7700_GAIN "|" D_CMND_VEML7700_INTTIME; + +enum VEML7700_Commands { // commands for Console + CMND_VEML7700_PWR, + CMND_VEML7700_GAIN, + CMND_VEML7700_SET_IT, + }; + struct VEML7700STRUCT { char types[9] = D_NAME_VEML7700; uint8_t address = VEML7700_I2CADDR_DEFAULT; uint16_t lux = 0; uint16_t white = 0; + uint16_t lux_normalized = 0; + uint16_t white_normalized = 0; } veml7700_sensor; uint8_t veml7700_active = 0; @@ -57,27 +72,95 @@ void VEML7700Detect(void) { } } +uint16_t VEML7700TranslateItMs (uint8_t ittime){ + switch (ittime) { + case 0: return 100; + case 1: return 200; + case 2: return 400; + case 3: return 800; + case 8: return 50; + case 12: return 25; + default: return 0xFFFF; + } +} + +uint8_t VEML7700TranslateItInt (uint16_t ittimems){ + switch (ittimems) { + case 100: return 0; + case 200: return 1; + case 400: return 2; + case 800: return 3; + case 50: return 8; + case 25: return 12; + default: return 0xFF; + } +} + void VEML7700EverySecond(void) { - veml7700_sensor.lux = (uint16_t) veml7700.readLux(); - veml7700_sensor.white = (uint16_t) veml7700.readWhite(); + veml7700_sensor.lux_normalized = (uint16_t) veml7700.readLuxNormalized(); + veml7700_sensor.white_normalized = (uint16_t) veml7700.readWhiteNormalized(); + //veml7700_sensor.lux = (uint16_t) veml7700.readLux(); + //veml7700_sensor.white = (uint16_t) veml7700.readWhite(); } void VEML7700Show(bool json) { if (json) { - ResponseAppend_P(JSON_SNS_VEML7700, D_NAME_VEML7700, veml7700_sensor.lux, veml7700_sensor.white); + ResponseAppend_P(JSON_SNS_VEML7700, D_NAME_VEML7700, veml7700_sensor.lux_normalized, veml7700_sensor.white); #ifdef USE_DOMOTICZ - if (0 == tele_period) DomoticzSensor(DZ_ILLUMINANCE, veml7700_sensor.lux); + if (0 == tele_period) DomoticzSensor(DZ_ILLUMINANCE, veml7700_sensor.lux_normalized); #endif // USE_DOMOTICZ #ifdef USE_WEBSERVER } else { - WSContentSend_PD(HTTP_SNS_ILLUMINANCE, D_NAME_VEML7700, veml7700_sensor.lux); - WSContentSend_PD(HTTP_SNS_WHITE, D_NAME_VEML7700, veml7700_sensor.white); + WSContentSend_PD(HTTP_SNS_ILLUMINANCE, D_NAME_VEML7700, veml7700_sensor.lux_normalized); + WSContentSend_PD(HTTP_SNS_WHITE, D_NAME_VEML7700, veml7700_sensor.white_normalized); #endif // USE_WEBSERVER } } +bool VEML7700Cmd(void) { + char command[CMDSZ]; + uint8_t name_len = strlen(D_NAME_VEML7700); + if (!strncasecmp_P(XdrvMailbox.topic, PSTR(D_NAME_VEML7700), name_len)) { + uint32_t command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic + name_len, kVEML7700_Commands); + switch (command_code) { + case CMND_VEML7700_PWR: + if (XdrvMailbox.data_len) { + if (2 >= XdrvMailbox.payload) { + veml7700.enable(XdrvMailbox.payload); + } + } + Response_P(S_JSON_VEML7700_COMMAND_NVALUE, command, veml7700.enabled()); + break; + case CMND_VEML7700_GAIN: + if (XdrvMailbox.data_len) { + if (4 >= XdrvMailbox.payload) { + veml7700.setGain(XdrvMailbox.payload); + } + } + Response_P(S_JSON_VEML7700_COMMAND_NVALUE, command, veml7700.getGain()); + break; + case CMND_VEML7700_SET_IT: { + if (XdrvMailbox.data_len) { + uint8_t data = VEML7700TranslateItInt(XdrvMailbox.payload); + if (0xFF != data) { + veml7700.setIntegrationTime(data); + } + } + uint16_t dataret = VEML7700TranslateItMs(veml7700.getIntegrationTime()); + Response_P(S_JSON_VEML7700_COMMAND_NVALUE, command, dataret); + } + break; + default: + return false; + } + return true; + } + else { + return false; + } +} /*********************************************************************************************\ * Interface \*********************************************************************************************/ @@ -97,7 +180,7 @@ bool Xsns71(uint8_t function) VEML7700EverySecond(); break; case FUNC_COMMAND: - //result = VEML7700Cmd(); + result = VEML7700Cmd(); break; case FUNC_JSON_APPEND: VEML7700Show(1); From 8c29ead09d52151c8e441dbd598ce743b0672678 Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Tue, 2 Jun 2020 08:35:49 +0200 Subject: [PATCH 29/48] Add support for up to eight MCP9808 temperature sensors --- BUILDS.md | 3 + I2CDEVICES.md | 3 +- RELEASENOTES.md | 1 + .../.github/ISSUE_TEMPLATE.md | 46 +++ .../.github/PULL_REQUEST_TEMPLATE.md | 26 ++ .../.github/workflows/githubci.yml | 32 ++ lib/Adafruit_MCP9808_Tasmota/.gitignore | 8 + .../Adafruit_MCP9808.cpp | 273 ++++++++++++++++++ .../Adafruit_MCP9808.h | 87 ++++++ lib/Adafruit_MCP9808_Tasmota/README.md | 18 ++ lib/Adafruit_MCP9808_Tasmota/assets/board.jpg | Bin 0 -> 134987 bytes .../code-of-conduct.md | 127 ++++++++ .../examples/mcp9808test/mcp9808test.ino | 69 +++++ .../library.properties | 10 + lib/Adafruit_MCP9808_Tasmota/license.txt | 26 ++ tasmota/my_user_config.h | 1 + tasmota/support_features.ino | 4 +- tasmota/tasmota_configurations.h | 1 + tasmota/xsns_72_mcp9808.ino | 136 +++++++++ tools/decode-status.py | 2 +- 20 files changed, 870 insertions(+), 3 deletions(-) create mode 100644 lib/Adafruit_MCP9808_Tasmota/.github/ISSUE_TEMPLATE.md create mode 100644 lib/Adafruit_MCP9808_Tasmota/.github/PULL_REQUEST_TEMPLATE.md create mode 100644 lib/Adafruit_MCP9808_Tasmota/.github/workflows/githubci.yml create mode 100644 lib/Adafruit_MCP9808_Tasmota/.gitignore create mode 100644 lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.cpp create mode 100644 lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.h create mode 100644 lib/Adafruit_MCP9808_Tasmota/README.md create mode 100644 lib/Adafruit_MCP9808_Tasmota/assets/board.jpg create mode 100644 lib/Adafruit_MCP9808_Tasmota/code-of-conduct.md create mode 100644 lib/Adafruit_MCP9808_Tasmota/examples/mcp9808test/mcp9808test.ino create mode 100644 lib/Adafruit_MCP9808_Tasmota/library.properties create mode 100644 lib/Adafruit_MCP9808_Tasmota/license.txt create mode 100644 tasmota/xsns_72_mcp9808.ino diff --git a/BUILDS.md b/BUILDS.md index ec1720d44..92d081c1e 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -139,6 +139,9 @@ | USE_HRXL | - | - | - | - | x | - | - | | USE_TASMOTA_SLAVE | - | - | - | - | - | - | - | | USE_OPENTHERM | - | - | - | - | - | - | - | +| USE_VEML6075 | - | - | - | - | - | - | - | +| USE_VEML7700 | - | - | - | - | - | - | - | +| USE_MCP9808 | - | - | - | - | - | - | - | | | | | | | | | | | USE_NRF24 | - | - | - | - | - | - | - | | USE_MIBLE | - | - | - | - | - | - | - | diff --git a/I2CDEVICES.md b/I2CDEVICES.md index 788145d3e..4f6718b90 100644 --- a/I2CDEVICES.md +++ b/I2CDEVICES.md @@ -71,4 +71,5 @@ Index | Define | Driver | Device | Address(es) | Description 47 | USE_DISPLAY_SEVENSEG| xdsp_11 | HT16K33 | 0x70 - 0x77 | Seven segment LED 48 | USE_AS3935 | xsns_67 | AS3935 | 0x03 | Franklin Lightning Sensor 49 | USE_VEML6075 | xsns_70 | VEML6075 | 0x10 | UVA/UVB/UVINDEX Sensor - 50 | USE_VEML7700 | xsns_71 | VEML7700 | 0x10 | Ambient light intensity sensor \ No newline at end of file + 50 | USE_VEML7700 | xsns_71 | VEML7700 | 0x10 | Ambient light intensity sensor + 51 | USE_MCP9808 | xsns_72 | MCP9808 | 0x18 - 0x1F | Temperature sensor \ No newline at end of file diff --git a/RELEASENOTES.md b/RELEASENOTES.md index f0f2c1e69..2453fd5d4 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -72,3 +72,4 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` - Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) +- Add support for up to eight MCP9808 temperature sensors by device111 (#8594) diff --git a/lib/Adafruit_MCP9808_Tasmota/.github/ISSUE_TEMPLATE.md b/lib/Adafruit_MCP9808_Tasmota/.github/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..f0e26146f --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,46 @@ +Thank you for opening an issue on an Adafruit Arduino library repository. To +improve the speed of resolution please review the following guidelines and +common troubleshooting steps below before creating the issue: + +- **Do not use GitHub issues for troubleshooting projects and issues.** Instead use + the forums at http://forums.adafruit.com to ask questions and troubleshoot why + something isn't working as expected. In many cases the problem is a common issue + that you will more quickly receive help from the forum community. GitHub issues + are meant for known defects in the code. If you don't know if there is a defect + in the code then start with troubleshooting on the forum first. + +- **If following a tutorial or guide be sure you didn't miss a step.** Carefully + check all of the steps and commands to run have been followed. Consult the + forum if you're unsure or have questions about steps in a guide/tutorial. + +- **For Arduino projects check these very common issues to ensure they don't apply**: + + - For uploading sketches or communicating with the board make sure you're using + a **USB data cable** and **not** a **USB charge-only cable**. It is sometimes + very hard to tell the difference between a data and charge cable! Try using the + cable with other devices or swapping to another cable to confirm it is not + the problem. + + - **Be sure you are supplying adequate power to the board.** Check the specs of + your board and plug in an external power supply. In many cases just + plugging a board into your computer is not enough to power it and other + peripherals. + + - **Double check all soldering joints and connections.** Flakey connections + cause many mysterious problems. See the [guide to excellent soldering](https://learn.adafruit.com/adafruit-guide-excellent-soldering/tools) for examples of good solder joints. + + - **Ensure you are using an official Arduino or Adafruit board.** We can't + guarantee a clone board will have the same functionality and work as expected + with this code and don't support them. + +If you're sure this issue is a defect in the code and checked the steps above +please fill in the following fields to provide enough troubleshooting information. +You may delete the guideline and text above to just leave the following details: + +- Arduino board: **INSERT ARDUINO BOARD NAME/TYPE HERE** + +- Arduino IDE version (found in Arduino -> About Arduino menu): **INSERT ARDUINO + VERSION HERE** + +- List the steps to reproduce the problem below (if possible attach a sketch or + copy the sketch code in too): **LIST REPRO STEPS BELOW** diff --git a/lib/Adafruit_MCP9808_Tasmota/.github/PULL_REQUEST_TEMPLATE.md b/lib/Adafruit_MCP9808_Tasmota/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..7b641eb86 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,26 @@ +Thank you for creating a pull request to contribute to Adafruit's GitHub code! +Before you open the request please review the following guidelines and tips to +help it be more easily integrated: + +- **Describe the scope of your change--i.e. what the change does and what parts + of the code were modified.** This will help us understand any risks of integrating + the code. + +- **Describe any known limitations with your change.** For example if the change + doesn't apply to a supported platform of the library please mention it. + +- **Please run any tests or examples that can exercise your modified code.** We + strive to not break users of the code and running tests/examples helps with this + process. + +Thank you again for contributing! We will try to test and integrate the change +as soon as we can, but be aware we have many GitHub repositories to manage and +can't immediately respond to every request. There is no need to bump or check in +on a pull request (it will clutter the discussion of the request). + +Also don't be worried if the request is closed or not integrated--sometimes the +priorities of Adafruit's GitHub code (education, ease of use) might not match the +priorities of the pull request. Don't fret, the open source community thrives on +forks and GitHub makes it easy to keep your changes in a forked repo. + +After reviewing the guidelines above you can delete this text from the pull request. diff --git a/lib/Adafruit_MCP9808_Tasmota/.github/workflows/githubci.yml b/lib/Adafruit_MCP9808_Tasmota/.github/workflows/githubci.yml new file mode 100644 index 000000000..cb226da94 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/.github/workflows/githubci.yml @@ -0,0 +1,32 @@ +name: Arduino Library CI + +on: [pull_request, push, repository_dispatch] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v1 + with: + python-version: '3.x' + - uses: actions/checkout@v2 + - uses: actions/checkout@v2 + with: + repository: adafruit/ci-arduino + path: ci + + - name: pre-install + run: bash ci/actions_install.sh + + - name: test platforms + run: python3 ci/build_platform.py main_platforms + + - name: clang + run: python3 ci/run-clang-format.py -e "ci/*" -e "bin/*" -r . + + - name: doxygen + env: + GH_REPO_TOKEN: ${{ secrets.GH_REPO_TOKEN }} + PRETTYNAME : "Adafruit MCP9808 Arduino Library" + run: bash ci/doxy_gen_and_deploy.sh diff --git a/lib/Adafruit_MCP9808_Tasmota/.gitignore b/lib/Adafruit_MCP9808_Tasmota/.gitignore new file mode 100644 index 000000000..542d266a9 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/.gitignore @@ -0,0 +1,8 @@ +# osx +.DS_Store + +# doxygen +Doxyfile* +doxygen_sqlite3.db +html +*.tmp diff --git a/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.cpp b/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.cpp new file mode 100644 index 000000000..db3fa9f86 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.cpp @@ -0,0 +1,273 @@ +/*! + * @file Adafruit_MCP9808.cpp + * + * @mainpage Adafruit MCP9808 I2C Temp Sensor + * + * @section intro_sec Introduction + * + * I2C Driver for Microchip's MCP9808 I2C Temp sensor + * + * This is a library for the Adafruit MCP9808 breakout: + * http://www.adafruit.com/products/1782 + * + * Adafruit invests time and resources providing this open source code, + * please support Adafruit and open-source hardware by purchasing products from + * Adafruit! + * + * @section author Author + * + * K.Townsend (Adafruit Industries) + * + * @section license License + * + * BSD (see license.txt) + * + * @section HISTORY + * + * v1.0 - First release + * + * changes by Martin Wagner for tasmota project: + * + * - the libary supports variabel I2C address + * + */ + +#if ARDUINO >= 100 +#include "Arduino.h" +#else +#include "WProgram.h" +#endif + +#ifdef __AVR_ATtiny85__ +#include "TinyWireM.h" +#define Wire TinyWireM +#else +#include +#endif + +#include "Adafruit_MCP9808.h" + +/*! + * @brief Instantiates a new MCP9808 class + */ +Adafruit_MCP9808::Adafruit_MCP9808() {} + +/*! + * @brief Setups the HW + * @param *theWire + * @return True if initialization was successful, otherwise false. + */ +bool Adafruit_MCP9808::begin(TwoWire *theWire) { + _wire = theWire; + _i2caddr = MCP9808_I2CADDR_DEFAULT; + return init(); +} + +/*! + * @brief Setups the HW + * @param addr + * @return True if initialization was successful, otherwise false. + */ +bool Adafruit_MCP9808::begin(uint8_t addr) { + _i2caddr = addr; + _wire = &Wire; + return init(); +} + +/*! + * @brief Setups the HW + * @param addr + * @param *theWire + * @return True if initialization was successful, otherwise false. + */ +bool Adafruit_MCP9808::begin(uint8_t addr, TwoWire *theWire) { + _i2caddr = addr; + _wire = theWire; + return init(); +} + +/*! + * @brief Setups the HW with default address + * @return True if initialization was successful, otherwise false. + */ +bool Adafruit_MCP9808::begin() { + _i2caddr = MCP9808_I2CADDR_DEFAULT; + _wire = &Wire; + return init(); +} + +/*! + * @brief init function + * @return True if initialization was successful, otherwise false. + */ +bool Adafruit_MCP9808::init() { + _wire->begin(); + + if (read16(MCP9808_REG_MANUF_ID) != 0x0054) + return false; + if (read16(MCP9808_REG_DEVICE_ID) != 0x0400) + return false; + + write16(MCP9808_REG_CONFIG, 0x0); + return true; +} + +/*! + * @brief Reads the 16-bit temperature register and returns the Centigrade + * temperature as a float. + * @return Temperature in Centigrade. + */ +float Adafruit_MCP9808::readTempC(uint8_t addr) { + _i2caddr = addr; + float temp = NAN; + uint16_t t = read16(MCP9808_REG_AMBIENT_TEMP); + + if (t != 0xFFFF) { + temp = t & 0x0FFF; + temp /= 16.0; + if (t & 0x1000) + temp -= 256; + } + + return temp; +} + +/*! + * @brief Reads the 16-bit temperature register and returns the Fahrenheit + * temperature as a float. + * @return Temperature in Fahrenheit. + */ +float Adafruit_MCP9808::readTempF(uint8_t addr) { + _i2caddr = addr; + float temp = NAN; + uint16_t t = read16(MCP9808_REG_AMBIENT_TEMP); + + if (t != 0xFFFF) { + temp = t & 0x0FFF; + temp /= 16.0; + if (t & 0x1000) + temp -= 256; + + temp = temp * 9.0 / 5.0 + 32; + } + + return temp; +} + +/*! + * @brief Set Sensor to Shutdown-State or wake up (Conf_Register BIT8) + * @param sw true = shutdown / false = wakeup + */ +void Adafruit_MCP9808::shutdown_wake(uint8_t addr, boolean sw) { + _i2caddr = addr; + uint16_t conf_shutdown; + uint16_t conf_register = read16(MCP9808_REG_CONFIG); + if (sw == true) { + conf_shutdown = conf_register | MCP9808_REG_CONFIG_SHUTDOWN; + write16(MCP9808_REG_CONFIG, conf_shutdown); + } + if (sw == false) { + conf_shutdown = conf_register & ~MCP9808_REG_CONFIG_SHUTDOWN; + write16(MCP9808_REG_CONFIG, conf_shutdown); + } +} + +/*! + * @brief Shutdown MCP9808 + */ +void Adafruit_MCP9808::shutdown(uint8_t addr) { shutdown_wake(addr, true); } + +/*! + * @brief Wake up MCP9808 + */ +void Adafruit_MCP9808::wake(uint8_t addr) { + shutdown_wake(addr, false); + delay(250); +} + +/*! + * @brief Get Resolution Value + * @return Resolution value + */ +uint8_t Adafruit_MCP9808::getResolution(uint8_t addr) { + _i2caddr = addr; + return read8(MCP9808_REG_RESOLUTION); +} + +/*! + * @brief Set Resolution Value + * @param value + */ +void Adafruit_MCP9808::setResolution(uint8_t addr, uint8_t value) { + _i2caddr = addr; + write8(MCP9808_REG_RESOLUTION, value & 0x03); +} + +/*! + * @brief Low level 16 bit write procedures + * @param reg + * @param value + */ +void Adafruit_MCP9808::write16(uint8_t reg, uint16_t value) { + _wire->beginTransmission(_i2caddr); + _wire->write((uint8_t)reg); + _wire->write(value >> 8); + _wire->write(value & 0xFF); + _wire->endTransmission(); +} + +/*! + * @brief Low level 16 bit read procedure + * @param reg + * @return value + */ +uint16_t Adafruit_MCP9808::read16(uint8_t reg) { + uint16_t val = 0xFFFF; + uint8_t state; + + _wire->beginTransmission(_i2caddr); + _wire->write((uint8_t)reg); + state = _wire->endTransmission(); + + if (state == 0) { + _wire->requestFrom((uint8_t)_i2caddr, (uint8_t)2); + val = _wire->read(); + val <<= 8; + val |= _wire->read(); + } + + return val; +} + +/*! + * @brief Low level 8 bit write procedure + * @param reg + * @param value + */ +void Adafruit_MCP9808::write8(uint8_t reg, uint8_t value) { + _wire->beginTransmission(_i2caddr); + _wire->write((uint8_t)reg); + _wire->write(value); + _wire->endTransmission(); +} + +/*! + * @brief Low level 8 bit read procedure + * @param reg + * @return value + */ +uint8_t Adafruit_MCP9808::read8(uint8_t reg) { + uint8_t val = 0xFF; + uint8_t state; + + _wire->beginTransmission(_i2caddr); + _wire->write((uint8_t)reg); + state = _wire->endTransmission(); + + if (state == 0) { + _wire->requestFrom((uint8_t)_i2caddr, (uint8_t)1); + val = _wire->read(); + } + + return val; +} diff --git a/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.h b/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.h new file mode 100644 index 000000000..c93351ca1 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/Adafruit_MCP9808.h @@ -0,0 +1,87 @@ +/*! + * @file Adafruit_MCP9808.h + * + * I2C Driver for Microchip's MCP9808 I2C Temp sensor + * + * This is a library for the Adafruit MCP9808 breakout: + * http://www.adafruit.com/products/1782 + * + * Adafruit invests time and resources providing this open source code, + *please support Adafruit and open-source hardware by purchasing products from + * Adafruit! + * + * + * BSD license (see license.txt) + * + * changes by Martin Wagner for tasmota project: + * + * - the libary supports variabel I2C address + * + */ + +#ifndef _ADAFRUIT_MCP9808_H +#define _ADAFRUIT_MCP9808_H + +#if ARDUINO >= 100 +#include "Arduino.h" +#else +#include "WProgram.h" +#endif + +#include + +#define MCP9808_I2CADDR_DEFAULT 0x18 ///< I2C address +#define MCP9808_REG_CONFIG 0x01 ///< MCP9808 config register + +#define MCP9808_REG_CONFIG_SHUTDOWN 0x0100 ///< shutdown config +#define MCP9808_REG_CONFIG_CRITLOCKED 0x0080 ///< critical trip lock +#define MCP9808_REG_CONFIG_WINLOCKED 0x0040 ///< alarm window lock +#define MCP9808_REG_CONFIG_INTCLR 0x0020 ///< interrupt clear +#define MCP9808_REG_CONFIG_ALERTSTAT 0x0010 ///< alert output status +#define MCP9808_REG_CONFIG_ALERTCTRL 0x0008 ///< alert output control +#define MCP9808_REG_CONFIG_ALERTSEL 0x0004 ///< alert output select +#define MCP9808_REG_CONFIG_ALERTPOL 0x0002 ///< alert output polarity +#define MCP9808_REG_CONFIG_ALERTMODE 0x0001 ///< alert output mode + +#define MCP9808_REG_UPPER_TEMP 0x02 ///< upper alert boundary +#define MCP9808_REG_LOWER_TEMP 0x03 ///< lower alert boundery +#define MCP9808_REG_CRIT_TEMP 0x04 ///< critical temperature +#define MCP9808_REG_AMBIENT_TEMP 0x05 ///< ambient temperature +#define MCP9808_REG_MANUF_ID 0x06 ///< manufacture ID +#define MCP9808_REG_DEVICE_ID 0x07 ///< device ID +#define MCP9808_REG_RESOLUTION 0x08 ///< resolutin + +/*! + * @brief Class that stores state and functions for interacting with + * MCP9808 Temp Sensor + */ +class Adafruit_MCP9808 { +public: + Adafruit_MCP9808(); + bool begin(); + bool begin(TwoWire *theWire); + bool begin(uint8_t addr); + bool begin(uint8_t addr, TwoWire *theWire); + + bool init(); + float readTempC(uint8_t addr); + float readTempF(uint8_t addr); + uint8_t getResolution(uint8_t addr); + void setResolution(uint8_t addr, uint8_t value); + + void shutdown_wake(uint8_t addr, boolean sw); + void shutdown(uint8_t addr); + void wake(uint8_t addr); + + void write16(uint8_t reg, uint16_t val); + uint16_t read16(uint8_t reg); + + void write8(uint8_t reg, uint8_t val); + uint8_t read8(uint8_t reg); + +private: + TwoWire *_wire; + uint8_t _i2caddr; +}; + +#endif diff --git a/lib/Adafruit_MCP9808_Tasmota/README.md b/lib/Adafruit_MCP9808_Tasmota/README.md new file mode 100644 index 000000000..0623f214f --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/README.md @@ -0,0 +1,18 @@ +# Adafruit MCP9808 Library [![Build Status](https://github.com/adafruit/Adafruit_MCP9808_Library/workflows/Arduino%20Library%20CI/badge.svg)](https://github.com/adafruit/Adafruit_MCP9808_Library/actions)[![Documentation](https://github.com/adafruit/ci-arduino/blob/master/assets/doxygen_badge.svg)](http://adafruit.github.io/Adafruit_MCP9808_Library/html/index.html) + + + +This is the Adafruit MCP9808 Precision I2C Temperature sensor library + +Tested and works great with the Adafruit MCP9808 Breakout Board +* http://www.adafruit.com/products/1782 + +This chip uses I2C to communicate, 2 pins are required to interface + +Adafruit invests time and resources providing this open source code, please support Adafruit and open-source hardware by purchasing products from Adafruit! + +Written by Kevin Townsend/Limor Fried for Adafruit Industries. +BSD license, check license.txt for more information +All text above must be included in any redistribution + +To install, use the Arduino Library Manager and search for "Adafruit MCP9808" and install the library. diff --git a/lib/Adafruit_MCP9808_Tasmota/assets/board.jpg b/lib/Adafruit_MCP9808_Tasmota/assets/board.jpg new file mode 100644 index 0000000000000000000000000000000000000000..69644e1c59e1ff997f97f2d9a639cf5dd6ad52b5 GIT binary patch literal 134987 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

QU8y~Z}(3ND7)e$MB`E}e{z(iDF2hd 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�t?P(0(nom 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

+T?-|in?1wF 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

@mgB8CCc36&plE% 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

s)C%R`)K=0ZE9jnj?4>0k>wf5LYLwKA z2vwNv^DjpGn-|S1-#U!#Y?sIqrm}{bS&h3D5VnFfUjRby^DKC@nv;94ikSfH97fjdVMzMS< zk!-pS$M`OdO9rMjk;m_8>Fl4R#WjkkY?pN!i-%Key^^G5i&_ED7BY9l97A_2lN30J z@WaF8*SG)9-6{#b6@KgDN1Hk+QK^4`+Iw=ViTjNU%A+znzx4kYkq1bd#>p3DAWIVZ zh4=`IfrjPyO<}!})Pz^TqdsH$bT4DV(#y z_wsp@P=i^5lrREbFjGfc<{XRkTunysvu9WpkBP*{2k5@8y*^~6=~~9#-aFNr8Q(Fz zzeS}mzPZh*FG{5{DVaoE+5A)+@T&MmM>BZf{_+N&Pw>wE+6jCF8o-lOSN3*hl(ygy{>JUUUwu%%)A+B8 z4SmUF@UVWievC{ZuVy}aA+_W&3RaRhme+hQ3C|DN3AL1X9o7~R{f+%S)=S58^Gr@l zTeex9F2{pN@R7yf?%LARodu*S<{t(R*A6F1gex(My zOtV?b+O(1~K|@=y`j`1bQa4mq*WR`IBtQ@o(ewGx5gCi%?~d+q*7a{T)O*7{U`KcO zxe@vx`*>4k;LKL1kczme(DoA`%-s6bjp`{}$`~>}? z1y7F08Ot4nU2Zr{+b7feL9lEc;r-miBiPDO!1VI={I8^##{rTChknnLX@8loYwgSm zuqE2#xTsI6@=9j47Rs-aTl3*lM*ZC1PX@8v8RYcG^{sBG9cfywSd*H99TR{1_5$LS zXYpx2DJV$vebd&aiENy?&;vP7+6QBnbX;mUqKthET~?K1_{p>B`_F=SsP~Tmb229f z(n$~}qcJ}t>J5)%9#K_Tk2t&`?gjl|6dkeikVifc_1lw<``{@0CEm?~&~)L>X@PP( zFqwa(AqSIMf>bjQ6?<>l4BC|1WUsc>7jsw z=@j4Z2X+IeFFVRmT5F8qVo*FWT6i_qd-O|c7F*2twnXm?NMm#9jRr_AEr3HmJ0R$W znsnWfa3~44a8C~)4@Hf%+~p0hC}t#I1i=5}>zED2G~3Q-W)mdP+Xo%nl|U1EODyS- zTc{Ubiz+}QR?cB}D(=OKWpgt^@_KhJdgl-!lcI+gsc-zAKy1>#4i!$HcYAmzs7Cuo zbFsgXD_?Y(q-jV4x^u4fO?-xc}(vz;;M!)-eX9TXk~ z`J^f9wXV+D!CO*gf+)%9xdc&Ea4qwW-;p^cY1QHB^OI;_zRL?wD-CWr>tUG%m9SQj z9afJVUMWwZx&3A%c}7ED4ZA()lsV^R2c_d}O%p!NuD>@@T$?lG~hf zO7bSKM1jBInm4a+NUEIIJs~I(Iz{KHO?vExq3F1XzQykdIH<0Wb`DRsP|tx5!+Ur4 zNT9l!+e+Ct&*<=eI)R_adBYXduOReoA%ZAiTV%j zu9)dYEc@PKDWsmR_4^N}9nU=nwIf$_mtUjrWYxwjyjChloN1p~eO(`kR<4j8s^Ik= zX98XCGY1p!5XX}{(qjdi zU?+7E>54w?4rbXlr6AX67qREQe47+sy(fgJf*lVO!#1!9{(_*x)OWa>ee!sh-of7O ze!c>^OijAPK_?GU+6L3b*?vdpGG=)=?Nw6eRFs_)d4{^8hOeSv_-?DU2Zv_>XqvvV zdC>W4WUa6V1#Fk>T*@nl+O*_%?w4ett+Ubog743&T%73$WQ3>` zmp>-fK_cS{!J;2G1lwz0EBTe2Kfllt>RbBKgKxPqu2a)?g^DI!OJ5Jx092hdA3IH8 z(wm;lAHg6DyW9LV%oLS8oMi7GjL44grEv7tO7E)ult}-A(b{@toKhpdzBom5)$mqb zAAoj+A1aw&LAGQ>kT-UpA9ud%**~;T{s_A`uZ<29XGM)fTBuZ9{dPOmzwy;Eu2%lL zvvrAhX`~_j#wqdlVg0FPF)XcwHeSoH-J=VPu_~?t{KrAagUBk)BaY$x!Rnm`)Zo+N zk+fYd+IS;X)d7p6Jg6{NlN3mC`OJ#L=V-#o51CAcEtPgV4gjT=L57eKjLnAL;M2Q!x!ES zv~kf$gbGJajwD)ZnXo%VjhR=T zfCoRv4@P8oJy1*+k z^<>bKj`+9*3{$Ua@vT!5(U1BMAdt|~!9k?F)V#0P7ANfk3NcjtQ4r;BPS z-smORKKbRR?aOdsZS%dQUDJ{mIPd5VGu3^~& z5v$kF(olTD1IW?&RDe^F^iR&q7x46Ptu#Fi13^p{M*B?6B&*HDZdB)Em|lvKQ>&KV z!xHQ3sRdSgRytBYNn*24w#}6TU&fa>YhGet4=(;!Z<$>7W`^^i^Q@f*$=AOy8mJJW zg^n3#I%sI??~hL(jG?W2hjgpQ4#y3)c>Z9KuO1s319;kM)@lnIqy^TOz@Lt~l;)h* zOeb$$aKvW^PO)_lm3~;p=kIHoq_sx+`t>Vcdr*Am55P4BfS^d{(Z>lmA&bFzy5ycbUoy z^%~J7FC@n&pu;?Sdm^?Bi>{~&<35i$v%LX7WR!HF%1-BXl;6_0Eg#ijV?lQSR&la+ z0wPrfE#M^1uvU)>vXE-e4kD{z6~H@tHO2!NIX1H!yZZ(m&wQeqf5y1108Cn^cMtX0J{}6JnZb%U<0;LCxPu%rr(kfnOvhnB5tOP!zo!j}i!zLh^q9-{(Z- zvZuHzG1!KhLsD)se%dL`ME%cdYTx=JQe2xq=Y+2!UjCN1WZ=v@vnj)u3`|;d>=3$N zqRD$k=g{xX7s#hXej5>Xu36T@yBvgTWM`I|WYQcLNbnibPbM+vMz!Or_{6j6$lH@? zgMuYaml_5hNCuAB%8PxGW~j8W*{@AR)B?)Nm1~wc1PtwHUOIJm0W5X-(jd<#k?I9{iT9D#xVFEm5lQ@PS%$p+;XFQ(gkhz=gCgXOf)S z>H(2n`_4E=28)Jj5j}UFSdVn<{3IVBkG7b7rSMNi`^R4_XpUj|1iiy2r2@J>FrWI# z8P!1&`)g_U>drLj|NQV$vnIq|i7d?kcf4w)!OqZh0`-;|4ySMu_7rgcK>=5;t) z=PIW(E_~5WvwKWJ>#IsH(IY_qK|nUWX|aP}Gx2QeGPBbs<7+a~YN~w@7#}aD5aph= zF=Q*9DfDq+fc;X0kf}+WTC5-^? zHP3vUAaF9~+P}F)e1n?X<0?|a*!mhr%8#wz(@X#ofV%YU!YJZX+eUpm4I^KFrd zESFRj&ks*+!v2ms7oTA$9hvIM_Qu}wA6%GW*!%P0E>a@7sxE&qL5%U{Oq8u84U}06 z4r;nt83429>8;i4l-!U2GW17+?X9n804$&jbFxJuNj!?DgBl8u`%h9u<}&{>#ljPh z)&{WI6VSl*SN5czn6deWpF18%!`Ac1ty24Ykqfrt=cRuwS=a}GBrb;M|Jt%q+|Z)^ z#t2VL^-{$yx?Sh5hPXW}u6aB&)V=)45F#heIuC_UgZ&Lnjf9sef(C|~;lbh=2&CC}R zbO-jko#zBcz$K6#b?F+h)^-c4N$Ot7Z__stt;QAHc*|leH}~G4=CbVsS6Jx1W@Q zp!yW$+|Jbam%6}}@V&|>LRV12O2O>;6uPDG_R#00ff7qLV3EFx1!x*Nl*uWf>s7;<#_oiSi@y5!#GongCB$ zgicx!&0{eON9u2hIguYPw8?f21j{*69i&o-NJ^;<7tYEU3;@L$V7W2hd=_%{wOiGs##Z`;IQD zphdwvwu_PXkV;6>TK2)USZU!G0!FBin&JfgBLwXGY<{c5*b zb1=k7<}sdf{8vx<-riMEa3%}vwHUd;u=l_|id*NBANQ}zCE406o%a(_NfIv@cZpb= zZwv5uxW*Y+IjlaH0-a(;xad6W3ofzZ*BU@@K`-+CGmYT zjWe~Kcb^GEPV`SP531P7rsT4Qv8pqBU9gC>$-*NfQ+Bl`1FWdK!EUZbX&U2( zbtxMEX@KMCAPJ|z8z^8K`NLOfMM50{HLLj$;uCH-G~KA{ED`h3>Gs(N!R(8@gLoO_ znfgtQkzWdlky1^2yYJMz@RplneAqAeE4M5v$-DE{e?aAhRsI2V$6H_(HDPFJY1{wkO903re=A4Q*`s=ZC>89K1oca1GiJO3S{DCasI49- z5;FYi7d8IqWS;AMczxSnVENzu?NVcJ-C8Y4`ai%ZL?0zm5_}=)@8ulpqejS$^9gh@ z5T0Eek!&CjLZ`VRYE;1Gx!e{fM0i6ruS4846ZyV|254xXZ0Kbhm9`jNT03_M*V{oA zoMmA}@%WNk(woim+3{9}ndF7zr<9bq6(aYGnX@Lk>>2V1{*~5#!}#VMchJR->5i`` z@*}G`oI<-9{x?EGV`t(0iO<%bD$>~RGaV$V^W(^ua|3zNGq1F`s_0w(tmBR3=E8Ac zf1U-l6FI;K{HJw@59btZ z$$ZgG&rCrO`m%x(AFV_lFT zW1+sC@M}+%2#(R7yHkZ%9p6UA{HCNXdmPVnq%62Jj_H5`7uPXdyI$K}p7;_9mnxd4 z0T7mq;{=fx;2&}`rEL}M`Na=x*|TL~niB2BZpfKV^4aoBW#O)ebLxVN0O_{ke}JZ{ z!d#NizIwvuri}#e1*8PyK90LI4k+J2OcDpvPvZ^TCFUrFhG1Uf#C*3+L~2M$Q5j(`um=NF;EOCEOPy6jdte|#(%MTC^9ylT+jb8=u{a**JxI~8JhDN;Rai$Qn|%H5RcT1RxZtG5X|p@CfmubJsaQ+>vlpK3H5`4f9k=UOZWZ4<$F!I} zAIk^HAo188PEc_!IQ;kF;b}LJ5RR_W}PmtUyzkryuSW>@_sqJas%sh+Fl##u7i+nZ~nrj~a=y2^%gb=e|{AkvQ$ zYdAmW*D?2|gra}m2~bJKV7x?eJR#QY7nU` z%HJIq(?YBTqszW0?o5ScM|YFI3TtJ`#aes$*~udymHIZ)if32&vka(!$f<74XrCGF z6iKA??(-6QNrJXsYE9_|>GR~v{g0YgPfwlonxVY&vxF(#&S$J{;UxzK?gZ}j{R9{D zyk&2iOj}z`&vY=KDa&ANO_gcO2M$p6mYXX&1-ti78xdg2eR`>>N&?!b&&w?Grnj;a z+L!HPeLa(sCgzV86%|RWQh_hgq^kQ5S>2a|hVLyU#RCelY-#3_U#o8H#@4f~rBa$8 zdRg+!k4?uD_C}sbvRmg!MTzkTvi|@tn=&Cio0AjddQ3V(ihEn^K-@jSHKL4m87dlW zEHgsPfX|O>g_SV_ufV>kjsGf0i+*2zQI5Z6?E6~bUu=0i6RX=nJT?UoZxvVnY^{(W z*bzbce%@90i+_R_ExEcCBsU$Q~TF8wgP9+(SZ=7;apxJ#mcB~#^Wo;3{YL@WMQ^Puq8 z+Nq%W-5H*{_s)Hgaf@mk23%s@ zS5h3Of*n%w?gg5?(0q|DI;&MMugO^(l@yrp1Txpi;r`byf{Cg>By{waX#8S^wI*wK z>j_yr^~DzHhbxW8MIdPuK?|%kUixe6w=j?7XR=uhPBmEt1*+`s?ap}8lvqtTMfsRa zEt;Co!4lZ}wkyYjnBZ25R3+$Rfg6B>7dsP|(w1Uvd8 z1K}M*mws|VW}vZ*TnuWJxAJJgRMr&dccp~2m&J?arOXJi^xL!<*?CW;^f?(BjiE!G z8$HS4*y5%`Me31Nctjqf2(A8gJZO?ZU&>5TBRNi|GF(xzuQZ=HZz%X^KS|Z{ z?>rQTVKPR8%-2a2o;mq(#p)kd=TLAH`h&{?F z+jRa!lZ-*xh}4_>79IK9Sr(hY9#8m-BbY<29PbOAw+EsXZBx`~Al%A&IC=D0P1b^+3!NqV$6@ z(#AwtB?9i17pK=PwXHzjD{09N>(A5182TJ1@Dxky*N20TmPX~m9e=D+Qt}0x2TOoJ zdan(&OsGIBQ-jh~1;NfGxCMrRjPp?>o11d5pWswr_(JReuvLW-l@(-sULVlBu;sPF z>xZKl$8}nY^8p`l2f9kIEXBhwnvd1YD;(ZPefWewPEd5!s2rw}fQ#(I<4S!;l#s4V z>*#c6wY6ZZcb@TTyS&b%2D-J~P&aYg#56Ai3`to?+acQkYbMB2WJTq{Z}v{v^Eed{32HW+pcI zH$Yn;QOs_;s*>`!A=R}F%WNe)ZHK=r>Jt?;B^fS@c)y$sR0HA~H+@l=W{j-2&xbEp zsS2kd_l)~c)I5u%e*l?>O_|LFho!YB&ElrG*c+d-6a$c{yU8_{T8)3V4je%$>E*f~ ztkJy&zGkAm7h8EVFj}5;y^WG7D$g0ZAtQP8f-<@@Pt*6{$L(2fg~+@#36t(|c`}Ca zpA49ALD&ZI_OYprmj957g-b}8{-U+ka(^*9P(tZf`r70>GgW_)h*iqF`pF}GnhgHZ zlDjX`0i1zu=P-MjoXmxX_l&J#R_244$}UPTs*!%K>f6ab45!U8Oiptwl{zIVcz=zv z`@sypBlKpzD!WH5_6|AfGwVSTZuBqtV=CZATKrr!m{1N*ohMD#qZG$Q&erN38~m6J zh<-u-F^pAA;|j$3m5Me*ILHYZ>|(unkIL$+_vnzYsR53gi=_-dR#?_8Ccc%YTb1d0 zp2RN%3uH3hKX?0s6PaG>9yvOn<{yo!ualiY0(FDgMxrR#^=aKEtocaw3w$<0Vqx^y zRL0l5+9<~XUWHZ!W{cH__r|&Xj3)Ux>BR*vj7vo-)oaNRB81c@{N&LziJpIeD0^^a zDQ%WKC?NwypP^sr9|(5Q0UJjHcM{$vOQdJ|W~C{Os-89Q;s~d%-Fl%-@0sslL3_3z zT%OMc49oI-x!qhiqT+%Nw_wHH+J#lH6a+7ctL$>t{Giqw`BSb3&(AFR1Vr9@;b4i= z4fC<@RH`l2t5&-L)uKP@M;WRV32qg87r>2$7i1@s(~mbKH_1|~J%Y+F3AwKZ6fQtL zB)Ka_jVrxO{>P-$$a@NMJt+(RJB@`Z*ZGau`=jyE-1l>#d+jthNOl+NqCW!GYHVP| ziW}dHtLCLH;u&J}tSgbSgW|my080CDsqqy352+r55AV6-iX64f{NZAu-oN(44kZdT z-rO2-@hkXb?Bn`g>4Ne4uIIutzxOJH7K=@;xq4j2v`KI8I3ZERG%k)6QF0xAsE-XB zlt#xGICS%hJx~pBfb&dcq-`$I7$1cm9WEASlOun!HJ-hJBa=Apz+)?_>mlQ%TH^5L ziJ;fK(_0Uo+gM9@8D`KcD?YYXa{d8c!ETufiL)L{Xh1j8w!8^1sjY~8>uHH4I)8n% z(hGR~^X^_es=r!c5uqr$B&+{3uXbS&oCNd`d7`@+zOLaDF4Va1?Ryyok3u5f`JqTJ zl>#XD(Qo48sSKw>R>yBx_-#?M;?gT7VNv9-nPQHXuI_8hYH5|=w&u0>x&m@O`X9mk z-By-dUi!W2+UARLRc`KKI&9=KGSrwvS}g~7+}lu{AK+Tv``71j{^=F3eV5a0Gc3Hi z4!36xK8wWTfjpCpTDuu)*7*3GDYpbF&jN;1U@g;)#EoX-j52e_1kt%)9mgSI}-!z?l3?Xi>CVPm~*$d{dK7ctdEjGM7|r3gNe@GaRaszZ zqr8LPE!S=Mb$7J-)$76zZCcmQOGTl(#2J%aYZ5s`IX5( z;UuU<0-Ly{kAk9m$z>)j(gL2}mv^4dw88X$AIc~NjB6M-UQBT;CvqQo%2LC{UbEue z$esE2STh(4i|})vQW9ml+-QUw2Y@QE5=edBt}T3ehbJwjYyRuq!e`>#W4jRddnW1c zN5E{I=BjUZ6;*OBz$EHplTA!w#jvfNta$spp6)2bQV#N0Td|g!WXc_4J7kaa@xJcMwDg+AF0~tvAkZaBg;v8u1rhX5O!K=E8cLr}3%j;6Y8`0o zxUlXd*5c?x&RTm9(5mkvvVU-1=NSq@{^I6W4?tizf`dBL?nnKgTE<(X*t^ga0!)O7CtIPD9-C3CWB@B5(@e z$3ZyXJyM~GnT|B&C{-X_L#~;>(?l)zIL_jJ8slC)O%wH(`?YRxxB{uXM($tiC1AF5 z=(=*Cu*7!#bXT$KDCyu$vBkn}9MG2lHO=~9K1nUDRfA~4Jenc zEwcD?IX_3~w^`4~!YxSoRJ7I1lPwD>xxDG#sPtL&-h>b-v3`)MG}6LecLa*K*AaNR z$R)|sX6cne>gPFx!dEj9C1U&jcA0dp;8<6%Ubn`7*}MfJ3%kijH1E|DYnM_n0bJh? zcL+LAV&)t-&C_|m7u>=yFv4lAmhmoXVlDbEqwW)0fJo*4{T9#!2k(EdRLL_*=NfYQ zMWyFZNC{?V7C6)G8b`fhIoT9KRV2)c42ET;XY+S#I^&_6C z>hGz|VN?_M>MXObx3K4KN=yf6;kHw6P7Tt+xZ~K#A~_mF)bX>ZQmkZq3Q_jEBMP#P zy!zezLiO1gDP2kxZv=>PuZB+k0=AK}By}P5ssb|G#C_*uv=tnh+G$7f&0+f5WJ+jN zpGTx9>w3#ESk?;r;OC`_j@h^^vICD~it4gIwXQ|e_nA+*`mW89FJTU3pTZAL6&Elk zIvCCO2H-bDx&Pw7+0z9;3(l4$8gc~$nfbWo<>CR$!2J1IVkpW7W>Jnx-7`SWVBUfA zr*!@SCW|_PQGDUt{Vv<3C-isycXYPrFHN@XLbZHUmxf7x94{`~XO(}hxS2V@yPPRH z5V)jk4rmk@A{Kw~c}-y7b<~CnTw7{k1K6CyGl; zRoTMJ_!pM0-R#?DTgK?<-~H`;I#bQ~y5M+mIYKc$(<649Uzow0)b}WpcX9uP zakiU&d-Ln=)$0FUUQ5%n3N?0D!0A=NcwLA1Vd3CBqj$YZiIvl?TGGWX0s zh%EUo1kFUnv`+0+W?80plA9OKo3@UZaXuRZq`^kQI?ZW?p@IoP#HMx@Edd4AZ=UHG zgqSVr16h8A22~!zxhZ;OK7dECnP+eBv4=Z4Q+8%a@w0n7QY!fJc~Ux4y^=6n(oJ)#V`G+A5q7fxFS9&ad^CtzlX6v23Bc|9}3M1pBNG?suU(9P@IogB7wp zlq(~#Kkmg=9#epyxadNs{1=efr@HLR7nSyTLWAb<=eX|1_G$C@ub7HK6}lxsDsFkg zD0jKJZRkBX2}p*sH-y z1ypabdeBlYvBRJ#8RIQN&R`w$SwfP0Wu8|PmU6D|$Bt>5Zj|*~RI5)|pZ*nrnK}N+ zE&F%8CcRh-T_?AhmL0a#&{=U<*%YDfL&pq>=8u;hMPe8nR&0s|@%E;uM@|Km|52s&%Lga-qlVq$gatN& z2LnL}Z7H^w%MjoD?y*4_@hrgZP`J>+IqS&#FSh71_*Ws%*y0ny85^2s@ht|yJE3cx z0e4@7yRK%qZ3iIyZe)LM+2#uC%K;~Z`ulN5#o-=>3@ z$MT8oWA9pS@>dL^3?+-{af4I?pA(7a@r(9K@ zGuS=hFo?y;?5ci4I7C==?Ah`K-M5aeGs#t!VR4hixSRfpu^QZJA@55``@&^O(Bj24}uA!5@B+Z|d2UF}mIXhaT;$f}59&6YlZ z+%Dh}Io95pv=si=Id4y)L`Nb`?ZNg|a1w*pNJPlZWM*Oh##b=REA1OH>ESy!s@x8x zItM0Fp1{pE`W83pC#|PahStE+UwL%@n_BUBX3^A~RzyFQA-=8mcc4SfS-#&`q4n>U zfuNZJfV=rRm*9UUo_lN!T{`0#&mGx4sgk_=g%*rlO#sq)n)6aM{W8^)l;}Kr&GudR z8TyK;%^RGR27X%N9&V6Ad#KwN5=AMvcv=~%jU{i{o7}7qTwE=O?Vy5pic~IUlk`ei zure8~_VcJ*@twc>udGy-MD+9)=BXaNlE|3}knktT_iVz3(y1h7!tgbIoWr0bsyY$4 zH33fU3}AOl?9~-mODOWcE6;$mtb@7FspD4&$E6OJ3~mF$58u}bN>`_IwwyKb#y%F_ z&tqF)#v<q(f>4Pw=R#HgKB<5@FycZpEByt%w8D_0v zW)$K^3L4%3`n^+@+9O{P&Z_f1gL%mLv9(=JD4hifSEF8Po)?bRb(#{aFzwbtn8K?i zKH#d7E>v1Af87?&+Q4085I+1QYa)^q z_23KoQR4p&^>!Qpzib&k0Y4owK@p~V6v`%8xi5Nag zUu_`Rgqh+Xm&*~Wi;-25IE+?G5RR9M_n1X<>OR1W=1~d z4uG#uWq_=jZT`(o3Fr?Noz;Mz>TusZ8~7x*sGZM!^P zvJ1ESTkV-WG$t_Jc;tZuNuojoW+`E+plVZ!q<(>7K4o1hX9GBR;E~+?7RlwVz~F2VM`~Eyn^%h4bvhag@NvR$H=3|x@FOauH=9=ArY|N+#j1=2Tk!_*J-tHJ zhf~7S8l3-fd6Zp>8WJ!C$fhcZ#Rcji{JCIdFGaZ7uJB|p0-3g&-Ihprj|@%LFMyrc zc_a}3LFDj#=`~;To$qj{nz?J*rb~;#OkDqgm7p)sHbI+LSk(8Eyu?U1Qg`rp|%jSeqlV{b+Yfug=B8{kT?$1 z3pXru4uhju)Ju|&sIP7WMd@~n$r!lN^{88xC`|e~0YD&DDNL3#lYUdGvti3wKZu;s zV)@^+_`{}^;zzw=!Y;zsCelGpV20jOW7*eJ%iKJytwc>sQH1{dn|GEn>w0?6ZUHY6 zd|WBP1~`QwV)L~x3ox4_oXKjfhtf-~LObQwE*eR-l+pHwpkB|XtM6L!3u1_A7F9E? z=808IwP!J~KlZUcw3huaye-Q|gsp3RRBFMJtZy~-LC)~k*4|S>@}-YPG?8BDAFdF@ zAmquJnEj_Q$*@fC-8Z!w8p5s<7h{6@H>5r$Kkk=p5762owzKri zFA|rn3Lwxdf{#hRkNv8Xrgttza2vl5>fsku>@MFaJ2T~l8+q`*lX@BR9md)Sq^OCd zfp_)XO*j;)D6R1v{N|49=2F&k-4l7t>I6-6>IWyZvDwYVhi;il14H|-j%1Al(ZI8p z)l^Yjal?tAB!~~s_Gw_~idu9f*YOH~^;DzyMVN)<4K{Ucn6D3v)PT+3Af^LMClc2*aM)2SIV9}g_=Buwp*u;Yd-mFxc`-X{yOsR%CJGBe+};|WfoWyR{xgl%d)*six_)RG&7}x| z{$!Nx@<$CgyqK8Y=ASCWhYDrdp_8sCD;KbB0R^wfQ9)mAZ-vh-OqKVn%OBprwmi77 zM`=qXoRfwwZduWtBymlr6Kv$sI02sYfF5`wc)Ba2Y0uR1?jPlDoG zE0}iS_uB6vbQ(Y^zoDE7^V0(BVS|3ko^W%E+qjyITRki?m&6FIG#Qd!(|(Wt0zdb= zD5kE~S7Mw-JH6 zDXZ?brTejuAW&}$nZ8RnJaSi1b)SA}&>&Q< z@c#t_1N;2VAj6>MpG_d1Q2qrTjzcoX3N0Cs*7NKntKfJFcay2ob#`|(Hx z5Kqs#h@@hf+dTCQAt&qmtVl|Fdi^Rp1J@Mfs$qYJ`7{E?Nd!LIC>c6B`t?FV0SHQ; z9aOP11_SUsT|<*(z&ePsW6Fq#__L6U*iX7<6U4Lmbw^|o;h6jCrak6y;;L&-CdaQn zEXV;OnG@Y=;AX$DSz?)Y%o_O<$e34 zV9wTT1#uz-)5qV#bgMIvWLIC{q#`OKxbPM^*x@_#Q3?A23+}~>p1SzXsYMD&JbQb6 zP)x4@Kb~k@CUi0PR2jf@O!H(ywXYu^mY$Rmi{_g<6$WOP;$z@N+{!uBp5G. + +On the Adafruit Discord, you may send an open message from any channel +to all Community Helpers by tagging @community helpers. You may also send an +open message from any channel, or a direct message to @kattni#1507, +@tannewt#4653, @Dan Halbert#1614, @cater#2442, @sommersoft#0222, or +@Andon#8175. + +Email and direct message reports will be kept confidential. + +In situations on Discord where the issue is particularly egregious, possibly +illegal, requires immediate action, or violates the Discord terms of service, +you should also report the message directly to Discord. + +These are the steps for upholding our community’s standards of conduct. + +1. Any member of the community may report any situation that violates the +Adafruit Community Code of Conduct. All reports will be reviewed and +investigated. +2. If the behavior is an egregious violation, the community member who +committed the violation may be banned immediately, without warning. +3. Otherwise, moderators will first respond to such behavior with a warning. +4. Moderators follow a soft "three strikes" policy - the community member may +be given another chance, if they are receptive to the warning and change their +behavior. +5. If the community member is unreceptive or unreasonable when warned by a +moderator, or the warning goes unheeded, they may be banned for a first or +second offense. Repeated offenses will result in the community member being +banned. + +## Scope + +This Code of Conduct and the enforcement policies listed above apply to all +Adafruit Community venues. This includes but is not limited to any community +spaces (both public and private), the entire Adafruit Discord server, and +Adafruit GitHub repositories. Examples of Adafruit Community spaces include +but are not limited to meet-ups, audio chats on the Adafruit Discord, or +interaction at a conference. + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. As a community +member, you are representing our community, and are expected to behave +accordingly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 1.4, available at +, +and the [Rust Code of Conduct](https://www.rust-lang.org/en-US/conduct.html). + +For other projects adopting the Adafruit Community Code of +Conduct, please contact the maintainers of those projects for enforcement. +If you wish to use this code of conduct for your own project, consider +explicitly mentioning your moderation policy or making a copy with your +own moderation policy so as to avoid confusion. diff --git a/lib/Adafruit_MCP9808_Tasmota/examples/mcp9808test/mcp9808test.ino b/lib/Adafruit_MCP9808_Tasmota/examples/mcp9808test/mcp9808test.ino new file mode 100644 index 000000000..6002487e9 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/examples/mcp9808test/mcp9808test.ino @@ -0,0 +1,69 @@ + +/**************************************************************************/ +/*! +This is a demo for the Adafruit MCP9808 breakout +----> http://www.adafruit.com/products/1782 +Adafruit invests time and resources providing this open source code, +please support Adafruit and open-source hardware by purchasing +products from Adafruit! +*/ +/**************************************************************************/ + +#include +#include "Adafruit_MCP9808.h" + +// Create the MCP9808 temperature sensor object +Adafruit_MCP9808 tempsensor = Adafruit_MCP9808(); + +void setup() { + Serial.begin(9600); + while (!Serial); //waits for serial terminal to be open, necessary in newer arduino boards. + Serial.println("MCP9808 demo"); + + // Make sure the sensor is found, you can also pass in a different i2c + // address with tempsensor.begin(0x19) for example, also can be left in blank for default address use + // Also there is a table with all addres possible for this sensor, you can connect multiple sensors + // to the same i2c bus, just configure each sensor with a different address and define multiple objects for that + // A2 A1 A0 address + // 0 0 0 0x18 this is the default address + // 0 0 1 0x19 + // 0 1 0 0x1A + // 0 1 1 0x1B + // 1 0 0 0x1C + // 1 0 1 0x1D + // 1 1 0 0x1E + // 1 1 1 0x1F + if (!tempsensor.begin(0x18)) { + Serial.println("Couldn't find MCP9808! Check your connections and verify the address is correct."); + while (1); + } + + Serial.println("Found MCP9808!"); + + tempsensor.setResolution(3); // sets the resolution mode of reading, the modes are defined in the table bellow: + // Mode Resolution SampleTime + // 0 0.5°C 30 ms + // 1 0.25°C 65 ms + // 2 0.125°C 130 ms + // 3 0.0625°C 250 ms +} + +void loop() { + Serial.println("wake up MCP9808.... "); // wake up MCP9808 - power consumption ~200 mikro Ampere + tempsensor.wake(); // wake up, ready to read! + + // Read and print out the temperature, also shows the resolution mode used for reading. + Serial.print("Resolution in mode: "); + Serial.println (tempsensor.getResolution()); + float c = tempsensor.readTempC(); + float f = tempsensor.readTempF(); + Serial.print("Temp: "); + Serial.print(c, 4); Serial.print("*C\t and "); + Serial.print(f, 4); Serial.println("*F."); + + delay(2000); + Serial.println("Shutdown MCP9808.... "); + tempsensor.shutdown_wake(1); // shutdown MSP9808 - power consumption ~0.1 mikro Ampere, stops temperature sampling + Serial.println(""); + delay(200); +} diff --git a/lib/Adafruit_MCP9808_Tasmota/library.properties b/lib/Adafruit_MCP9808_Tasmota/library.properties new file mode 100644 index 000000000..3c4ee86ce --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/library.properties @@ -0,0 +1,10 @@ +name=Adafruit MCP9808 Library +version=1.1.2 +author=Adafruit +maintainer=Adafruit +sentence=Arduino library for the MCP9808 sensors in the Adafruit shop +paragraph=Arduino library for the MCP9808 sensors in the Adafruit shop +category=Sensors +url=https://github.com/adafruit/Adafruit_MCP9808_Library +architectures=* +depends=Adafruit Unified Sensor diff --git a/lib/Adafruit_MCP9808_Tasmota/license.txt b/lib/Adafruit_MCP9808_Tasmota/license.txt new file mode 100644 index 000000000..f6a0f22b8 --- /dev/null +++ b/lib/Adafruit_MCP9808_Tasmota/license.txt @@ -0,0 +1,26 @@ +Software License Agreement (BSD License) + +Copyright (c) 2012, Adafruit Industries +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the +names of its contributors may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ''AS IS'' AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 46d88e0d7..0d968b1f8 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -518,6 +518,7 @@ // #define USE_AS3935 // [I2cDriver48] Enable AS3935 Franklin Lightning Sensor (I2C address 0x03) (+5k4 code) // #define USE_VEML6075 // [I2cDriver49] Enable VEML6075 UVA/UVB/UVINDEX Sensor (I2C address 0x10) (+2k1 code) // #define USE_VEML7700 // [I2cDriver50] Enable VEML7700 Ambient Light sensor (I2C addresses 0x10) (+4k5 code) +// #define USE_MCP9808 // [I2cDriver51] Enable MCP9808 temperature sensor (I2C addresses 0x18 - 0x1F) (+0k9 code) // #define USE_DISPLAY // Add I2C Display Support (+2k code) #define USE_DISPLAY_MODES1TO5 // Enable display mode 1 to 5 in addition to mode 0 diff --git a/tasmota/support_features.ino b/tasmota/support_features.ino index ef757e5b6..5622bcde9 100644 --- a/tasmota/support_features.ino +++ b/tasmota/support_features.ino @@ -569,8 +569,10 @@ void GetFeatures(void) #ifdef USE_VEML7700 feature6 |= 0x00001000; // xsns_71_veml7700.ino #endif +#ifdef USE_MCP9808 + feature6 |= 0x00002000; // xsns_72_mcp9808.ino +#endif -// feature6 |= 0x00002000; // feature6 |= 0x00004000; // feature6 |= 0x00008000; diff --git a/tasmota/tasmota_configurations.h b/tasmota/tasmota_configurations.h index 8ba54e60b..811076d8f 100644 --- a/tasmota/tasmota_configurations.h +++ b/tasmota/tasmota_configurations.h @@ -154,6 +154,7 @@ #define USE_HRXL // Add support for MaxBotix HRXL-MaxSonar ultrasonic range finders (+0k7) //#define USE_TASMOTA_SLAVE // Add support for Arduino Uno/Pro Mini via serial interface including flashing (+2k3 code, 44 mem) //#define USE_OPENTHERM // Add support for OpenTherm (+15k code) +//#define USE_MCP9808 // Add support for MCP9808 temperature sensor (+0k9 code) #define USE_ENERGY_SENSOR // Add energy sensors (-14k code) #define USE_PZEM004T // Add support for PZEM004T Energy monitor (+2k code) diff --git a/tasmota/xsns_72_mcp9808.ino b/tasmota/xsns_72_mcp9808.ino new file mode 100644 index 000000000..42fdd7ab9 --- /dev/null +++ b/tasmota/xsns_72_mcp9808.ino @@ -0,0 +1,136 @@ +/* + xsns_72_mcp9808 - MCP9808 I2C temperature sensor support for Tasmota + + Copyright (C) 2020 Martin Wagner and Theo Arends + + 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_I2C +#ifdef USE_MCP9808 +/*********************************************************************************************\ + * MCP9808 - Temperature Sensor + * + * I2C Address: 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F + * +\*********************************************************************************************/ + +#define XSNS_72 72 +#define XI2C_51 51 // See I2CDEVICES.md + +#include "Adafruit_MCP9808.h" +Adafruit_MCP9808 mcp9808 = Adafruit_MCP9808(); // create object copy + +#define MCP9808_MAX_SENSORS 8 +#define MCP9808_START_ADDRESS 0x18 + +struct { +char types[9] = "MCP9808"; +uint8_t count = 0; +} mcp9808_cfg; + +struct { + float temperature = NAN; + uint8_t address; +} mcp9808_sensors[MCP9808_MAX_SENSORS]; + +/********************************************************************************************/ + +float MCP9808Read(uint8_t addr) { + float t = mcp9808.readTempC(addr); + return t; +} + +void MCP9808Detect(void) { + for (uint8_t i = 0; i < MCP9808_MAX_SENSORS; i++) { + if (!I2cSetDevice(MCP9808_START_ADDRESS + i)) { continue; } + + if (mcp9808.begin(MCP9808_START_ADDRESS + i)) { + mcp9808_sensors[mcp9808_cfg.count].address = MCP9808_START_ADDRESS + i; + I2cSetActiveFound(mcp9808_sensors[mcp9808_cfg.count].address, mcp9808_cfg.types); + mcp9808.setResolution (mcp9808_sensors[mcp9808_cfg.count].address, 2); // Set Resolution to 0.125°C + mcp9808_cfg.count++; + } + } +} + +void MCP9808EverySecond(void) { + for (uint32_t i = 0; i < mcp9808_cfg.count; i++) { + float t = MCP9808Read(mcp9808_sensors[i].address); + mcp9808_sensors[i].temperature = ConvertTemp(t); + } +} + +void MCP9808Show(bool json) { + for (uint32_t i = 0; i < mcp9808_cfg.count; i++) { + char temperature[33]; + dtostrfd(mcp9808_sensors[i].temperature, Settings.flag2.temperature_resolution, temperature); + + char sensor_name[10]; + strlcpy(sensor_name, mcp9808_cfg.types, sizeof(sensor_name)); + if (mcp9808_cfg.count > 1) { + snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%d"), sensor_name, IndexSeparator(), i +1); // MCP9808-1 + } + + if (json) { + ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s}"), sensor_name, temperature); + if (0 == tele_period) { +#ifdef USE_DOMOTICZ + DomoticzSensor(DZ_TEMP, temperature); +#endif // USE_DOMOTICZ +#ifdef USE_KNX + KnxSensor(KNX_TEMPERATURE, mcp9808_sensors[i].temperature); +#endif // USE_KNX + } +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_SNS_TEMP, sensor_name, temperature, TempUnit()); +#endif // USE_WEBSERVER + } + } +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xsns72(uint8_t function) +{ + if (!I2cEnabled(XI2C_51)) { return false; } + bool result = false; + + if (FUNC_INIT == function) { + MCP9808Detect(); + } + else if (mcp9808_cfg.count){ + switch (function) { + case FUNC_EVERY_SECOND: + MCP9808EverySecond(); + break; + case FUNC_JSON_APPEND: + MCP9808Show(1); + break; + #ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + MCP9808Show(0); + break; + #endif // USE_WEBSERVER + } + } + return result; +} + +#endif // USE_MCP9808 +#endif // USE_I2C \ No newline at end of file diff --git a/tools/decode-status.py b/tools/decode-status.py index a992a621c..67898d784 100755 --- a/tools/decode-status.py +++ b/tools/decode-status.py @@ -204,7 +204,7 @@ a_features = [[ "USE_KEELOQ","USE_HRXL","USE_SONOFF_D1","USE_HDC1080", "USE_IAQ","USE_DISPLAY_SEVENSEG","USE_AS3935","USE_PING", "USE_WINDMETER","USE_OPENTHERM","USE_THERMOSTAT","USE_VEML6075", - "USE_VEML7700","","","", + "USE_VEML7700","USE_MCP9808","","", "","","","", "","","","", "","","","", From 05a2c3c164ce324f4f263286c0cd63636cb6f358 Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Tue, 2 Jun 2020 11:34:25 +0200 Subject: [PATCH 30/48] correction of BUILS.md and mcp9808 Domoticz index --- BUILDS.md | 6 +++--- tasmota/xsns_72_mcp9808.ino | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/BUILDS.md b/BUILDS.md index 92d081c1e..7e642de4e 100644 --- a/BUILDS.md +++ b/BUILDS.md @@ -120,6 +120,9 @@ | USE_WEMOS_MOTOR_V1 | - | - | - | - | x | - | - | | USE_IAQ | - | - | - | - | x | - | - | | USE_AS3935 | - | - | - | - | x | - | - | +| USE_VEML6075 | - | - | - | - | - | - | - | +| USE_VEML7700 | - | - | - | - | - | - | - | +| USE_MCP9808 | - | - | - | - | - | - | - | | | | | | | | | | | Feature or Sensor | minimal | lite | tasmota | knx | sensors | ir | display | Remarks | USE_SPI | - | - | - | - | - | - | x | @@ -139,9 +142,6 @@ | USE_HRXL | - | - | - | - | x | - | - | | USE_TASMOTA_SLAVE | - | - | - | - | - | - | - | | USE_OPENTHERM | - | - | - | - | - | - | - | -| USE_VEML6075 | - | - | - | - | - | - | - | -| USE_VEML7700 | - | - | - | - | - | - | - | -| USE_MCP9808 | - | - | - | - | - | - | - | | | | | | | | | | | USE_NRF24 | - | - | - | - | - | - | - | | USE_MIBLE | - | - | - | - | - | - | - | diff --git a/tasmota/xsns_72_mcp9808.ino b/tasmota/xsns_72_mcp9808.ino index 42fdd7ab9..fdd200099 100644 --- a/tasmota/xsns_72_mcp9808.ino +++ b/tasmota/xsns_72_mcp9808.ino @@ -86,7 +86,7 @@ void MCP9808Show(bool json) { if (json) { ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s}"), sensor_name, temperature); - if (0 == tele_period) { + if ((0 == tele_period) && (0 == i)) { #ifdef USE_DOMOTICZ DomoticzSensor(DZ_TEMP, temperature); #endif // USE_DOMOTICZ From e57bbb2b7d10ce4a525b6c5121bd462787d77111 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Tue, 2 Jun 2020 12:09:31 +0200 Subject: [PATCH 31/48] Some code refactoring --- tasmota/xsns_26_lm75ad.ino | 2 +- tasmota/xsns_59_ds1624.ino | 2 +- tasmota/xsns_72_mcp9808.ino | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tasmota/xsns_26_lm75ad.ino b/tasmota/xsns_26_lm75ad.ino index 5b735dcb7..d5073f759 100644 --- a/tasmota/xsns_26_lm75ad.ino +++ b/tasmota/xsns_26_lm75ad.ino @@ -89,7 +89,7 @@ void LM75ADShow(bool json) dtostrfd(t, Settings.flag2.temperature_resolution, temperature); if (json) { - ResponseAppend_P(PSTR(",\"LM75AD\":{\"" D_JSON_TEMPERATURE "\":%s}"), temperature); + ResponseAppend_P(JSON_SNS_TEMP, "LM75AD", temperature); #ifdef USE_DOMOTICZ if (0 == tele_period) DomoticzSensor(DZ_TEMP, temperature); #endif // USE_DOMOTICZ diff --git a/tasmota/xsns_59_ds1624.ino b/tasmota/xsns_59_ds1624.ino index f3ffcd1f1..60b176c6e 100644 --- a/tasmota/xsns_59_ds1624.ino +++ b/tasmota/xsns_59_ds1624.ino @@ -184,7 +184,7 @@ void DS1624Show(bool json) dtostrfd(ds1624_sns[i].value, Settings.flag2.temperature_resolution, temperature); if (json) { - ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s}"), ds1624_sns[i].name, temperature); + ResponseAppend_P(JSON_SNS_TEMP, ds1624_sns[i].name, temperature); if ((0 == tele_period) && once) { #ifdef USE_DOMOTICZ DomoticzSensor(DZ_TEMP, temperature); diff --git a/tasmota/xsns_72_mcp9808.ino b/tasmota/xsns_72_mcp9808.ino index fdd200099..0d757c684 100644 --- a/tasmota/xsns_72_mcp9808.ino +++ b/tasmota/xsns_72_mcp9808.ino @@ -55,9 +55,9 @@ float MCP9808Read(uint8_t addr) { void MCP9808Detect(void) { for (uint8_t i = 0; i < MCP9808_MAX_SENSORS; i++) { - if (!I2cSetDevice(MCP9808_START_ADDRESS + i)) { continue; } + if (!I2cSetDevice(MCP9808_START_ADDRESS + i)) { continue; } - if (mcp9808.begin(MCP9808_START_ADDRESS + i)) { + if (mcp9808.begin(MCP9808_START_ADDRESS + i)) { mcp9808_sensors[mcp9808_cfg.count].address = MCP9808_START_ADDRESS + i; I2cSetActiveFound(mcp9808_sensors[mcp9808_cfg.count].address, mcp9808_cfg.types); mcp9808.setResolution (mcp9808_sensors[mcp9808_cfg.count].address, 2); // Set Resolution to 0.125°C @@ -77,7 +77,7 @@ void MCP9808Show(bool json) { for (uint32_t i = 0; i < mcp9808_cfg.count; i++) { char temperature[33]; dtostrfd(mcp9808_sensors[i].temperature, Settings.flag2.temperature_resolution, temperature); - + char sensor_name[10]; strlcpy(sensor_name, mcp9808_cfg.types, sizeof(sensor_name)); if (mcp9808_cfg.count > 1) { @@ -85,7 +85,7 @@ void MCP9808Show(bool json) { } if (json) { - ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_TEMPERATURE "\":%s}"), sensor_name, temperature); + ResponseAppend_P(JSON_SNS_TEMP, sensor_name, temperature); if ((0 == tele_period) && (0 == i)) { #ifdef USE_DOMOTICZ DomoticzSensor(DZ_TEMP, temperature); From 57639d95241a0c85e7665ffe878d2f26b6bbdb0d Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Tue, 2 Jun 2020 12:24:29 +0200 Subject: [PATCH 32/48] fix VEML7700 MQTT white value message and I2C detect --- tasmota/xsns_71_veml7700.ino | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tasmota/xsns_71_veml7700.ino b/tasmota/xsns_71_veml7700.ino index 18a57bcf7..3869e72d6 100644 --- a/tasmota/xsns_71_veml7700.ino +++ b/tasmota/xsns_71_veml7700.ino @@ -54,8 +54,8 @@ struct VEML7700STRUCT { char types[9] = D_NAME_VEML7700; uint8_t address = VEML7700_I2CADDR_DEFAULT; - uint16_t lux = 0; - uint16_t white = 0; + //uint16_t lux = 0; + //uint16_t white = 0; uint16_t lux_normalized = 0; uint16_t white_normalized = 0; } veml7700_sensor; @@ -65,7 +65,7 @@ uint8_t veml7700_active = 0; /********************************************************************************************/ void VEML7700Detect(void) { - if (I2cActive(veml7700_sensor.address)) return; + if (!I2cSetDevice(veml7700_sensor.address)) return; if (veml7700.begin()) { I2cSetActiveFound(veml7700_sensor.address, veml7700_sensor.types); veml7700_active = 1; @@ -106,7 +106,7 @@ void VEML7700EverySecond(void) { void VEML7700Show(bool json) { if (json) { - ResponseAppend_P(JSON_SNS_VEML7700, D_NAME_VEML7700, veml7700_sensor.lux_normalized, veml7700_sensor.white); + ResponseAppend_P(JSON_SNS_VEML7700, D_NAME_VEML7700, veml7700_sensor.lux_normalized, veml7700_sensor.white_normalized); #ifdef USE_DOMOTICZ if (0 == tele_period) DomoticzSensor(DZ_ILLUMINANCE, veml7700_sensor.lux_normalized); From 006af71803757ec52f85665b730b5174115463bc Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 17:35:25 +0200 Subject: [PATCH 33/48] no need for HueEmulation for core 2.4.2 anymore. Out of support. --- tasmota/Parsing.cpp | 627 -------------------------------------------- 1 file changed, 627 deletions(-) delete mode 100644 tasmota/Parsing.cpp diff --git a/tasmota/Parsing.cpp b/tasmota/Parsing.cpp deleted file mode 100644 index a7665d7b1..000000000 --- a/tasmota/Parsing.cpp +++ /dev/null @@ -1,627 +0,0 @@ -/* - Parsing.cpp - HTTP request parsing. - - Copyright (c) 2015 Ivan Grokhotkov. All rights reserved. - - This library is free software; you can redistribute it and/or - modify it under the terms of the GNU Lesser General Public - License as published by the Free Software Foundation; either - version 2.1 of the License, or (at your option) any later version. - - This library 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 - Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public - License along with this library; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA - Modified 8 May 2015 by Hristo Gochkov (proper post and file upload handling) -*/ - -#ifdef ESP8266 - -// Use patched Parsing.cpp to fix ALEXA parsing issue in v2.4.2 -#include -#if defined(ARDUINO_ESP8266_RELEASE_2_4_2) -#warning **** Tasmota is using v2.4.2 patched Parsing.cpp as planned **** - -#include -#include "WiFiServer.h" -#include "WiFiClient.h" -#include "ESP8266WebServer.h" -#include "detail/mimetable.h" - -//#define DEBUG_ESP_HTTP_SERVER -#ifdef DEBUG_ESP_PORT -#define DEBUG_OUTPUT DEBUG_ESP_PORT -#else -#define DEBUG_OUTPUT Serial -#endif - -static const char Content_Type[] PROGMEM = "Content-Type"; -static const char filename[] PROGMEM = "filename"; - -static char* readBytesWithTimeout(WiFiClient& client, size_t maxLength, size_t& dataLength, int timeout_ms) -{ - char *buf = nullptr; - dataLength = 0; - while (dataLength < maxLength) { - int tries = timeout_ms; - size_t newLength; - while (!(newLength = client.available()) && tries--) delay(1); - if (!newLength) { - break; - } - if (!buf) { - buf = (char *) malloc(newLength + 1); - if (!buf) { - return nullptr; - } - } - else { - char* newBuf = (char *) realloc(buf, dataLength + newLength + 1); - if (!newBuf) { - free(buf); - return nullptr; - } - buf = newBuf; - } - client.readBytes(buf + dataLength, newLength); - dataLength += newLength; - buf[dataLength] = '\0'; - } - return buf; -} - -bool ESP8266WebServer::_parseRequest(WiFiClient& client) { - // Read the first line of HTTP request - String req = client.readStringUntil('\r'); - client.readStringUntil('\n'); - //reset header value - for (int i = 0; i < _headerKeysCount; ++i) { - _currentHeaders[i].value =String(); - } - - // First line of HTTP request looks like "GET /path HTTP/1.1" - // Retrieve the "/path" part by finding the spaces - int addr_start = req.indexOf(' '); - int addr_end = req.indexOf(' ', addr_start + 1); - if (addr_start == -1 || addr_end == -1) { -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Invalid request: "); - DEBUG_OUTPUT.println(req); -#endif - return false; - } - - String methodStr = req.substring(0, addr_start); - String url = req.substring(addr_start + 1, addr_end); - String versionEnd = req.substring(addr_end + 8); - _currentVersion = atoi(versionEnd.c_str()); - String searchStr = ""; - int hasSearch = url.indexOf('?'); - if (hasSearch != -1){ - searchStr = url.substring(hasSearch + 1); - url = url.substring(0, hasSearch); - } - _currentUri = url; - _chunked = false; - - HTTPMethod method = HTTP_GET; - if (methodStr == F("POST")) { - method = HTTP_POST; - } else if (methodStr == F("DELETE")) { - method = HTTP_DELETE; - } else if (methodStr == F("OPTIONS")) { - method = HTTP_OPTIONS; - } else if (methodStr == F("PUT")) { - method = HTTP_PUT; - } else if (methodStr == F("PATCH")) { - method = HTTP_PATCH; - } - _currentMethod = method; - -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("method: "); - DEBUG_OUTPUT.print(methodStr); - DEBUG_OUTPUT.print(" url: "); - DEBUG_OUTPUT.print(url); - DEBUG_OUTPUT.print(" search: "); - DEBUG_OUTPUT.println(searchStr); -#endif - - //attach handler - RequestHandler* handler; - for (handler = _firstHandler; handler; handler = handler->next()) { - if (handler->canHandle(_currentMethod, _currentUri)) - break; - } - _currentHandler = handler; - - String formData; - // below is needed only when POST type request - if (method == HTTP_POST || method == HTTP_PUT || method == HTTP_PATCH || method == HTTP_DELETE){ - String boundaryStr; - String headerName; - String headerValue; - bool isForm = false; - bool isEncoded = false; - uint32_t contentLength = 0; - //parse headers - while(1){ - req = client.readStringUntil('\r'); - client.readStringUntil('\n'); - if (req == "") break;//no moar headers - int headerDiv = req.indexOf(':'); - if (headerDiv == -1){ - break; - } - headerName = req.substring(0, headerDiv); - headerValue = req.substring(headerDiv + 1); - headerValue.trim(); - _collectHeader(headerName.c_str(),headerValue.c_str()); - - #ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("headerName: "); - DEBUG_OUTPUT.println(headerName); - DEBUG_OUTPUT.print("headerValue: "); - DEBUG_OUTPUT.println(headerValue); - #endif - - if (headerName.equalsIgnoreCase(FPSTR(Content_Type))){ - using namespace mime; - if (headerValue.startsWith(FPSTR(mimeTable[txt].mimeType))){ - isForm = false; - } else if (headerValue.startsWith(F("application/x-www-form-urlencoded"))){ - isForm = false; - isEncoded = true; - } else if (headerValue.startsWith(F("multipart/"))){ - boundaryStr = headerValue.substring(headerValue.indexOf('=') + 1); - boundaryStr.replace("\"",""); - isForm = true; - } - } else if (headerName.equalsIgnoreCase(F("Content-Length"))){ - contentLength = headerValue.toInt(); - } else if (headerName.equalsIgnoreCase(F("Host"))){ - _hostHeader = headerValue; - } - } - - if (!isForm){ - size_t plainLength; - char* plainBuf = readBytesWithTimeout(client, contentLength, plainLength, HTTP_MAX_POST_WAIT); - if (plainLength < contentLength) { - free(plainBuf); - return false; - } - if (contentLength > 0) { - if(isEncoded){ - //url encoded form - if (searchStr != "") searchStr += '&'; - searchStr += plainBuf; - } - _parseArguments(searchStr); - if(!isEncoded||(0==_currentArgCount)){ // @20180124OF01: Workarround for Alexa Bug - //plain post json or other data - RequestArgument& arg = _currentArgs[_currentArgCount++]; - arg.key = F("plain"); - arg.value = String(plainBuf); - } - - #ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Plain: "); - DEBUG_OUTPUT.println(plainBuf); - #endif - free(plainBuf); - } else { - // No content - but we can still have arguments in the URL. - _parseArguments(searchStr); - } - } - - if (isForm){ - _parseArguments(searchStr); - if (!_parseForm(client, boundaryStr, contentLength)) { - return false; - } - } - } else { - String headerName; - String headerValue; - //parse headers - while(1){ - req = client.readStringUntil('\r'); - client.readStringUntil('\n'); - if (req == "") break;//no moar headers - int headerDiv = req.indexOf(':'); - if (headerDiv == -1){ - break; - } - headerName = req.substring(0, headerDiv); - headerValue = req.substring(headerDiv + 2); - _collectHeader(headerName.c_str(),headerValue.c_str()); - - #ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("headerName: "); - DEBUG_OUTPUT.println(headerName); - DEBUG_OUTPUT.print("headerValue: "); - DEBUG_OUTPUT.println(headerValue); - #endif - - if (headerName.equalsIgnoreCase("Host")){ - _hostHeader = headerValue; - } - } - _parseArguments(searchStr); - } - client.flush(); - -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Request: "); - DEBUG_OUTPUT.println(url); - DEBUG_OUTPUT.print(" Arguments: "); - DEBUG_OUTPUT.println(searchStr); -#endif - - return true; -} - -bool ESP8266WebServer::_collectHeader(const char* headerName, const char* headerValue) { - for (int i = 0; i < _headerKeysCount; i++) { - if (_currentHeaders[i].key.equalsIgnoreCase(headerName)) { - _currentHeaders[i].value=headerValue; - return true; - } - } - return false; -} - -void ESP8266WebServer::_parseArguments(String data) { -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("args: "); - DEBUG_OUTPUT.println(data); -#endif - if (_currentArgs) - delete[] _currentArgs; - _currentArgs = 0; - if (data.length() == 0) { - _currentArgCount = 0; - _currentArgs = new RequestArgument[1]; - return; - } - _currentArgCount = 1; - - for (int i = 0; i < (int)data.length(); ) { - i = data.indexOf('&', i); - if (i == -1) - break; - ++i; - ++_currentArgCount; - } -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("args count: "); - DEBUG_OUTPUT.println(_currentArgCount); -#endif - - _currentArgs = new RequestArgument[_currentArgCount+1]; - int pos = 0; - int iarg; - for (iarg = 0; iarg < _currentArgCount;) { - int equal_sign_index = data.indexOf('=', pos); - int next_arg_index = data.indexOf('&', pos); -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("pos "); - DEBUG_OUTPUT.print(pos); - DEBUG_OUTPUT.print("=@ "); - DEBUG_OUTPUT.print(equal_sign_index); - DEBUG_OUTPUT.print(" &@ "); - DEBUG_OUTPUT.println(next_arg_index); -#endif - if ((equal_sign_index == -1) || ((equal_sign_index > next_arg_index) && (next_arg_index != -1))) { -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("arg missing value: "); - DEBUG_OUTPUT.println(iarg); -#endif - if (next_arg_index == -1) - break; - pos = next_arg_index + 1; - continue; - } - RequestArgument& arg = _currentArgs[iarg]; - arg.key = urlDecode(data.substring(pos, equal_sign_index)); - arg.value = urlDecode(data.substring(equal_sign_index + 1, next_arg_index)); -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("arg "); - DEBUG_OUTPUT.print(iarg); - DEBUG_OUTPUT.print(" key: "); - DEBUG_OUTPUT.print(arg.key); - DEBUG_OUTPUT.print(" value: "); - DEBUG_OUTPUT.println(arg.value); -#endif - ++iarg; - if (next_arg_index == -1) - break; - pos = next_arg_index + 1; - } - _currentArgCount = iarg; -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("args count: "); - DEBUG_OUTPUT.println(_currentArgCount); -#endif - -} - -void ESP8266WebServer::_uploadWriteByte(uint8_t b){ - if (_currentUpload->currentSize == HTTP_UPLOAD_BUFLEN){ - if(_currentHandler && _currentHandler->canUpload(_currentUri)) - _currentHandler->upload(*this, _currentUri, *_currentUpload); - _currentUpload->totalSize += _currentUpload->currentSize; - _currentUpload->currentSize = 0; - } - _currentUpload->buf[_currentUpload->currentSize++] = b; -} - -uint8_t ESP8266WebServer::_uploadReadByte(WiFiClient& client){ - int res = client.read(); - if(res == -1){ - while(!client.available() && client.connected()) - yield(); - res = client.read(); - } - return (uint8_t)res; -} - -bool ESP8266WebServer::_parseForm(WiFiClient& client, String boundary, uint32_t len){ - (void) len; -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Parse Form: Boundary: "); - DEBUG_OUTPUT.print(boundary); - DEBUG_OUTPUT.print(" Length: "); - DEBUG_OUTPUT.println(len); -#endif - String line; - int retry = 0; - do { - line = client.readStringUntil('\r'); - ++retry; - } while (line.length() == 0 && retry < 3); - - client.readStringUntil('\n'); - //start reading the form - if (line == ("--"+boundary)){ - RequestArgument* postArgs = new RequestArgument[32]; - int postArgsLen = 0; - while(1){ - String argName; - String argValue; - String argType; - String argFilename; - bool argIsFile = false; - - line = client.readStringUntil('\r'); - client.readStringUntil('\n'); - if (line.length() > 19 && line.substring(0, 19).equalsIgnoreCase(F("Content-Disposition"))){ - int nameStart = line.indexOf('='); - if (nameStart != -1){ - argName = line.substring(nameStart+2); - nameStart = argName.indexOf('='); - if (nameStart == -1){ - argName = argName.substring(0, argName.length() - 1); - } else { - argFilename = argName.substring(nameStart+2, argName.length() - 1); - argName = argName.substring(0, argName.indexOf('"')); - argIsFile = true; -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("PostArg FileName: "); - DEBUG_OUTPUT.println(argFilename); -#endif - //use GET to set the filename if uploading using blob - if (argFilename == F("blob") && hasArg(FPSTR(filename))) - argFilename = arg(FPSTR(filename)); - } -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("PostArg Name: "); - DEBUG_OUTPUT.println(argName); -#endif - using namespace mime; - argType = FPSTR(mimeTable[txt].mimeType); - line = client.readStringUntil('\r'); - client.readStringUntil('\n'); - if (line.length() > 12 && line.substring(0, 12).equalsIgnoreCase(FPSTR(Content_Type))){ - argType = line.substring(line.indexOf(':')+2); - //skip next line - client.readStringUntil('\r'); - client.readStringUntil('\n'); - } -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("PostArg Type: "); - DEBUG_OUTPUT.println(argType); -#endif - if (!argIsFile){ - while(1){ - line = client.readStringUntil('\r'); - client.readStringUntil('\n'); - if (line.startsWith("--"+boundary)) break; - if (argValue.length() > 0) argValue += "\n"; - argValue += line; - } -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("PostArg Value: "); - DEBUG_OUTPUT.println(argValue); - DEBUG_OUTPUT.println(); -#endif - - RequestArgument& arg = postArgs[postArgsLen++]; - arg.key = argName; - arg.value = argValue; - - if (line == ("--"+boundary+"--")){ -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.println("Done Parsing POST"); -#endif - break; - } - } else { - _currentUpload.reset(new HTTPUpload()); - _currentUpload->status = UPLOAD_FILE_START; - _currentUpload->name = argName; - _currentUpload->filename = argFilename; - _currentUpload->type = argType; - _currentUpload->totalSize = 0; - _currentUpload->currentSize = 0; -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Start File: "); - DEBUG_OUTPUT.print(_currentUpload->filename); - DEBUG_OUTPUT.print(" Type: "); - DEBUG_OUTPUT.println(_currentUpload->type); -#endif - if(_currentHandler && _currentHandler->canUpload(_currentUri)) - _currentHandler->upload(*this, _currentUri, *_currentUpload); - _currentUpload->status = UPLOAD_FILE_WRITE; - uint8_t argByte = _uploadReadByte(client); -readfile: - while(argByte != 0x0D){ - if (!client.connected()) return _parseFormUploadAborted(); - _uploadWriteByte(argByte); - argByte = _uploadReadByte(client); - } - - argByte = _uploadReadByte(client); - if (!client.connected()) return _parseFormUploadAborted(); - if (argByte == 0x0A){ - argByte = _uploadReadByte(client); - if (!client.connected()) return _parseFormUploadAborted(); - if ((char)argByte != '-'){ - //continue reading the file - _uploadWriteByte(0x0D); - _uploadWriteByte(0x0A); - goto readfile; - } else { - argByte = _uploadReadByte(client); - if (!client.connected()) return _parseFormUploadAborted(); - if ((char)argByte != '-'){ - //continue reading the file - _uploadWriteByte(0x0D); - _uploadWriteByte(0x0A); - _uploadWriteByte((uint8_t)('-')); - goto readfile; - } - } - - uint8_t endBuf[boundary.length()]; - client.readBytes(endBuf, boundary.length()); - - if (strstr((const char*)endBuf, boundary.c_str()) != nullptr){ - if(_currentHandler && _currentHandler->canUpload(_currentUri)) - _currentHandler->upload(*this, _currentUri, *_currentUpload); - _currentUpload->totalSize += _currentUpload->currentSize; - _currentUpload->status = UPLOAD_FILE_END; - if(_currentHandler && _currentHandler->canUpload(_currentUri)) - _currentHandler->upload(*this, _currentUri, *_currentUpload); -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("End File: "); - DEBUG_OUTPUT.print(_currentUpload->filename); - DEBUG_OUTPUT.print(" Type: "); - DEBUG_OUTPUT.print(_currentUpload->type); - DEBUG_OUTPUT.print(" Size: "); - DEBUG_OUTPUT.println(_currentUpload->totalSize); -#endif - line = client.readStringUntil(0x0D); - client.readStringUntil(0x0A); - if (line == "--"){ -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.println("Done Parsing POST"); -#endif - break; - } - continue; - } else { - _uploadWriteByte(0x0D); - _uploadWriteByte(0x0A); - _uploadWriteByte((uint8_t)('-')); - _uploadWriteByte((uint8_t)('-')); - uint32_t i = 0; - while(i < boundary.length()){ - _uploadWriteByte(endBuf[i++]); - } - argByte = _uploadReadByte(client); - goto readfile; - } - } else { - _uploadWriteByte(0x0D); - goto readfile; - } - break; - } - } - } - } - - int iarg; - int totalArgs = ((32 - postArgsLen) < _currentArgCount)?(32 - postArgsLen):_currentArgCount; - for (iarg = 0; iarg < totalArgs; iarg++){ - RequestArgument& arg = postArgs[postArgsLen++]; - arg.key = _currentArgs[iarg].key; - arg.value = _currentArgs[iarg].value; - } - if (_currentArgs) delete[] _currentArgs; - _currentArgs = new RequestArgument[postArgsLen]; - for (iarg = 0; iarg < postArgsLen; iarg++){ - RequestArgument& arg = _currentArgs[iarg]; - arg.key = postArgs[iarg].key; - arg.value = postArgs[iarg].value; - } - _currentArgCount = iarg; - if (postArgs) - delete[] postArgs; - return true; - } -#ifdef DEBUG_ESP_HTTP_SERVER - DEBUG_OUTPUT.print("Error: line: "); - DEBUG_OUTPUT.println(line); -#endif - return false; -} - -String ESP8266WebServer::urlDecode(const String& text) -{ - String decoded = ""; - char temp[] = "0x00"; - unsigned int len = text.length(); - unsigned int i = 0; - while (i < len) - { - char decodedChar; - char encodedChar = text.charAt(i++); - if ((encodedChar == '%') && (i + 1 < len)) - { - temp[2] = text.charAt(i++); - temp[3] = text.charAt(i++); - - decodedChar = strtol(temp, NULL, 16); - } - else { - if (encodedChar == '+') - { - decodedChar = ' '; - } - else { - decodedChar = encodedChar; // normal ascii char - } - } - decoded += decodedChar; - } - return decoded; -} - -bool ESP8266WebServer::_parseFormUploadAborted(){ - _currentUpload->status = UPLOAD_FILE_ABORTED; - if(_currentHandler && _currentHandler->canUpload(_currentUri)) - _currentHandler->upload(*this, _currentUri, *_currentUpload); - return false; -} - -#endif // ARDUINO_ESP8266_RELEASE - -#endif // ESP8266 From 98fc7db8fa9fdf789e84591b4c85c95793307d10 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:02:36 +0200 Subject: [PATCH 34/48] Flash_freq in override --- platformio_override_sample.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index 22256f679..f85ce989f 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -52,6 +52,11 @@ build_flags = ${core_active.build_flags} ; set CPU frequency to 80MHz (default) or 160MHz ;board_build.f_cpu = 160000000L +; set Flash chip frequency to 40MHz (default), 20MHz, 26Mhz, 80Mhz +;board_build.f_flash = 20000000L +;board_build.f_flash = 26000000L +;board_build.f_flash = 80000000L + ; *** Upload Serial reset method for Wemos and NodeMCU upload_port = COM5 From bf19634e1cd27497123f479860f40bdb0ab11850 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:04:31 +0200 Subject: [PATCH 35/48] Update platformio_tasmota_env.ini --- platformio_tasmota_env.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio_tasmota_env.ini b/platformio_tasmota_env.ini index dbf79d288..3d08d5cd6 100644 --- a/platformio_tasmota_env.ini +++ b/platformio_tasmota_env.ini @@ -5,6 +5,7 @@ framework = ${common.framework} board = ${common.board} board_build.ldscript = ${common.board_build.ldscript} board_build.flash_mode = ${common.board_build.flash_mode} +board_build.f_flash = ${common.board_build.f_flash} board_build.f_cpu = ${common.board_build.f_cpu} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} From e997ce4a8abf8af1a99ced6c369b909e9fbe5bce Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:05:15 +0200 Subject: [PATCH 36/48] Update platformio_tasmota_env32.ini --- platformio_tasmota_env32.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio_tasmota_env32.ini b/platformio_tasmota_env32.ini index f4f99282c..4398dcb04 100644 --- a/platformio_tasmota_env32.ini +++ b/platformio_tasmota_env32.ini @@ -6,6 +6,7 @@ board = ${common32.board} board_build.ldscript = ${common32.board_build.ldscript} board_build.partitions = ${common32.board_build.partitions} board_build.flash_mode = ${common32.board_build.flash_mode} +board_build.f_flash = ${common32.board_build.f_flash} board_build.f_cpu = ${common32.board_build.f_cpu} monitor_speed = ${common32.monitor_speed} upload_port = ${common32.upload_port} From a5868452a054a4b892758154d0872765390e9b0e Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 18:06:04 +0200 Subject: [PATCH 37/48] Update platformio_override_sample.ini --- platformio_override_sample.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini index f85ce989f..2becf05f7 100644 --- a/platformio_override_sample.ini +++ b/platformio_override_sample.ini @@ -170,6 +170,7 @@ board = esp32dev board_build.ldscript = esp32_out.ld board_build.partitions = esp32_partition_app1984k_spiffs64k.csv board_build.flash_mode = ${common.board_build.flash_mode} +board_build.f_flash = ${common.board_build.f_flash} board_build.f_cpu = ${common.board_build.f_cpu} build_unflags = ${common.build_unflags} -Wpointer-arith From f10ed3261c51eb3630d1e9302b86c4238242682a Mon Sep 17 00:00:00 2001 From: device111 <48546979+device111@users.noreply.github.com> Date: Tue, 2 Jun 2020 19:13:06 +0200 Subject: [PATCH 38/48] AS3935, add stage to json, fix overwrite nf-floor --- tasmota/i18n.h | 1 + tasmota/xsns_67_as3935.ino | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tasmota/i18n.h b/tasmota/i18n.h index b538e705e..073f537b2 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -147,6 +147,7 @@ #define D_JSON_SPEED "Speed" #define D_JSON_SPEED_UNIT "SpeedUnit" #define D_JSON_SSID "SSId" +#define D_JSON_STAGE "Stage" #define D_JSON_STARTDST "StartDST" // Start Daylight Savings Time #define D_JSON_STARTED "Started" #define D_JSON_STARTUPUTC "StartupUTC" diff --git a/tasmota/xsns_67_as3935.ino b/tasmota/xsns_67_as3935.ino index 2b73a2749..3e795b91a 100644 --- a/tasmota/xsns_67_as3935.ino +++ b/tasmota/xsns_67_as3935.ino @@ -51,8 +51,6 @@ #define INDOORS 0x24 #define OUTDOORS 0x1C - - // Global const char HTTP_SNS_UNIT_KILOMETER[] PROGMEM = D_UNIT_KILOMETER; // Http @@ -78,7 +76,7 @@ const char HTTP_SNS_AS3935_INTNOEV[] PROGMEM = "{s}%s: " D_AS3935_INTNOEV "{e}"; const char HTTP_SNS_AS3935_MSG[] PROGMEM = "{s}%s: " D_AS3935_LIGHT " " D_AS3935_APRX " %d " D_UNIT_KILOMETER " " D_AS3935_AWAY "{e}"; const char* const HTTP_SNS_AS3935_TABLE_1[] PROGMEM = { HTTP_SNS_AS3935_EMPTY, HTTP_SNS_AS3935_MSG, HTTP_SNS_AS3935_OUT, HTTP_SNS_AS3935_NOT, HTTP_SNS_AS3935_ABOVE, HTTP_SNS_AS3935_NOISE, HTTP_SNS_AS3935_DISTURB, HTTP_SNS_AS3935_INTNOEV }; // Json -const char JSON_SNS_AS3935_EVENTS[] PROGMEM = ",\"%s\":{\"" D_JSON_EVENT "\":%d,\"" D_JSON_DISTANCE "\":%d,\"" D_JSON_ENERGY "\":%u}"; +const char JSON_SNS_AS3935_EVENTS[] PROGMEM = ",\"%s\":{\"" D_JSON_EVENT "\":%d,\"" D_JSON_DISTANCE "\":%d,\"" D_JSON_ENERGY "\":%u,\"" D_JSON_STAGE "\":%d}"; // Json Command const char* const S_JSON_AS3935_COMMAND_ONOFF[] PROGMEM = {"\"" D_AS3935_OFF "\"","\"" D_AS3935_ON"\""}; const char* const S_JSON_AS3935_COMMAND_GAIN[] PROGMEM = {"\"" D_AS3935_INDOORS "\"", "\"" D_AS3935_OUTDOORS "\""}; @@ -473,9 +471,6 @@ void AS3935InitSettings() { AS3935SetGain(INDOORS); AS3935SetNoiseFloor(Settings.as3935_parameter.nf_autotune_min); } - } else { - AS3935SetGain(INDOORS); - AS3935SetNoiseFloor(0); } } I2cWrite8(AS3935_ADDR, 0x00, Settings.as3935_sensor_cfg[0]); @@ -756,8 +751,10 @@ bool AS3935Cmd(void) { void AH3935Show(bool json) { if (json) { - ResponseAppend_P(JSON_SNS_AS3935_EVENTS, D_SENSOR_AS3935, as3935_sensor.mqtt_irq, as3935_sensor.distance, as3935_sensor.intensity ); - + uint16_t vrms; + uint8_t stage; + AS3935CalcVrmsLevel(vrms, stage); + ResponseAppend_P(JSON_SNS_AS3935_EVENTS, D_SENSOR_AS3935, as3935_sensor.mqtt_irq, as3935_sensor.distance, as3935_sensor.intensity, stage); #ifdef USE_WEBSERVER } else { uint8_t gain = AS3935GetGainInt(); From 0a88c56376a305546204ff1b48d5993e87fe5929 Mon Sep 17 00:00:00 2001 From: Jason2866 <24528715+Jason2866@users.noreply.github.com> Date: Tue, 2 Jun 2020 21:46:56 +0200 Subject: [PATCH 39/48] Update platformio.ini --- platformio.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/platformio.ini b/platformio.ini index c9eb6d5f4..04835d6c0 100755 --- a/platformio.ini +++ b/platformio.ini @@ -74,6 +74,7 @@ build_flags = ${core_active.build_flags} build_unflags = -Wall board_build.f_cpu = 80000000L +board_build.f_flash = 40000000L monitor_speed = 115200 upload_speed = 115200 ; *** Upload Serial reset method for Wemos and NodeMCU From 6dd50a394c1f4a5ac8cee0bf086d59ba92d5511f Mon Sep 17 00:00:00 2001 From: halfbakery Date: Tue, 2 Jun 2020 22:17:20 +0200 Subject: [PATCH 40/48] Add support for switches using an AC detection circuitry --- tasmota/support_switch.ino | 123 ++++++++++++++++++++++++++++++------- 1 file changed, 101 insertions(+), 22 deletions(-) diff --git a/tasmota/support_switch.ino b/tasmota/support_switch.ino index c13243c0e..9fc3b57c3 100644 --- a/tasmota/support_switch.ino +++ b/tasmota/support_switch.ino @@ -26,6 +26,8 @@ \*********************************************************************************************/ const uint8_t SWITCH_PROBE_INTERVAL = 10; // Time in milliseconds between switch input probe +const uint8_t SWITCH_FAST_PROBE_INTERVAL =2;// Time in milliseconds between switch input probe for AC detection +const uint8_t AC_PERIOD = (20 + SWITCH_FAST_PROBE_INTERVAL - 1) / SWITCH_FAST_PROBE_INTERVAL; // Duration of an AC wave in probe intervals #include @@ -38,6 +40,7 @@ struct SWITCH { uint8_t last_state[MAX_SWITCHES]; // Last wall switch states uint8_t hold_timer[MAX_SWITCHES] = { 0 }; // Timer for wallswitch push button hold uint8_t virtual_state[MAX_SWITCHES]; // Virtual switch states + uint8_t first_change = 0; uint8_t present = 0; } Switch; @@ -81,60 +84,136 @@ void SwitchProbe(void) { if (uptime < 4) { return; } // Block GPIO for 4 seconds after poweron to workaround Wemos D1 / Obi RTS circuit - uint8_t state_filter = Settings.switch_debounce / SWITCH_PROBE_INTERVAL; // 5, 10, 15 - uint8_t force_high = (Settings.switch_debounce % 10) &1; // 51, 101, 151 etc - uint8_t force_low = (Settings.switch_debounce % 10) &2; // 52, 102, 152 etc + uint8_t state_filter; + uint8_t debounce_flags = Settings.switch_debounce % 10; + uint8_t force_high = debounce_flags &1; // 51, 101, 151 etc + uint8_t force_low = debounce_flags &2; // 52, 102, 152 etc + uint8_t ac_detect = debounce_flags == 9; + uint8_t switch_probe_interval; + uint8_t first_change = Switch.first_change; + + if (ac_detect) { + switch_probe_interval = SWITCH_FAST_PROBE_INTERVAL; + if (Settings.switch_debounce < 2 * AC_PERIOD * SWITCH_FAST_PROBE_INTERVAL + 9) { + state_filter = 2 * AC_PERIOD; + } else if (Settings.switch_debounce > (0x7f - 2 * AC_PERIOD) * SWITCH_FAST_PROBE_INTERVAL) { + state_filter = 0x7f; + } else { + state_filter = (Settings.switch_debounce - 9) / SWITCH_FAST_PROBE_INTERVAL; + } + } else { + switch_probe_interval = SWITCH_PROBE_INTERVAL; + state_filter = Settings.switch_debounce / SWITCH_PROBE_INTERVAL; // 5, 10, 15 + } for (uint32_t i = 0; i < MAX_SWITCHES; i++) { if (PinUsed(GPIO_SWT1, i)) { // Olimex user_switch2.c code to fix 50Hz induced pulses if (1 == digitalRead(Pin(GPIO_SWT1, i))) { - if (force_high) { // Enabled with SwitchDebounce x1 - if (1 == Switch.virtual_state[i]) { - Switch.state[i] = state_filter; // With noisy input keep current state 1 unless constant 0 + if (ac_detect) { // Enabled with SwitchDebounce x9 + Switch.state[i] |= 0x80; + if (Switch.state[i] > 0x80) { + Switch.state[i]--; + if (0x80 == Switch.state[i]) { + Switch.virtual_state[i] = 0; + Switch.first_change = false; + } } - } + } else { - if (Switch.state[i] < state_filter) { - Switch.state[i]++; - if (state_filter == Switch.state[i]) { - Switch.virtual_state[i] = 1; + if (force_high) { // Enabled with SwitchDebounce x1 + if (1 == Switch.virtual_state[i]) { + Switch.state[i] = state_filter; // With noisy input keep current state 1 unless constant 0 + } + } + + if (Switch.state[i] < state_filter) { + Switch.state[i]++; + if (state_filter == Switch.state[i]) { + Switch.virtual_state[i] = 1; + } } } } else { - if (force_low) { // Enabled with SwitchDebounce x2 - if (0 == Switch.virtual_state[i]) { - Switch.state[i] = 0; // With noisy input keep current state 0 unless constant 1 + if (ac_detect) { // Enabled with SwitchDebounce x9 + /* + * Moes MS-104B and similar devices using an AC detection circuitry + * on their switch inputs generating an ~4 ms long low pulse every + * AC wave. We start the time measurement on the falling edge. + * + * state: bit7: previous state, bit6..0: counter + */ + if (Switch.state[i] & 0x80) { + Switch.state[i] &= 0x7f; + if (Switch.state[i] < state_filter - 2 * AC_PERIOD) { + Switch.state[i] += 2 * AC_PERIOD; + } else { + Switch.state[i] = state_filter; + Switch.virtual_state[i] = 1; + if (first_change) { + Switch.last_state[i] = 1; + Switch.first_change = false; + } + } + } else { + if (Switch.state[i] > 0x00) { + Switch.state[i]--; + if (0x00 == Switch.state[i]) { + Switch.virtual_state[i] = 0; + Switch.first_change = false; + } + } } - } + } else { - if (Switch.state[i] > 0) { - Switch.state[i]--; - if (0 == Switch.state[i]) { - Switch.virtual_state[i] = 0; + if (force_low) { // Enabled with SwitchDebounce x2 + if (0 == Switch.virtual_state[i]) { + Switch.state[i] = 0; // With noisy input keep current state 0 unless constant 1 + } + } + + if (Switch.state[i] > 0) { + Switch.state[i]--; + if (0 == Switch.state[i]) { + Switch.virtual_state[i] = 0; + } } } } } } - TickerSwitch.attach_ms(SWITCH_PROBE_INTERVAL, SwitchProbe); // Re-arm as core 2.3.0 does only support ONCE mode + TickerSwitch.attach_ms(switch_probe_interval, SwitchProbe); // Re-arm as core 2.3.0 does only support ONCE mode } void SwitchInit(void) { + uint8_t ac_detect = Settings.switch_debounce % 10 == 9; + Switch.present = 0; for (uint32_t i = 0; i < MAX_SWITCHES; i++) { Switch.last_state[i] = 1; // Init global to virtual switch state; if (PinUsed(GPIO_SWT1, i)) { Switch.present++; pinMode(Pin(GPIO_SWT1, i), bitRead(Switch.no_pullup_mask, i) ? INPUT : ((16 == Pin(GPIO_SWT1, i)) ? INPUT_PULLDOWN_16 : INPUT_PULLUP)); - Switch.last_state[i] = digitalRead(Pin(GPIO_SWT1, i)); // Set global now so doesn't change the saved power state on first switch check + if (ac_detect) { + Switch.state[i] = 0x80 + 2 * AC_PERIOD; + Switch.last_state[i] = 0; // Will set later in the debouncing code + } else { + Switch.last_state[i] = digitalRead(Pin(GPIO_SWT1, i)); // Set global now so doesn't change the saved power state on first switch check + } } Switch.virtual_state[i] = Switch.last_state[i]; } - if (Switch.present) { TickerSwitch.attach_ms(SWITCH_PROBE_INTERVAL, SwitchProbe); } + if (Switch.present) { + if (ac_detect) { + TickerSwitch.attach_ms(SWITCH_FAST_PROBE_INTERVAL, SwitchProbe); + Switch.first_change = true; + } else { + TickerSwitch.attach_ms(SWITCH_PROBE_INTERVAL, SwitchProbe); + } + } } /*********************************************************************************************\ From 3748bb2121a1efddfd7828923a95c49e25b97156 Mon Sep 17 00:00:00 2001 From: m-hume Date: Wed, 3 Jun 2020 13:06:01 +0100 Subject: [PATCH 41/48] Label sensors with hex address --- tasmota/xsns_72_mcp9808.ino | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tasmota/xsns_72_mcp9808.ino b/tasmota/xsns_72_mcp9808.ino index 0d757c684..8af129f97 100644 --- a/tasmota/xsns_72_mcp9808.ino +++ b/tasmota/xsns_72_mcp9808.ino @@ -78,10 +78,10 @@ void MCP9808Show(bool json) { char temperature[33]; dtostrfd(mcp9808_sensors[i].temperature, Settings.flag2.temperature_resolution, temperature); - char sensor_name[10]; + char sensor_name[11]; strlcpy(sensor_name, mcp9808_cfg.types, sizeof(sensor_name)); if (mcp9808_cfg.count > 1) { - snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%d"), sensor_name, IndexSeparator(), i +1); // MCP9808-1 + snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%02X"), sensor_name, IndexSeparator(), mcp9808_sensors[i].address); // MCP9808-18, MCP9808-1A etc. } if (json) { @@ -133,4 +133,4 @@ bool Xsns72(uint8_t function) } #endif // USE_MCP9808 -#endif // USE_I2C \ No newline at end of file +#endif // USE_I2C From f035932c7486d239b040f404ab441adfd3485735 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 3 Jun 2020 15:12:23 +0200 Subject: [PATCH 42/48] Change BH1750 indexes to I2C address --- tasmota/xsns_10_bh1750.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xsns_10_bh1750.ino b/tasmota/xsns_10_bh1750.ino index 7af8e3960..2c3c801a7 100644 --- a/tasmota/xsns_10_bh1750.ino +++ b/tasmota/xsns_10_bh1750.ino @@ -170,7 +170,7 @@ void Bh1750Show(bool json) { char sensor_name[10]; strlcpy(sensor_name, Bh1750.types, sizeof(sensor_name)); if (Bh1750.count > 1) { - snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%d"), sensor_name, IndexSeparator(), sensor_index +1); // BH1750-1 + snprintf_P(sensor_name, sizeof(sensor_name), PSTR("%s%c%02X"), sensor_name, IndexSeparator(), Bh1750_sensors[sensor_index].address); // BH1750-23 } if (json) { From 45397293e1c54fa673e3a7f1f5d67f2909e5c6ba Mon Sep 17 00:00:00 2001 From: Stephan Hadinger Date: Wed, 3 Jun 2020 22:39:04 +0200 Subject: [PATCH 43/48] Add Zigbee auto-responder for common attributes --- tasmota/CHANGELOG.md | 1 + tasmota/i18n.h | 1 + tasmota/support_json.ino | 8 +- tasmota/xdrv_04_light.ino | 4 + tasmota/xdrv_23_zigbee_0_constants.ino | 1 - tasmota/xdrv_23_zigbee_2_devices.ino | 2 + tasmota/xdrv_23_zigbee_5_converters.ino | 705 +++++++++++++----------- tasmota/xdrv_23_zigbee_8_parsers.ino | 88 ++- tasmota/xdrv_23_zigbee_9_impl.ino | 100 +++- 9 files changed, 563 insertions(+), 347 deletions(-) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index 60563404f..e12680d36 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -15,6 +15,7 @@ - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` - Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) +- Add Zigbee auto-responder for common attributes ### 8.3.1.1 20200518 diff --git a/tasmota/i18n.h b/tasmota/i18n.h index 073f537b2..01122fabd 100644 --- a/tasmota/i18n.h +++ b/tasmota/i18n.h @@ -528,6 +528,7 @@ #define D_CMND_ZIGBEE_SEND "Send" #define D_CMND_ZIGBEE_WRITE "Write" #define D_CMND_ZIGBEE_REPORT "Report" +#define D_CMND_ZIGBEE_RESPONSE "Response" #define D_JSON_ZIGBEE_ZCL_SENT "ZbZCLSent" #define D_JSON_ZIGBEE_RECEIVED "ZbReceived" #define D_CMND_ZIGBEE_BIND "Bind" diff --git a/tasmota/support_json.ino b/tasmota/support_json.ino index 51fa0e6a3..902a6e926 100644 --- a/tasmota/support_json.ino +++ b/tasmota/support_json.ino @@ -89,7 +89,7 @@ const JsonVariant &GetCaseInsensitive(const JsonObject &json, const char *needle // key can be in PROGMEM // if needle == "?" then we return the first valid key bool wildcard = strcmp_P("?", needle) == 0; - if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle))) { + if ((nullptr == &json) || (nullptr == needle) || (0 == pgm_read_byte(needle)) || (!json.success())) { return *(JsonVariant*)nullptr; } @@ -104,3 +104,9 @@ const JsonVariant &GetCaseInsensitive(const JsonObject &json, const char *needle // if not found return *(JsonVariant*)nullptr; } + +// This function returns true if the JsonObject contains the specified key +// It's just a wrapper to the previous function but it can be tricky to test nullptr on an object ref +bool HasKeyCaseInsensitive(const JsonObject &json, const char *needle) { + return &GetCaseInsensitive(json, needle) != nullptr; +} diff --git a/tasmota/xdrv_04_light.ino b/tasmota/xdrv_04_light.ino index 79e0510ad..1d6ac6a58 100644 --- a/tasmota/xdrv_04_light.ino +++ b/tasmota/xdrv_04_light.ino @@ -1429,6 +1429,10 @@ void LightGetHSB(uint16_t *hue, uint8_t *sat, uint8_t *bri) { light_state.getHSB(hue, sat, bri); } +void LightGetXY(float *X, float *Y) { + light_state.getXY(X, Y); +} + void LightHsToRgb(uint16_t hue, uint8_t sat, uint8_t *r_r, uint8_t *r_g, uint8_t *r_b) { light_state.HsToRgb(hue, sat, r_r, r_g, r_b); } diff --git a/tasmota/xdrv_23_zigbee_0_constants.ino b/tasmota/xdrv_23_zigbee_0_constants.ino index 28628f93c..57bf22499 100644 --- a/tasmota/xdrv_23_zigbee_0_constants.ino +++ b/tasmota/xdrv_23_zigbee_0_constants.ino @@ -384,7 +384,6 @@ enum ZCL_Global_Commands { ZCL_DEFAULT_RESPONSE = 0x0b, ZCL_DISCOVER_ATTRIBUTES = 0x0c, ZCL_DISCOVER_ATTRIBUTES_RESPONSE = 0x0d - }; #define ZF(s) static const char ZS_ ## s[] PROGMEM = #s; diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino index 60f116002..23a9343d4 100644 --- a/tasmota/xdrv_23_zigbee_2_devices.ino +++ b/tasmota/xdrv_23_zigbee_2_devices.ino @@ -526,6 +526,8 @@ void Z_Devices::addEndpoint(uint16_t shortaddr, uint8_t endpoint) { // Find the first endpoint of the device uint8_t Z_Devices::findFirstEndpoint(uint16_t shortaddr) const { + // When in router of end-device mode, the coordinator was not probed, in this case always talk to endpoint 1 + if (0x0000 == shortaddr) { return 1; } int32_t found = findShortAddr(shortaddr); if (found < 0) return 0; // avoid creating an entry if the device was never seen const Z_Device &device = devicesAt(found); diff --git a/tasmota/xdrv_23_zigbee_5_converters.ino b/tasmota/xdrv_23_zigbee_5_converters.ino index 5da74a1ed..bf508d9c2 100644 --- a/tasmota/xdrv_23_zigbee_5_converters.ino +++ b/tasmota/xdrv_23_zigbee_5_converters.ino @@ -89,7 +89,9 @@ typedef struct Z_AttributeConverter { uint8_t cluster_short; uint16_t attribute; const char * name; - Z_AttrConverter func; + int16_t multiplier; // multiplier for numerical value, (if > 0 multiply by x, if <0 device by x) + uint8_t cb; // callback func from Z_ConvOperators + // Z_AttrConverter func; } Z_AttributeConverter; // Cluster numbers are store in 8 bits format to save space, @@ -118,6 +120,16 @@ uint16_t CxToCluster(uint8_t cx) { return 0xFFFF; } +enum Z_ConvOperators { + Z_Nop, // copy value + Z_AddPressureUnit, // add pressure unit attribute (non numerical) + Z_ManufKeep, // copy and record Manufacturer attribute + Z_ModelKeep, // copy and record ModelId attribute + Z_AqaraSensor, // decode prioprietary Aqara Sensor message + Z_AqaraVibration, // decode Aqara vibration modes + Z_AqaraCube, // decode Aqara cube +}; + ZF(ZCLVersion) ZF(AppVersion) ZF(StackVersion) ZF(HWVersion) ZF(Manufacturer) ZF(ModelId) ZF(DateCode) ZF(PowerSource) ZF(SWBuildID) ZF(Power) ZF(SwitchType) ZF(Dimmer) ZF(MainsVoltage) ZF(MainsFrequency) ZF(BatteryVoltage) ZF(BatteryPercentage) @@ -209,356 +221,356 @@ ZF(SoftwareRevision) ZF(POD) ZF(AvailablePower) ZF(PowerThreshold) ZF(ProductRev ZF(NumberOfResets) ZF(PersistentMemoryWrites) ZF(LastMessageLQI) ZF(LastMessageRSSI) // list of post-processing directives const Z_AttributeConverter Z_PostProcess[] PROGMEM = { - { Zuint8, Cx0000, 0x0000, Z(ZCLVersion), &Z_Copy }, - { Zuint8, Cx0000, 0x0001, Z(AppVersion), &Z_Copy }, - { Zuint8, Cx0000, 0x0002, Z(StackVersion), &Z_Copy }, - { Zuint8, Cx0000, 0x0003, Z(HWVersion), &Z_Copy }, - { Zstring, Cx0000, 0x0004, Z(Manufacturer), &Z_ManufKeep }, // record Manufacturer - { Zstring, Cx0000, 0x0005, Z(ModelId), &Z_ModelKeep }, // record Model - { Zstring, Cx0000, 0x0006, Z(DateCode), &Z_Copy }, - { Zenum8, Cx0000, 0x0007, Z(PowerSource), &Z_Copy }, - { Zstring, Cx0000, 0x4000, Z(SWBuildID), &Z_Copy }, - { Zunk, Cx0000, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zuint8, Cx0000, 0x0000, Z(ZCLVersion), 1, Z_Nop }, + { Zuint8, Cx0000, 0x0001, Z(AppVersion), 1, Z_Nop }, + { Zuint8, Cx0000, 0x0002, Z(StackVersion), 1, Z_Nop }, + { Zuint8, Cx0000, 0x0003, Z(HWVersion), 1, Z_Nop }, + { Zstring, Cx0000, 0x0004, Z(Manufacturer), 1, Z_ManufKeep }, // record Manufacturer + { Zstring, Cx0000, 0x0005, Z(ModelId), 1, Z_ModelKeep }, // record Model + { Zstring, Cx0000, 0x0006, Z(DateCode), 1, Z_Nop }, + { Zenum8, Cx0000, 0x0007, Z(PowerSource), 1, Z_Nop }, + { Zstring, Cx0000, 0x4000, Z(SWBuildID), 1, Z_Nop }, + { Zunk, Cx0000, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Cmd 0x0A - Cluster 0x0000, attribute 0xFF01 - proprietary - { Zmap8, Cx0000, 0xFF01, nullptr, &Z_AqaraSensor }, // Occupancy (map8) + { Zmap8, Cx0000, 0xFF01, nullptr, 0, Z_AqaraSensor }, // Occupancy (map8) // Power Configuration cluster - { Zuint16, Cx0001, 0x0000, Z(MainsVoltage), &Z_Copy }, - { Zuint8, Cx0001, 0x0001, Z(MainsFrequency), &Z_Copy }, - { Zuint8, Cx0001, 0x0020, Z(BatteryVoltage), &Z_FloatDiv10 }, - { Zuint8, Cx0001, 0x0021, Z(BatteryPercentage), &Z_FloatDiv2 }, + { Zuint16, Cx0001, 0x0000, Z(MainsVoltage), 1, Z_Nop }, + { Zuint8, Cx0001, 0x0001, Z(MainsFrequency), 1, Z_Nop }, + { Zuint8, Cx0001, 0x0020, Z(BatteryVoltage), -10,Z_Nop }, // divide by 10 + { Zuint8, Cx0001, 0x0021, Z(BatteryPercentage), -2, Z_Nop }, // divide by 2 // Device Temperature Configuration cluster - { Zint16, Cx0002, 0x0000, Z(CurrentTemperature), &Z_Copy }, - { Zint16, Cx0002, 0x0001, Z(MinTempExperienced), &Z_Copy }, - { Zint16, Cx0002, 0x0002, Z(MaxTempExperienced), &Z_Copy }, - { Zuint16, Cx0002, 0x0003, Z(OverTempTotalDwell), &Z_Copy }, + { Zint16, Cx0002, 0x0000, Z(CurrentTemperature), 1, Z_Nop }, + { Zint16, Cx0002, 0x0001, Z(MinTempExperienced), 1, Z_Nop }, + { Zint16, Cx0002, 0x0002, Z(MaxTempExperienced), 1, Z_Nop }, + { Zuint16, Cx0002, 0x0003, Z(OverTempTotalDwell), 1, Z_Nop }, // Scenes cluster - { Zuint8, Cx0005, 0x0000, Z(SceneCount), &Z_Copy }, - { Zuint8, Cx0005, 0x0001, Z(CurrentScene), &Z_Copy }, - { Zuint16, Cx0005, 0x0002, Z(CurrentGroup), &Z_Copy }, - { Zbool, Cx0005, 0x0003, Z(SceneValid), &Z_Copy }, - //{ Zmap8, Cx0005, 0x0004, Z(NameSupport), &Z_Copy }, + { Zuint8, Cx0005, 0x0000, Z(SceneCount), 1, Z_Nop }, + { Zuint8, Cx0005, 0x0001, Z(CurrentScene), 1, Z_Nop }, + { Zuint16, Cx0005, 0x0002, Z(CurrentGroup), 1, Z_Nop }, + { Zbool, Cx0005, 0x0003, Z(SceneValid), 1, Z_Nop }, + //{ Zmap8, Cx0005, 0x0004, Z(NameSupport), 1, Z_Nop }, // On/off cluster - { Zbool, Cx0006, 0x0000, Z(Power), &Z_Copy }, - { Zbool, Cx0006, 0x8000, Z(Power), &Z_Copy }, // See 7280 + { Zbool, Cx0006, 0x0000, Z(Power), 1, Z_Nop }, + { Zbool, Cx0006, 0x8000, Z(Power), 1, Z_Nop }, // See 7280 // On/Off Switch Configuration cluster - { Zenum8, Cx0007, 0x0000, Z(SwitchType), &Z_Copy }, + { Zenum8, Cx0007, 0x0000, Z(SwitchType), 1, Z_Nop }, // Level Control cluster - { Zuint8, Cx0008, 0x0000, Z(Dimmer), &Z_Copy }, - // { Zuint16, Cx0008, 0x0001, Z(RemainingTime", &Z_Copy }, - // { Zuint16, Cx0008, 0x0010, Z(OnOffTransitionTime", &Z_Copy }, - // { Zuint8, Cx0008, 0x0011, Z(OnLevel", &Z_Copy }, - // { Zuint16, Cx0008, 0x0012, Z(OnTransitionTime", &Z_Copy }, - // { Zuint16, Cx0008, 0x0013, Z(OffTransitionTime", &Z_Copy }, - // { Zuint16, Cx0008, 0x0014, Z(DefaultMoveRate", &Z_Copy }, + { Zuint8, Cx0008, 0x0000, Z(Dimmer), 1, Z_Nop }, + // { Zuint16, Cx0008, 0x0001, Z(RemainingTime", 1, Z_Nop }, + // { Zuint16, Cx0008, 0x0010, Z(OnOffTransitionTime", 1, Z_Nop }, + // { Zuint8, Cx0008, 0x0011, Z(OnLevel", 1, Z_Nop }, + // { Zuint16, Cx0008, 0x0012, Z(OnTransitionTime", 1, Z_Nop }, + // { Zuint16, Cx0008, 0x0013, Z(OffTransitionTime", 1, Z_Nop }, + // { Zuint16, Cx0008, 0x0014, Z(DefaultMoveRate", 1, Z_Nop }, // Alarms cluster - { Zuint16, Cx0009, 0x0000, Z(AlarmCount), &Z_Copy }, + { Zuint16, Cx0009, 0x0000, Z(AlarmCount), 1, Z_Nop }, // Time cluster - { ZUTC, Cx000A, 0x0000, Z(Time), &Z_Copy }, - { Zmap8, Cx000A, 0x0001, Z(TimeStatus), &Z_Copy }, - { Zint32, Cx000A, 0x0002, Z(TimeZone), &Z_Copy }, - { Zuint32, Cx000A, 0x0003, Z(DstStart), &Z_Copy }, - { Zuint32, Cx000A, 0x0004, Z(DstEnd), &Z_Copy }, - { Zint32, Cx000A, 0x0005, Z(DstShift), &Z_Copy }, - { Zuint32, Cx000A, 0x0006, Z(StandardTime), &Z_Copy }, - { Zuint32, Cx000A, 0x0007, Z(LocalTime), &Z_Copy }, - { ZUTC, Cx000A, 0x0008, Z(LastSetTime), &Z_Copy }, - { ZUTC, Cx000A, 0x0009, Z(ValidUntilTime), &Z_Copy }, + { ZUTC, Cx000A, 0x0000, Z(Time), 1, Z_Nop }, + { Zmap8, Cx000A, 0x0001, Z(TimeStatus), 1, Z_Nop }, + { Zint32, Cx000A, 0x0002, Z(TimeZone), 1, Z_Nop }, + { Zuint32, Cx000A, 0x0003, Z(DstStart), 1, Z_Nop }, + { Zuint32, Cx000A, 0x0004, Z(DstEnd), 1, Z_Nop }, + { Zint32, Cx000A, 0x0005, Z(DstShift), 1, Z_Nop }, + { Zuint32, Cx000A, 0x0006, Z(StandardTime), 1, Z_Nop }, + { Zuint32, Cx000A, 0x0007, Z(LocalTime), 1, Z_Nop }, + { ZUTC, Cx000A, 0x0008, Z(LastSetTime), 1, Z_Nop }, + { ZUTC, Cx000A, 0x0009, Z(ValidUntilTime), 1, Z_Nop }, // RSSI Location cluster - { Zdata8, Cx000B, 0x0000, Z(LocationType), &Z_Copy }, - { Zenum8, Cx000B, 0x0001, Z(LocationMethod), &Z_Copy }, - { Zuint16, Cx000B, 0x0002, Z(LocationAge), &Z_Copy }, - { Zuint8, Cx000B, 0x0003, Z(QualityMeasure), &Z_Copy }, - { Zuint8, Cx000B, 0x0004, Z(NumberOfDevices), &Z_Copy }, + { Zdata8, Cx000B, 0x0000, Z(LocationType), 1, Z_Nop }, + { Zenum8, Cx000B, 0x0001, Z(LocationMethod), 1, Z_Nop }, + { Zuint16, Cx000B, 0x0002, Z(LocationAge), 1, Z_Nop }, + { Zuint8, Cx000B, 0x0003, Z(QualityMeasure), 1, Z_Nop }, + { Zuint8, Cx000B, 0x0004, Z(NumberOfDevices), 1, Z_Nop }, // Analog Input cluster - // { 0xFF, Cx000C, 0x0004, Z(AnalogInActiveText), &Z_Copy }, - { Zstring, Cx000C, 0x001C, Z(AnalogInDescription), &Z_Copy }, - // { 0xFF, Cx000C, 0x002E, Z(AnalogInInactiveText), &Z_Copy }, - { Zsingle, Cx000C, 0x0041, Z(AnalogInMaxValue), &Z_Copy }, - { Zsingle, Cx000C, 0x0045, Z(AnalogInMinValue), &Z_Copy }, - { Zbool, Cx000C, 0x0051, Z(AnalogInOutOfService), &Z_Copy }, - { Zsingle, Cx000C, 0x0055, Z(AqaraRotate), &Z_Copy }, - // { 0xFF, Cx000C, 0x0057, Z(AnalogInPriorityArray),&Z_Copy }, - { Zenum8, Cx000C, 0x0067, Z(AnalogInReliability), &Z_Copy }, - // { 0xFF, Cx000C, 0x0068, Z(AnalogInRelinquishDefault),&Z_Copy }, - { Zsingle, Cx000C, 0x006A, Z(AnalogInResolution), &Z_Copy }, - { Zmap8, Cx000C, 0x006F, Z(AnalogInStatusFlags), &Z_Copy }, - { Zenum16, Cx000C, 0x0075, Z(AnalogInEngineeringUnits),&Z_Copy }, - { Zuint32, Cx000C, 0x0100, Z(AnalogInApplicationType),&Z_Copy }, - { Zuint16, Cx000C, 0xFF05, Z(Aqara_FF05), &Z_Copy }, + // { 0xFF, Cx000C, 0x0004, Z(AnalogInActiveText), 1, Z_Nop }, + { Zstring, Cx000C, 0x001C, Z(AnalogInDescription), 1, Z_Nop }, + // { 0xFF, Cx000C, 0x002E, Z(AnalogInInactiveText), 1, Z_Nop }, + { Zsingle, Cx000C, 0x0041, Z(AnalogInMaxValue), 1, Z_Nop }, + { Zsingle, Cx000C, 0x0045, Z(AnalogInMinValue), 1, Z_Nop }, + { Zbool, Cx000C, 0x0051, Z(AnalogInOutOfService), 1, Z_Nop }, + { Zsingle, Cx000C, 0x0055, Z(AqaraRotate), 1, Z_Nop }, + // { 0xFF, Cx000C, 0x0057, Z(AnalogInPriorityArray),1, Z_Nop }, + { Zenum8, Cx000C, 0x0067, Z(AnalogInReliability), 1, Z_Nop }, + // { 0xFF, Cx000C, 0x0068, Z(AnalogInRelinquishDefault),1, Z_Nop }, + { Zsingle, Cx000C, 0x006A, Z(AnalogInResolution), 1, Z_Nop }, + { Zmap8, Cx000C, 0x006F, Z(AnalogInStatusFlags), 1, Z_Nop }, + { Zenum16, Cx000C, 0x0075, Z(AnalogInEngineeringUnits),1, Z_Nop }, + { Zuint32, Cx000C, 0x0100, Z(AnalogInApplicationType),1, Z_Nop }, + { Zuint16, Cx000C, 0xFF05, Z(Aqara_FF05), 1, Z_Nop }, // Analog Output cluster - { Zstring, Cx000D, 0x001C, Z(AnalogOutDescription), &Z_Copy }, - { Zsingle, Cx000D, 0x0041, Z(AnalogOutMaxValue), &Z_Copy }, - { Zsingle, Cx000D, 0x0045, Z(AnalogOutMinValue), &Z_Copy }, - { Zbool, Cx000D, 0x0051, Z(AnalogOutOutOfService),&Z_Copy }, - { Zsingle, Cx000D, 0x0055, Z(AnalogOutValue), &Z_Copy }, - // { Zunk, Cx000D, 0x0057, Z(AnalogOutPriorityArray),&Z_Copy }, - { Zenum8, Cx000D, 0x0067, Z(AnalogOutReliability), &Z_Copy }, - { Zsingle, Cx000D, 0x0068, Z(AnalogOutRelinquishDefault),&Z_Copy }, - { Zsingle, Cx000D, 0x006A, Z(AnalogOutResolution), &Z_Copy }, - { Zmap8, Cx000D, 0x006F, Z(AnalogOutStatusFlags), &Z_Copy }, - { Zenum16, Cx000D, 0x0075, Z(AnalogOutEngineeringUnits),&Z_Copy }, - { Zuint32, Cx000D, 0x0100, Z(AnalogOutApplicationType),&Z_Copy }, + { Zstring, Cx000D, 0x001C, Z(AnalogOutDescription), 1, Z_Nop }, + { Zsingle, Cx000D, 0x0041, Z(AnalogOutMaxValue), 1, Z_Nop }, + { Zsingle, Cx000D, 0x0045, Z(AnalogOutMinValue), 1, Z_Nop }, + { Zbool, Cx000D, 0x0051, Z(AnalogOutOutOfService),1, Z_Nop }, + { Zsingle, Cx000D, 0x0055, Z(AnalogOutValue), 1, Z_Nop }, + // { Zunk, Cx000D, 0x0057, Z(AnalogOutPriorityArray),1, Z_Nop }, + { Zenum8, Cx000D, 0x0067, Z(AnalogOutReliability), 1, Z_Nop }, + { Zsingle, Cx000D, 0x0068, Z(AnalogOutRelinquishDefault),1, Z_Nop }, + { Zsingle, Cx000D, 0x006A, Z(AnalogOutResolution), 1, Z_Nop }, + { Zmap8, Cx000D, 0x006F, Z(AnalogOutStatusFlags), 1, Z_Nop }, + { Zenum16, Cx000D, 0x0075, Z(AnalogOutEngineeringUnits),1, Z_Nop }, + { Zuint32, Cx000D, 0x0100, Z(AnalogOutApplicationType),1, Z_Nop }, // Analog Value cluster - { Zstring, Cx000E, 0x001C, Z(AnalogDescription), &Z_Copy }, - { Zbool, Cx000E, 0x0051, Z(AnalogOutOfService), &Z_Copy }, - { Zsingle, Cx000E, 0x0055, Z(AnalogValue), &Z_Copy }, - { Zunk, Cx000E, 0x0057, Z(AnalogPriorityArray), &Z_Copy }, - { Zenum8, Cx000E, 0x0067, Z(AnalogReliability), &Z_Copy }, - { Zsingle, Cx000E, 0x0068, Z(AnalogRelinquishDefault),&Z_Copy }, - { Zmap8, Cx000E, 0x006F, Z(AnalogStatusFlags), &Z_Copy }, - { Zenum16, Cx000E, 0x0075, Z(AnalogEngineeringUnits),&Z_Copy }, - { Zuint32, Cx000E, 0x0100, Z(AnalogApplicationType),&Z_Copy }, + { Zstring, Cx000E, 0x001C, Z(AnalogDescription), 1, Z_Nop }, + { Zbool, Cx000E, 0x0051, Z(AnalogOutOfService), 1, Z_Nop }, + { Zsingle, Cx000E, 0x0055, Z(AnalogValue), 1, Z_Nop }, + { Zunk, Cx000E, 0x0057, Z(AnalogPriorityArray), 1, Z_Nop }, + { Zenum8, Cx000E, 0x0067, Z(AnalogReliability), 1, Z_Nop }, + { Zsingle, Cx000E, 0x0068, Z(AnalogRelinquishDefault),1, Z_Nop }, + { Zmap8, Cx000E, 0x006F, Z(AnalogStatusFlags), 1, Z_Nop }, + { Zenum16, Cx000E, 0x0075, Z(AnalogEngineeringUnits),1, Z_Nop }, + { Zuint32, Cx000E, 0x0100, Z(AnalogApplicationType),1, Z_Nop }, // Binary Input cluster - { Zstring, Cx000F, 0x0004, Z(BinaryInActiveText), &Z_Copy }, - { Zstring, Cx000F, 0x001C, Z(BinaryInDescription), &Z_Copy }, - { Zstring, Cx000F, 0x002E, Z(BinaryInInactiveText),&Z_Copy }, - { Zbool, Cx000F, 0x0051, Z(BinaryInOutOfService),&Z_Copy }, - { Zenum8, Cx000F, 0x0054, Z(BinaryInPolarity), &Z_Copy }, - { Zstring, Cx000F, 0x0055, Z(BinaryInValue), &Z_Copy }, - // { 0xFF, Cx000F, 0x0057, Z(BinaryInPriorityArray),&Z_Copy }, - { Zenum8, Cx000F, 0x0067, Z(BinaryInReliability), &Z_Copy }, - { Zmap8, Cx000F, 0x006F, Z(BinaryInStatusFlags), &Z_Copy }, - { Zuint32, Cx000F, 0x0100, Z(BinaryInApplicationType),&Z_Copy }, + { Zstring, Cx000F, 0x0004, Z(BinaryInActiveText), 1, Z_Nop }, + { Zstring, Cx000F, 0x001C, Z(BinaryInDescription), 1, Z_Nop }, + { Zstring, Cx000F, 0x002E, Z(BinaryInInactiveText),1, Z_Nop }, + { Zbool, Cx000F, 0x0051, Z(BinaryInOutOfService),1, Z_Nop }, + { Zenum8, Cx000F, 0x0054, Z(BinaryInPolarity), 1, Z_Nop }, + { Zstring, Cx000F, 0x0055, Z(BinaryInValue), 1, Z_Nop }, + // { 0xFF, Cx000F, 0x0057, Z(BinaryInPriorityArray),1, Z_Nop }, + { Zenum8, Cx000F, 0x0067, Z(BinaryInReliability), 1, Z_Nop }, + { Zmap8, Cx000F, 0x006F, Z(BinaryInStatusFlags), 1, Z_Nop }, + { Zuint32, Cx000F, 0x0100, Z(BinaryInApplicationType),1, Z_Nop }, // Binary Output cluster - { Zstring, Cx0010, 0x0004, Z(BinaryOutActiveText), &Z_Copy }, - { Zstring, Cx0010, 0x001C, Z(BinaryOutDescription), &Z_Copy }, - { Zstring, Cx0010, 0x002E, Z(BinaryOutInactiveText),&Z_Copy }, - { Zuint32, Cx0010, 0x0042, Z(BinaryOutMinimumOffTime),&Z_Copy }, - { Zuint32, Cx0010, 0x0043, Z(BinaryOutMinimumOnTime),&Z_Copy }, - { Zbool, Cx0010, 0x0051, Z(BinaryOutOutOfService),&Z_Copy }, - { Zenum8, Cx0010, 0x0054, Z(BinaryOutPolarity), &Z_Copy }, - { Zbool, Cx0010, 0x0055, Z(BinaryOutValue), &Z_Copy }, - // { Zunk, Cx0010, 0x0057, Z(BinaryOutPriorityArray),&Z_Copy }, - { Zenum8, Cx0010, 0x0067, Z(BinaryOutReliability), &Z_Copy }, - { Zbool, Cx0010, 0x0068, Z(BinaryOutRelinquishDefault),&Z_Copy }, - { Zmap8, Cx0010, 0x006F, Z(BinaryOutStatusFlags), &Z_Copy }, - { Zuint32, Cx0010, 0x0100, Z(BinaryOutApplicationType),&Z_Copy }, + { Zstring, Cx0010, 0x0004, Z(BinaryOutActiveText), 1, Z_Nop }, + { Zstring, Cx0010, 0x001C, Z(BinaryOutDescription), 1, Z_Nop }, + { Zstring, Cx0010, 0x002E, Z(BinaryOutInactiveText),1, Z_Nop }, + { Zuint32, Cx0010, 0x0042, Z(BinaryOutMinimumOffTime),1, Z_Nop }, + { Zuint32, Cx0010, 0x0043, Z(BinaryOutMinimumOnTime),1, Z_Nop }, + { Zbool, Cx0010, 0x0051, Z(BinaryOutOutOfService),1, Z_Nop }, + { Zenum8, Cx0010, 0x0054, Z(BinaryOutPolarity), 1, Z_Nop }, + { Zbool, Cx0010, 0x0055, Z(BinaryOutValue), 1, Z_Nop }, + // { Zunk, Cx0010, 0x0057, Z(BinaryOutPriorityArray),1, Z_Nop }, + { Zenum8, Cx0010, 0x0067, Z(BinaryOutReliability), 1, Z_Nop }, + { Zbool, Cx0010, 0x0068, Z(BinaryOutRelinquishDefault),1, Z_Nop }, + { Zmap8, Cx0010, 0x006F, Z(BinaryOutStatusFlags), 1, Z_Nop }, + { Zuint32, Cx0010, 0x0100, Z(BinaryOutApplicationType),1, Z_Nop }, // Binary Value cluster - { Zstring, Cx0011, 0x0004, Z(BinaryActiveText), &Z_Copy }, - { Zstring, Cx0011, 0x001C, Z(BinaryDescription), &Z_Copy }, - { Zstring, Cx0011, 0x002E, Z(BinaryInactiveText), &Z_Copy }, - { Zuint32, Cx0011, 0x0042, Z(BinaryMinimumOffTime), &Z_Copy }, - { Zuint32, Cx0011, 0x0043, Z(BinaryMinimumOnTime), &Z_Copy }, - { Zbool, Cx0011, 0x0051, Z(BinaryOutOfService), &Z_Copy }, - { Zbool, Cx0011, 0x0055, Z(BinaryValue), &Z_Copy }, - // { Zunk, Cx0011, 0x0057, Z(BinaryPriorityArray), &Z_Copy }, - { Zenum8, Cx0011, 0x0067, Z(BinaryReliability), &Z_Copy }, - { Zbool, Cx0011, 0x0068, Z(BinaryRelinquishDefault),&Z_Copy }, - { Zmap8, Cx0011, 0x006F, Z(BinaryStatusFlags), &Z_Copy }, - { Zuint32, Cx0011, 0x0100, Z(BinaryApplicationType),&Z_Copy }, + { Zstring, Cx0011, 0x0004, Z(BinaryActiveText), 1, Z_Nop }, + { Zstring, Cx0011, 0x001C, Z(BinaryDescription), 1, Z_Nop }, + { Zstring, Cx0011, 0x002E, Z(BinaryInactiveText), 1, Z_Nop }, + { Zuint32, Cx0011, 0x0042, Z(BinaryMinimumOffTime), 1, Z_Nop }, + { Zuint32, Cx0011, 0x0043, Z(BinaryMinimumOnTime), 1, Z_Nop }, + { Zbool, Cx0011, 0x0051, Z(BinaryOutOfService), 1, Z_Nop }, + { Zbool, Cx0011, 0x0055, Z(BinaryValue), 1, Z_Nop }, + // { Zunk, Cx0011, 0x0057, Z(BinaryPriorityArray), 1, Z_Nop }, + { Zenum8, Cx0011, 0x0067, Z(BinaryReliability), 1, Z_Nop }, + { Zbool, Cx0011, 0x0068, Z(BinaryRelinquishDefault),1, Z_Nop }, + { Zmap8, Cx0011, 0x006F, Z(BinaryStatusFlags), 1, Z_Nop }, + { Zuint32, Cx0011, 0x0100, Z(BinaryApplicationType),1, Z_Nop }, // Multistate Input cluster - // { Zunk, Cx0012, 0x000E, Z(MultiInStateText), &Z_Copy }, - { Zstring, Cx0012, 0x001C, Z(MultiInDescription), &Z_Copy }, - { Zuint16, Cx0012, 0x004A, Z(MultiInNumberOfStates),&Z_Copy }, - { Zbool, Cx0012, 0x0051, Z(MultiInOutOfService), &Z_Copy }, - { Zuint16, Cx0012, 0x0055, Z(MultiInValue), &Z_AqaraCube }, - { Zenum8, Cx0012, 0x0067, Z(MultiInReliability), &Z_Copy }, - { Zmap8, Cx0012, 0x006F, Z(MultiInStatusFlags), &Z_Copy }, - { Zuint32, Cx0012, 0x0100, Z(MultiInApplicationType),&Z_Copy }, + // { Zunk, Cx0012, 0x000E, Z(MultiInStateText), 1, Z_Nop }, + { Zstring, Cx0012, 0x001C, Z(MultiInDescription), 1, Z_Nop }, + { Zuint16, Cx0012, 0x004A, Z(MultiInNumberOfStates),1, Z_Nop }, + { Zbool, Cx0012, 0x0051, Z(MultiInOutOfService), 1, Z_Nop }, + { Zuint16, Cx0012, 0x0055, Z(MultiInValue), 0, Z_AqaraCube }, + { Zenum8, Cx0012, 0x0067, Z(MultiInReliability), 1, Z_Nop }, + { Zmap8, Cx0012, 0x006F, Z(MultiInStatusFlags), 1, Z_Nop }, + { Zuint32, Cx0012, 0x0100, Z(MultiInApplicationType),1, Z_Nop }, // Multistate output - // { Zunk, Cx0013, 0x000E, Z(MultiOutStateText), &Z_Copy }, - { Zstring, Cx0013, 0x001C, Z(MultiOutDescription), &Z_Copy }, - { Zuint16, Cx0013, 0x004A, Z(MultiOutNumberOfStates),&Z_Copy }, - { Zbool, Cx0013, 0x0051, Z(MultiOutOutOfService), &Z_Copy }, - { Zuint16, Cx0013, 0x0055, Z(MultiOutValue), &Z_Copy }, - // { Zunk, Cx0013, 0x0057, Z(MultiOutPriorityArray),&Z_Copy }, - { Zenum8, Cx0013, 0x0067, Z(MultiOutReliability), &Z_Copy }, - { Zuint16, Cx0013, 0x0068, Z(MultiOutRelinquishDefault),&Z_Copy }, - { Zmap8, Cx0013, 0x006F, Z(MultiOutStatusFlags), &Z_Copy }, - { Zuint32, Cx0013, 0x0100, Z(MultiOutApplicationType),&Z_Copy }, + // { Zunk, Cx0013, 0x000E, Z(MultiOutStateText), 1, Z_Nop }, + { Zstring, Cx0013, 0x001C, Z(MultiOutDescription), 1, Z_Nop }, + { Zuint16, Cx0013, 0x004A, Z(MultiOutNumberOfStates),1, Z_Nop }, + { Zbool, Cx0013, 0x0051, Z(MultiOutOutOfService), 1, Z_Nop }, + { Zuint16, Cx0013, 0x0055, Z(MultiOutValue), 1, Z_Nop }, + // { Zunk, Cx0013, 0x0057, Z(MultiOutPriorityArray),1, Z_Nop }, + { Zenum8, Cx0013, 0x0067, Z(MultiOutReliability), 1, Z_Nop }, + { Zuint16, Cx0013, 0x0068, Z(MultiOutRelinquishDefault),1, Z_Nop }, + { Zmap8, Cx0013, 0x006F, Z(MultiOutStatusFlags), 1, Z_Nop }, + { Zuint32, Cx0013, 0x0100, Z(MultiOutApplicationType),1, Z_Nop }, // Multistate Value cluster - // { Zunk, Cx0014, 0x000E, Z(MultiStateText), &Z_Copy }, - { Zstring, Cx0014, 0x001C, Z(MultiDescription), &Z_Copy }, - { Zuint16, Cx0014, 0x004A, Z(MultiNumberOfStates), &Z_Copy }, - { Zbool, Cx0014, 0x0051, Z(MultiOutOfService), &Z_Copy }, - { Zuint16, Cx0014, 0x0055, Z(MultiValue), &Z_Copy }, - { Zenum8, Cx0014, 0x0067, Z(MultiReliability), &Z_Copy }, - { Zuint16, Cx0014, 0x0068, Z(MultiRelinquishDefault),&Z_Copy }, - { Zmap8, Cx0014, 0x006F, Z(MultiStatusFlags), &Z_Copy }, - { Zuint32, Cx0014, 0x0100, Z(MultiApplicationType), &Z_Copy }, + // { Zunk, Cx0014, 0x000E, Z(MultiStateText), 1, Z_Nop }, + { Zstring, Cx0014, 0x001C, Z(MultiDescription), 1, Z_Nop }, + { Zuint16, Cx0014, 0x004A, Z(MultiNumberOfStates), 1, Z_Nop }, + { Zbool, Cx0014, 0x0051, Z(MultiOutOfService), 1, Z_Nop }, + { Zuint16, Cx0014, 0x0055, Z(MultiValue), 1, Z_Nop }, + { Zenum8, Cx0014, 0x0067, Z(MultiReliability), 1, Z_Nop }, + { Zuint16, Cx0014, 0x0068, Z(MultiRelinquishDefault),1, Z_Nop }, + { Zmap8, Cx0014, 0x006F, Z(MultiStatusFlags), 1, Z_Nop }, + { Zuint32, Cx0014, 0x0100, Z(MultiApplicationType), 1, Z_Nop }, // Power Profile cluster - { Zuint8, Cx001A, 0x0000, Z(TotalProfileNum), &Z_Copy }, - { Zbool, Cx001A, 0x0001, Z(MultipleScheduling), &Z_Copy }, - { Zmap8, Cx001A, 0x0002, Z(EnergyFormatting), &Z_Copy }, - { Zbool, Cx001A, 0x0003, Z(EnergyRemote), &Z_Copy }, - { Zmap8, Cx001A, 0x0004, Z(ScheduleMode), &Z_Copy }, + { Zuint8, Cx001A, 0x0000, Z(TotalProfileNum), 1, Z_Nop }, + { Zbool, Cx001A, 0x0001, Z(MultipleScheduling), 1, Z_Nop }, + { Zmap8, Cx001A, 0x0002, Z(EnergyFormatting), 1, Z_Nop }, + { Zbool, Cx001A, 0x0003, Z(EnergyRemote), 1, Z_Nop }, + { Zmap8, Cx001A, 0x0004, Z(ScheduleMode), 1, Z_Nop }, // Poll Control cluster - { Zuint32, Cx0020, 0x0000, Z(CheckinInterval), &Z_Copy }, - { Zuint32, Cx0020, 0x0001, Z(LongPollInterval), &Z_Copy }, - { Zuint16, Cx0020, 0x0002, Z(ShortPollInterval), &Z_Copy }, - { Zuint16, Cx0020, 0x0003, Z(FastPollTimeout), &Z_Copy }, - { Zuint32, Cx0020, 0x0004, Z(CheckinIntervalMin), &Z_Copy }, - { Zuint32, Cx0020, 0x0005, Z(LongPollIntervalMin), &Z_Copy }, - { Zuint16, Cx0020, 0x0006, Z(FastPollTimeoutMax), &Z_Copy }, + { Zuint32, Cx0020, 0x0000, Z(CheckinInterval), 1, Z_Nop }, + { Zuint32, Cx0020, 0x0001, Z(LongPollInterval), 1, Z_Nop }, + { Zuint16, Cx0020, 0x0002, Z(ShortPollInterval), 1, Z_Nop }, + { Zuint16, Cx0020, 0x0003, Z(FastPollTimeout), 1, Z_Nop }, + { Zuint32, Cx0020, 0x0004, Z(CheckinIntervalMin), 1, Z_Nop }, + { Zuint32, Cx0020, 0x0005, Z(LongPollIntervalMin), 1, Z_Nop }, + { Zuint16, Cx0020, 0x0006, Z(FastPollTimeoutMax), 1, Z_Nop }, // Shade Configuration cluster - { Zuint16, Cx0100, 0x0000, Z(PhysicalClosedLimit), &Z_Copy }, - { Zuint8, Cx0100, 0x0001, Z(MotorStepSize), &Z_Copy }, - { Zmap8, Cx0100, 0x0002, Z(Status), &Z_Copy }, - { Zuint16, Cx0100, 0x0010, Z(ClosedLimit), &Z_Copy }, - { Zenum8, Cx0100, 0x0011, Z(Mode), &Z_Copy }, + { Zuint16, Cx0100, 0x0000, Z(PhysicalClosedLimit), 1, Z_Nop }, + { Zuint8, Cx0100, 0x0001, Z(MotorStepSize), 1, Z_Nop }, + { Zmap8, Cx0100, 0x0002, Z(Status), 1, Z_Nop }, + { Zuint16, Cx0100, 0x0010, Z(ClosedLimit), 1, Z_Nop }, + { Zenum8, Cx0100, 0x0011, Z(Mode), 1, Z_Nop }, // Door Lock cluster - { Zenum8, Cx0101, 0x0000, Z(LockState), &Z_Copy }, - { Zenum8, Cx0101, 0x0001, Z(LockType), &Z_Copy }, - { Zbool, Cx0101, 0x0002, Z(ActuatorEnabled), &Z_Copy }, - { Zenum8, Cx0101, 0x0003, Z(DoorState), &Z_Copy }, - { Zuint32, Cx0101, 0x0004, Z(DoorOpenEvents), &Z_Copy }, - { Zuint32, Cx0101, 0x0005, Z(DoorClosedEvents), &Z_Copy }, - { Zuint16, Cx0101, 0x0006, Z(OpenPeriod), &Z_Copy }, + { Zenum8, Cx0101, 0x0000, Z(LockState), 1, Z_Nop }, + { Zenum8, Cx0101, 0x0001, Z(LockType), 1, Z_Nop }, + { Zbool, Cx0101, 0x0002, Z(ActuatorEnabled), 1, Z_Nop }, + { Zenum8, Cx0101, 0x0003, Z(DoorState), 1, Z_Nop }, + { Zuint32, Cx0101, 0x0004, Z(DoorOpenEvents), 1, Z_Nop }, + { Zuint32, Cx0101, 0x0005, Z(DoorClosedEvents), 1, Z_Nop }, + { Zuint16, Cx0101, 0x0006, Z(OpenPeriod), 1, Z_Nop }, // Aqara Lumi Vibration Sensor - { Zuint16, Cx0101, 0x0055, Z(AqaraVibrationMode), &Z_AqaraVibration }, - { Zuint16, Cx0101, 0x0503, Z(AqaraVibrationsOrAngle), &Z_Copy }, - { Zuint32, Cx0101, 0x0505, Z(AqaraVibration505), &Z_Copy }, - { Zuint48, Cx0101, 0x0508, Z(AqaraAccelerometer), &Z_AqaraVibration }, + { Zuint16, Cx0101, 0x0055, Z(AqaraVibrationMode), 0, Z_AqaraVibration }, + { Zuint16, Cx0101, 0x0503, Z(AqaraVibrationsOrAngle), 1, Z_Nop }, + { Zuint32, Cx0101, 0x0505, Z(AqaraVibration505), 1, Z_Nop }, + { Zuint48, Cx0101, 0x0508, Z(AqaraAccelerometer), 0, Z_AqaraVibration }, // Window Covering cluster - { Zenum8, Cx0102, 0x0000, Z(WindowCoveringType), &Z_Copy }, - { Zuint16, Cx0102, 0x0001, Z(PhysicalClosedLimitLift),&Z_Copy }, - { Zuint16, Cx0102, 0x0002, Z(PhysicalClosedLimitTilt),&Z_Copy }, - { Zuint16, Cx0102, 0x0003, Z(CurrentPositionLift), &Z_Copy }, - { Zuint16, Cx0102, 0x0004, Z(CurrentPositionTilt), &Z_Copy }, - { Zuint16, Cx0102, 0x0005, Z(NumberofActuationsLift),&Z_Copy }, - { Zuint16, Cx0102, 0x0006, Z(NumberofActuationsTilt),&Z_Copy }, - { Zmap8, Cx0102, 0x0007, Z(ConfigStatus), &Z_Copy }, - { Zuint8, Cx0102, 0x0008, Z(CurrentPositionLiftPercentage),&Z_Copy }, - { Zuint8, Cx0102, 0x0009, Z(CurrentPositionTiltPercentage),&Z_Copy }, - { Zuint16, Cx0102, 0x0010, Z(InstalledOpenLimitLift),&Z_Copy }, - { Zuint16, Cx0102, 0x0011, Z(InstalledClosedLimitLift),&Z_Copy }, - { Zuint16, Cx0102, 0x0012, Z(InstalledOpenLimitTilt),&Z_Copy }, - { Zuint16, Cx0102, 0x0013, Z(InstalledClosedLimitTilt),&Z_Copy }, - { Zuint16, Cx0102, 0x0014, Z(VelocityLift), &Z_Copy }, - { Zuint16, Cx0102, 0x0015, Z(AccelerationTimeLift),&Z_Copy }, - { Zuint16, Cx0102, 0x0016, Z(DecelerationTimeLift), &Z_Copy }, - { Zmap8, Cx0102, 0x0017, Z(Mode), &Z_Copy }, - { Zoctstr, Cx0102, 0x0018, Z(IntermediateSetpointsLift),&Z_Copy }, - { Zoctstr, Cx0102, 0x0019, Z(IntermediateSetpointsTilt),&Z_Copy }, + { Zenum8, Cx0102, 0x0000, Z(WindowCoveringType), 1, Z_Nop }, + { Zuint16, Cx0102, 0x0001, Z(PhysicalClosedLimitLift),1, Z_Nop }, + { Zuint16, Cx0102, 0x0002, Z(PhysicalClosedLimitTilt),1, Z_Nop }, + { Zuint16, Cx0102, 0x0003, Z(CurrentPositionLift), 1, Z_Nop }, + { Zuint16, Cx0102, 0x0004, Z(CurrentPositionTilt), 1, Z_Nop }, + { Zuint16, Cx0102, 0x0005, Z(NumberofActuationsLift),1, Z_Nop }, + { Zuint16, Cx0102, 0x0006, Z(NumberofActuationsTilt),1, Z_Nop }, + { Zmap8, Cx0102, 0x0007, Z(ConfigStatus), 1, Z_Nop }, + { Zuint8, Cx0102, 0x0008, Z(CurrentPositionLiftPercentage),1, Z_Nop }, + { Zuint8, Cx0102, 0x0009, Z(CurrentPositionTiltPercentage),1, Z_Nop }, + { Zuint16, Cx0102, 0x0010, Z(InstalledOpenLimitLift),1, Z_Nop }, + { Zuint16, Cx0102, 0x0011, Z(InstalledClosedLimitLift),1, Z_Nop }, + { Zuint16, Cx0102, 0x0012, Z(InstalledOpenLimitTilt),1, Z_Nop }, + { Zuint16, Cx0102, 0x0013, Z(InstalledClosedLimitTilt),1, Z_Nop }, + { Zuint16, Cx0102, 0x0014, Z(VelocityLift), 1, Z_Nop }, + { Zuint16, Cx0102, 0x0015, Z(AccelerationTimeLift),1, Z_Nop }, + { Zuint16, Cx0102, 0x0016, Z(DecelerationTimeLift), 1, Z_Nop }, + { Zmap8, Cx0102, 0x0017, Z(Mode), 1, Z_Nop }, + { Zoctstr, Cx0102, 0x0018, Z(IntermediateSetpointsLift),1, Z_Nop }, + { Zoctstr, Cx0102, 0x0019, Z(IntermediateSetpointsTilt),1, Z_Nop }, // Color Control cluster - { Zuint8, Cx0300, 0x0000, Z(Hue), &Z_Copy }, - { Zuint8, Cx0300, 0x0001, Z(Sat), &Z_Copy }, - { Zuint16, Cx0300, 0x0002, Z(RemainingTime), &Z_Copy }, - { Zuint16, Cx0300, 0x0003, Z(X), &Z_Copy }, - { Zuint16, Cx0300, 0x0004, Z(Y), &Z_Copy }, - { Zenum8, Cx0300, 0x0005, Z(DriftCompensation), &Z_Copy }, - { Zstring, Cx0300, 0x0006, Z(CompensationText), &Z_Copy }, - { Zuint16, Cx0300, 0x0007, Z(CT), &Z_Copy }, - { Zenum8, Cx0300, 0x0008, Z(ColorMode), &Z_Copy }, - { Zuint8, Cx0300, 0x0010, Z(NumberOfPrimaries), &Z_Copy }, - { Zuint16, Cx0300, 0x0011, Z(Primary1X), &Z_Copy }, - { Zuint16, Cx0300, 0x0012, Z(Primary1Y), &Z_Copy }, - { Zuint8, Cx0300, 0x0013, Z(Primary1Intensity), &Z_Copy }, - { Zuint16, Cx0300, 0x0015, Z(Primary2X), &Z_Copy }, - { Zuint16, Cx0300, 0x0016, Z(Primary2Y), &Z_Copy }, - { Zuint8, Cx0300, 0x0017, Z(Primary2Intensity), &Z_Copy }, - { Zuint16, Cx0300, 0x0019, Z(Primary3X), &Z_Copy }, - { Zuint16, Cx0300, 0x001A, Z(Primary3Y), &Z_Copy }, - { Zuint8, Cx0300, 0x001B, Z(Primary3Intensity), &Z_Copy }, - { Zuint16, Cx0300, 0x0030, Z(WhitePointX), &Z_Copy }, - { Zuint16, Cx0300, 0x0031, Z(WhitePointY), &Z_Copy }, - { Zuint16, Cx0300, 0x0032, Z(ColorPointRX), &Z_Copy }, - { Zuint16, Cx0300, 0x0033, Z(ColorPointRY), &Z_Copy }, - { Zuint8, Cx0300, 0x0034, Z(ColorPointRIntensity), &Z_Copy }, - { Zuint16, Cx0300, 0x0036, Z(ColorPointGX), &Z_Copy }, - { Zuint16, Cx0300, 0x0037, Z(ColorPointGY), &Z_Copy }, - { Zuint8, Cx0300, 0x0038, Z(ColorPointGIntensity), &Z_Copy }, - { Zuint16, Cx0300, 0x003A, Z(ColorPointBX), &Z_Copy }, - { Zuint16, Cx0300, 0x003B, Z(ColorPointBY), &Z_Copy }, - { Zuint8, Cx0300, 0x003C, Z(ColorPointBIntensity), &Z_Copy }, + { Zuint8, Cx0300, 0x0000, Z(Hue), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0001, Z(Sat), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0002, Z(RemainingTime), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0003, Z(X), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0004, Z(Y), 1, Z_Nop }, + { Zenum8, Cx0300, 0x0005, Z(DriftCompensation), 1, Z_Nop }, + { Zstring, Cx0300, 0x0006, Z(CompensationText), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0007, Z(CT), 1, Z_Nop }, + { Zenum8, Cx0300, 0x0008, Z(ColorMode), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0010, Z(NumberOfPrimaries), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0011, Z(Primary1X), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0012, Z(Primary1Y), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0013, Z(Primary1Intensity), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0015, Z(Primary2X), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0016, Z(Primary2Y), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0017, Z(Primary2Intensity), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0019, Z(Primary3X), 1, Z_Nop }, + { Zuint16, Cx0300, 0x001A, Z(Primary3Y), 1, Z_Nop }, + { Zuint8, Cx0300, 0x001B, Z(Primary3Intensity), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0030, Z(WhitePointX), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0031, Z(WhitePointY), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0032, Z(ColorPointRX), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0033, Z(ColorPointRY), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0034, Z(ColorPointRIntensity), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0036, Z(ColorPointGX), 1, Z_Nop }, + { Zuint16, Cx0300, 0x0037, Z(ColorPointGY), 1, Z_Nop }, + { Zuint8, Cx0300, 0x0038, Z(ColorPointGIntensity), 1, Z_Nop }, + { Zuint16, Cx0300, 0x003A, Z(ColorPointBX), 1, Z_Nop }, + { Zuint16, Cx0300, 0x003B, Z(ColorPointBY), 1, Z_Nop }, + { Zuint8, Cx0300, 0x003C, Z(ColorPointBIntensity), 1, Z_Nop }, // Illuminance Measurement cluster - { Zuint16, Cx0400, 0x0000, Z(Illuminance), &Z_Copy }, // Illuminance (in Lux) - { Zuint16, Cx0400, 0x0001, Z(IlluminanceMinMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0400, 0x0002, Z(IlluminanceMaxMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0400, 0x0003, Z(IlluminanceTolerance), &Z_Copy }, // - { Zenum8, Cx0400, 0x0004, Z(IlluminanceLightSensorType), &Z_Copy }, // - { Zunk, Cx0400, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zuint16, Cx0400, 0x0000, Z(Illuminance), 1, Z_Nop }, // Illuminance (in Lux) + { Zuint16, Cx0400, 0x0001, Z(IlluminanceMinMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0400, 0x0002, Z(IlluminanceMaxMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0400, 0x0003, Z(IlluminanceTolerance), 1, Z_Nop }, // + { Zenum8, Cx0400, 0x0004, Z(IlluminanceLightSensorType), 1, Z_Nop }, // + { Zunk, Cx0400, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Illuminance Level Sensing cluster - { Zenum8, Cx0401, 0x0000, Z(IlluminanceLevelStatus), &Z_Copy }, // Illuminance (in Lux) - { Zenum8, Cx0401, 0x0001, Z(IlluminanceLightSensorType), &Z_Copy }, // LightSensorType - { Zuint16, Cx0401, 0x0010, Z(IlluminanceTargetLevel), &Z_Copy }, // - { Zunk, Cx0401, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zenum8, Cx0401, 0x0000, Z(IlluminanceLevelStatus), 1, Z_Nop }, // Illuminance (in Lux) + { Zenum8, Cx0401, 0x0001, Z(IlluminanceLightSensorType), 1, Z_Nop }, // LightSensorType + { Zuint16, Cx0401, 0x0010, Z(IlluminanceTargetLevel), 1, Z_Nop }, // + { Zunk, Cx0401, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Temperature Measurement cluster - { Zint16, Cx0402, 0x0000, Z(Temperature), &Z_FloatDiv100 }, // Temperature - { Zint16, Cx0402, 0x0001, Z(TemperatureMinMeasuredValue), &Z_FloatDiv100 }, // - { Zint16, Cx0402, 0x0002, Z(TemperatureMaxMeasuredValue), &Z_FloatDiv100 }, // - { Zuint16, Cx0402, 0x0003, Z(TemperatureTolerance), &Z_FloatDiv100 }, // - { Zunk, Cx0402, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zint16, Cx0402, 0x0000, Z(Temperature), -100, Z_Nop }, // divide by 100 + { Zint16, Cx0402, 0x0001, Z(TemperatureMinMeasuredValue), -100, Z_Nop }, // + { Zint16, Cx0402, 0x0002, Z(TemperatureMaxMeasuredValue), -100, Z_Nop }, // + { Zuint16, Cx0402, 0x0003, Z(TemperatureTolerance), -100, Z_Nop }, // + { Zunk, Cx0402, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Pressure Measurement cluster - { Zunk, Cx0403, 0x0000, Z(PressureUnit), &Z_AddPressureUnit }, // Pressure Unit - { Zint16, Cx0403, 0x0000, Z(Pressure), &Z_Copy }, // Pressure - { Zint16, Cx0403, 0x0001, Z(PressureMinMeasuredValue), &Z_Copy }, // - { Zint16, Cx0403, 0x0002, Z(PressureMaxMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0403, 0x0003, Z(PressureTolerance), &Z_Copy }, // - { Zint16, Cx0403, 0x0010, Z(PressureScaledValue), &Z_Copy }, // - { Zint16, Cx0403, 0x0011, Z(PressureMinScaledValue), &Z_Copy }, // - { Zint16, Cx0403, 0x0012, Z(PressureMaxScaledValue), &Z_Copy }, // - { Zuint16, Cx0403, 0x0013, Z(PressureScaledTolerance), &Z_Copy }, // - { Zint8, Cx0403, 0x0014, Z(PressureScale), &Z_Copy }, // - { Zunk, Cx0403, 0xFFFF, nullptr, &Z_Remove }, // Remove all other Pressure values + { Zunk, Cx0403, 0x0000, Z(PressureUnit), 0, Z_AddPressureUnit }, // Pressure Unit + { Zint16, Cx0403, 0x0000, Z(Pressure), 1, Z_Nop }, // Pressure + { Zint16, Cx0403, 0x0001, Z(PressureMinMeasuredValue), 1, Z_Nop }, // + { Zint16, Cx0403, 0x0002, Z(PressureMaxMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0403, 0x0003, Z(PressureTolerance), 1, Z_Nop }, // + { Zint16, Cx0403, 0x0010, Z(PressureScaledValue), 1, Z_Nop }, // + { Zint16, Cx0403, 0x0011, Z(PressureMinScaledValue), 1, Z_Nop }, // + { Zint16, Cx0403, 0x0012, Z(PressureMaxScaledValue), 1, Z_Nop }, // + { Zuint16, Cx0403, 0x0013, Z(PressureScaledTolerance), 1, Z_Nop }, // + { Zint8, Cx0403, 0x0014, Z(PressureScale), 1, Z_Nop }, // + { Zunk, Cx0403, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other Pressure values // Flow Measurement cluster - { Zuint16, Cx0404, 0x0000, Z(FlowRate), &Z_FloatDiv10 }, // Flow (in m3/h) - { Zuint16, Cx0404, 0x0001, Z(FlowMinMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0404, 0x0002, Z(FlowMaxMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0404, 0x0003, Z(FlowTolerance), &Z_Copy }, // - { Zunk, Cx0404, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zuint16, Cx0404, 0x0000, Z(FlowRate), -10, Z_Nop }, // Flow (in m3/h) + { Zuint16, Cx0404, 0x0001, Z(FlowMinMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0404, 0x0002, Z(FlowMaxMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0404, 0x0003, Z(FlowTolerance), 1, Z_Nop }, // + { Zunk, Cx0404, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Relative Humidity Measurement cluster - { Zuint16, Cx0405, 0x0000, Z(Humidity), &Z_FloatDiv100 }, // Humidity - { Zuint16, Cx0405, 0x0001, Z(HumidityMinMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0405, 0x0002, Z(HumidityMaxMeasuredValue), &Z_Copy }, // - { Zuint16, Cx0405, 0x0003, Z(HumidityTolerance), &Z_Copy }, // - { Zunk, Cx0405, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zuint16, Cx0405, 0x0000, Z(Humidity), -100, Z_Nop }, // Humidity + { Zuint16, Cx0405, 0x0001, Z(HumidityMinMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0405, 0x0002, Z(HumidityMaxMeasuredValue), 1, Z_Nop }, // + { Zuint16, Cx0405, 0x0003, Z(HumidityTolerance), 1, Z_Nop }, // + { Zunk, Cx0405, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Occupancy Sensing cluster - { Zmap8, Cx0406, 0x0000, Z(Occupancy), &Z_Copy }, // Occupancy (map8) - { Zenum8, Cx0406, 0x0001, Z(OccupancySensorType), &Z_Copy }, // OccupancySensorType - { Zunk, Cx0406, 0xFFFF, nullptr, &Z_Remove }, // Remove all other values + { Zmap8, Cx0406, 0x0000, Z(Occupancy), 1, Z_Nop }, // Occupancy (map8) + { Zenum8, Cx0406, 0x0001, Z(OccupancySensorType), 1, Z_Nop }, // OccupancySensorType + { Zunk, Cx0406, 0xFFFF, nullptr, 0, Z_Nop }, // Remove all other values // Meter Identification cluster - { Zstring, Cx0B01, 0x0000, Z(CompanyName), &Z_Copy }, - { Zuint16, Cx0B01, 0x0001, Z(MeterTypeID), &Z_Copy }, - { Zuint16, Cx0B01, 0x0004, Z(DataQualityID), &Z_Copy }, - { Zstring, Cx0B01, 0x0005, Z(CustomerName), &Z_Copy }, - { Zoctstr, Cx0B01, 0x0006, Z(Model), &Z_Copy }, - { Zoctstr, Cx0B01, 0x0007, Z(PartNumber), &Z_Copy }, - { Zoctstr, Cx0B01, 0x0008, Z(ProductRevision), &Z_Copy }, - { Zoctstr, Cx0B01, 0x000A, Z(SoftwareRevision), &Z_Copy }, - { Zstring, Cx0B01, 0x000B, Z(UtilityName), &Z_Copy }, - { Zstring, Cx0B01, 0x000C, Z(POD), &Z_Copy }, - { Zint24, Cx0B01, 0x000D, Z(AvailablePower), &Z_Copy }, - { Zint24, Cx0B01, 0x000E, Z(PowerThreshold), &Z_Copy }, + { Zstring, Cx0B01, 0x0000, Z(CompanyName), 1, Z_Nop }, + { Zuint16, Cx0B01, 0x0001, Z(MeterTypeID), 1, Z_Nop }, + { Zuint16, Cx0B01, 0x0004, Z(DataQualityID), 1, Z_Nop }, + { Zstring, Cx0B01, 0x0005, Z(CustomerName), 1, Z_Nop }, + { Zoctstr, Cx0B01, 0x0006, Z(Model), 1, Z_Nop }, + { Zoctstr, Cx0B01, 0x0007, Z(PartNumber), 1, Z_Nop }, + { Zoctstr, Cx0B01, 0x0008, Z(ProductRevision), 1, Z_Nop }, + { Zoctstr, Cx0B01, 0x000A, Z(SoftwareRevision), 1, Z_Nop }, + { Zstring, Cx0B01, 0x000B, Z(UtilityName), 1, Z_Nop }, + { Zstring, Cx0B01, 0x000C, Z(POD), 1, Z_Nop }, + { Zint24, Cx0B01, 0x000D, Z(AvailablePower), 1, Z_Nop }, + { Zint24, Cx0B01, 0x000E, Z(PowerThreshold), 1, Z_Nop }, // Diagnostics cluster - { Zuint16, Cx0B05, 0x0000, Z(NumberOfResets), &Z_Copy }, - { Zuint16, Cx0B05, 0x0001, Z(PersistentMemoryWrites),&Z_Copy }, - { Zuint8, Cx0B05, 0x011C, Z(LastMessageLQI), &Z_Copy }, - { Zuint8, Cx0B05, 0x011D, Z(LastMessageRSSI), &Z_Copy }, + { Zuint16, Cx0B05, 0x0000, Z(NumberOfResets), 1, Z_Nop }, + { Zuint16, Cx0B05, 0x0001, Z(PersistentMemoryWrites),1, Z_Nop }, + { Zuint8, Cx0B05, 0x011C, Z(LastMessageLQI), 1, Z_Nop }, + { Zuint8, Cx0B05, 0x011D, Z(LastMessageRSSI), 1, Z_Nop }, }; @@ -650,7 +662,7 @@ public: } static void generateAttributeName(const JsonObject& json, uint16_t cluster, uint16_t attr, char *key, size_t key_len); - void parseRawAttributes(JsonObject& json, uint8_t offset = 0); + void parseReportAttributes(JsonObject& json, uint8_t offset = 0); void parseReadAttributes(JsonObject& json, uint8_t offset = 0); void parseReadAttributesResponse(JsonObject& json, uint8_t offset = 0); void parseResponse(void); @@ -734,36 +746,50 @@ uint8_t toPercentageCR2032(uint32_t voltage) { // - 1 byte: attribute type // - n bytes: value (typically between 1 and 4 bytes, or bigger for strings) // returns number of bytes of attribute, or <0 if error -int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, uint16_t attr, uint8_t attrtype) { +// status: shall we insert a status OK (0x00) as required by ReadResponse +int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, float val_f, uint16_t attr, uint8_t attrtype, bool status = false) { uint32_t len = Z_getDatatypeLen(attrtype); // pre-compute lenght, overloaded for variable length attributes + uint32_t u32; + int32_t i32; + float f32; - uint32_t u32 = val.as(); - int32_t i32 = val.as(); - float f32 = val.as(); + if (&val) { + u32 = val.as(); + i32 = val.as(); + f32 = val.as(); + } else { + u32 = val_f; + i32 = val_f; + f32 = val_f; + } buf.add16(attr); // prepend with attribute identifier + if (status) { + buf.add8(Z_SUCCESS); // status OK = 0x00 + } buf.add8(attrtype); // prepend with attribute type switch (attrtype) { // unsigned 8 - case Zbool: // bool + case Zbool: // bool case Zuint8: // uint8 case Zenum8: // enum8 case Zdata8: // data8 - case Zmap8: // map8 + case Zmap8: // map8 buf.add8(u32); break; // unsigned 16 - case Zuint16: // uint16 - case Zenum16: // enum16 - case Zdata16: // data16 + case Zuint16: // uint16 + case Zenum16: // enum16 + case Zdata16: // data16 case Zmap16: // map16 buf.add16(u32); break; // unisgned 32 - case Zuint32: // uint32 - case Zdata32: // data32 + case Zuint32: // uint32 + case Zdata32: // data32 case Zmap32: // map32 + case ZUTC: // UTC - epoch 32 bits, seconds since 1-Jan-2000 buf.add32(u32); break; @@ -786,7 +812,7 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, uint16 case Zstring: case Zstring16: { - const char * val_str = val.as(); + const char * val_str = (&val) ? val.as() : ""; // avoid crash if &val is null if (nullptr == val_str) { return -2; } size_t val_len = strlen(val_str); if (val_len > 32) { val_len = 32; } @@ -804,10 +830,10 @@ int32_t encodeSingleAttribute(class SBuffer &buf, const JsonVariant &val, uint16 default: // remove the attribute type we just added - buf.setLen(buf.len() - 3); + buf.setLen(buf.len() - (status ? 4 : 3)); return -1; } - return len + 3; + return len + (status ? 4 : 3); } uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer &buf, @@ -848,6 +874,7 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer } break; case Zuint32: // uint32 + case ZUTC: // UTC { uint32_t uint32_val = buf.get32(i); // i += 4; @@ -974,7 +1001,6 @@ uint32_t parseSingleAttribute(JsonObject& json, char *attrid_str, class SBuffer // TODO case ZToD: // ToD case Zdate: // date - case ZUTC: // UTC case ZclusterId: // clusterId case ZattribId: // attribId case ZbacOID: // bacOID @@ -1028,7 +1054,7 @@ void ZCLFrame::generateAttributeName(const JsonObject& json, uint16_t cluster, u } // First pass, parse all attributes in their native format -void ZCLFrame::parseRawAttributes(JsonObject& json, uint8_t offset) { +void ZCLFrame::parseReportAttributes(JsonObject& json, uint8_t offset) { uint32_t i = offset; uint32_t len = _payload.len(); @@ -1083,7 +1109,7 @@ void ZCLFrame::parseReadAttributesResponse(JsonObject& json, uint8_t offset) { uint32_t i = offset; uint32_t len = _payload.len(); - while (len >= 4 + i) { + while (len >= i + 4) { uint16_t attrid = _payload.get16(i); i += 2; uint8_t status = _payload.get8(i++); @@ -1148,48 +1174,36 @@ void ZCLFrame::parseClusterSpecificCommand(JsonObject& json, uint8_t offset) { // ====================================================================== // Record Manuf -int32_t Z_ManufKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_ManufKeepFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; zigbee_devices.setManufId(shortaddr, value.as()); return 1; } // -int32_t Z_ModelKeep(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_ModelKeepFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; zigbee_devices.setModelId(shortaddr, value.as()); return 1; } -// ====================================================================== -// Remove attribute -int32_t Z_Remove(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - return 1; // remove original key -} - -// Copy value as-is -int32_t Z_Copy(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { - json[new_name] = value; - return 1; // remove original key -} - // Add pressure unit -int32_t Z_AddPressureUnit(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_AddPressureUnitFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = F(D_UNIT_PRESSURE); return 0; // keep original key } // Convert int to float and divide by 100 -int32_t Z_FloatDiv100(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_FloatDiv100Func(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = ((float)value) / 100.0f; return 1; // remove original key } // Convert int to float and divide by 10 -int32_t Z_FloatDiv10(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_FloatDiv10Func(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = ((float)value) / 10.0f; return 1; // remove original key } // Convert int to float and divide by 10 -int32_t Z_FloatDiv2(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_FloatDiv2Func(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = ((float)value) / 2.0f; return 1; // remove original key } @@ -1203,7 +1217,7 @@ int32_t Z_OccupancyCallback(uint16_t shortaddr, uint16_t groupaddr, uint16_t clu } // Aqara Cube -int32_t Z_AqaraCube(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_AqaraCubeFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { json[new_name] = value; // copy the original value int32_t val = value; const __FlashStringHelper *aqara_cube = F("AqaraCube"); @@ -1262,7 +1276,7 @@ int32_t Z_AqaraCube(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& j } // Aqara Vibration Sensor - special proprietary attributes -int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_AqaraVibrationFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { //json[new_name] = value; switch (attr) { case 0x0055: @@ -1313,7 +1327,7 @@ int32_t Z_AqaraVibration(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObje return 1; // remove original key } -int32_t Z_AqaraSensor(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { +int32_t Z_AqaraSensorFunc(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, uint16_t cluster, uint16_t attr) { String hex = value; SBuffer buf2 = SBuffer::SBufferFromHex(hex.c_str(), hex.length()); uint32_t i = 0; @@ -1373,6 +1387,50 @@ int32_t Z_AqaraSensor(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& } // ====================================================================== +// apply the transformation from the converter +int32_t Z_ApplyConverter(const class ZCLFrame *zcl, uint16_t shortaddr, JsonObject& json, const char *name, JsonVariant& value, const String &new_name, + uint16_t cluster, uint16_t attr, int16_t multiplier, uint16_t cb) { + // apply multiplier if needed + if (1 == multiplier) { // copy unchanged + json[new_name] = value; + } else if (0 != multiplier) { + if (multiplier > 0) { + json[new_name] = ((float)value) * multiplier; + } else { + json[new_name] = ((float)value) / multiplier; + } + } + + // apply callback if needed + Z_AttrConverter func = nullptr; + switch (cb) { + case Z_Nop: + return 1; // drop original key + case Z_AddPressureUnit: + func = &Z_AddPressureUnitFunc; + break; + case Z_ManufKeep: + func = &Z_ManufKeepFunc; + break; + case Z_ModelKeep: + func = &Z_ModelKeepFunc; + break; + case Z_AqaraSensor: + func = &Z_AqaraSensorFunc; + break; + case Z_AqaraVibration: + func = &Z_AqaraVibrationFunc; + break; + case Z_AqaraCube: + func = &Z_AqaraCubeFunc; + break; + }; + + if (func) { + return (*func)(zcl, shortaddr, json, name, value, new_name, cluster, attr); + } +} + void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { // iterate on json elements for (auto kv : json) { @@ -1434,12 +1492,15 @@ void ZCLFrame::postProcessAttributes(uint16_t shortaddr, JsonObject& json) { const Z_AttributeConverter *converter = &Z_PostProcess[i]; uint16_t conv_cluster = CxToCluster(pgm_read_byte(&converter->cluster_short)); uint16_t conv_attribute = pgm_read_word(&converter->attribute); + int16_t conv_multiplier = pgm_read_word(&converter->multiplier); + uint16_t conv_cb = pgm_read_word(&converter->cb); // callback id if ((conv_cluster == cluster) && ((conv_attribute == attribute) || (conv_attribute == 0xFFFF)) ) { String new_name_str = (const __FlashStringHelper*) converter->name; if (suffix > 1) { new_name_str += suffix; } // append suffix number - int32_t drop = (*converter->func)(this, shortaddr, json, key, value, new_name_str, conv_cluster, conv_attribute); + // apply the transformation + int32_t drop = Z_ApplyConverter(this, shortaddr, json, key, value, new_name_str, conv_cluster, conv_attribute, conv_multiplier, conv_cb); if (drop) { json.remove(key); } diff --git a/tasmota/xdrv_23_zigbee_8_parsers.ino b/tasmota/xdrv_23_zigbee_8_parsers.ino index 0b1144eb5..996103fe5 100644 --- a/tasmota/xdrv_23_zigbee_8_parsers.ino +++ b/tasmota/xdrv_23_zigbee_8_parsers.ino @@ -657,21 +657,24 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { } else { // Build the ZbReceive json if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_REPORT_ATTRIBUTES == zcl_received.getCmdId())) { - zcl_received.parseRawAttributes(json); // Zigbee report attributes from sensors + zcl_received.parseReportAttributes(json); // Zigbee report attributes from sensors if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES_RESPONSE == zcl_received.getCmdId())) { zcl_received.parseReadAttributesResponse(json); if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages } else if ( (!zcl_received.isClusterSpecificCommand()) && (ZCL_READ_ATTRIBUTES == zcl_received.getCmdId())) { zcl_received.parseReadAttributes(json); - if (clusterid) { defer_attributes = true; } // don't defer system Cluster=0 messages + // never defer read_attributes, so the auto-responder can send response back on a per cluster basis } else if (zcl_received.isClusterSpecificCommand()) { zcl_received.parseClusterSpecificCommand(json); } - String msg(""); - msg.reserve(100); - json.printTo(msg); - AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZCL_RAW_RECEIVED ": {\"0x%04X\":%s}"), srcaddr, msg.c_str()); + + { // fence to force early de-allocation of msg + String msg(""); + msg.reserve(100); + json.printTo(msg); + AddLog_P2(LOG_LEVEL_DEBUG, PSTR(D_LOG_ZIGBEE D_JSON_ZIGBEEZCL_RAW_RECEIVED ": {\"0x%04X\":%s}"), srcaddr, msg.c_str()); + } zcl_received.postProcessAttributes(srcaddr, json); // Add Endpoint @@ -701,6 +704,9 @@ int32_t Z_ReceiveAfIncomingMessage(int32_t res, const class SBuffer &buf) { } else { // Publish immediately zigbee_devices.jsonPublishNow(srcaddr, json); + + // Add auto-responder here + Z_AutoResponder(srcaddr, clusterid, srcendpoint, json[F("ReadNames")]); } } return -1; @@ -817,4 +823,74 @@ int32_t Z_State_Ready(uint8_t value) { return 0; // continue } +// +// Auto-responder for Read request from extenal devices. +// +// Mostly used for routers/end-devices +// json: holds the attributes in JSON format +void Z_AutoResponder(uint16_t srcaddr, uint16_t cluster, uint8_t endpoint, const JsonObject &json) { + DynamicJsonBuffer jsonBuffer; + JsonObject& json_out = jsonBuffer.createObject(); + + // responder + switch (cluster) { + case 0x0000: + if (HasKeyCaseInsensitive(json, PSTR("ModelId"))) { json_out[F("ModelId")] = F("Tasmota Z2T"); } + if (HasKeyCaseInsensitive(json, PSTR("Manufacturer"))) { json_out[F("Manufacturer")] = F("Tasmota"); } + break; +#ifdef USE_LIGHT + case 0x0006: + if (HasKeyCaseInsensitive(json, PSTR("Power"))) { json_out[F("Power")] = Light.power ? 1 : 0; } + break; + case 0x0008: + if (HasKeyCaseInsensitive(json, PSTR("Dimmer"))) { json_out[F("Dimmer")] = LightGetDimmer(0); } + break; + case 0x0300: + { + uint16_t hue; + uint8_t sat; + float XY[2]; + LightGetHSB(&hue, &sat, nullptr); + LightGetXY(&XY[0], &XY[1]); + uint16_t uxy[2]; + for (uint32_t i = 0; i < ARRAY_SIZE(XY); i++) { + uxy[i] = XY[i] * 65536.0f; + uxy[i] = (uxy[i] > 0xFEFF) ? uxy[i] : 0xFEFF; + } + if (HasKeyCaseInsensitive(json, PSTR("Hue"))) { json_out[F("Hue")] = changeUIntScale(hue, 0, 360, 0, 254); } + if (HasKeyCaseInsensitive(json, PSTR("Sat"))) { json_out[F("Sat")] = changeUIntScale(sat, 0, 255, 0, 254); } + if (HasKeyCaseInsensitive(json, PSTR("CT"))) { json_out[F("CT")] = LightGetColorTemp(); } + if (HasKeyCaseInsensitive(json, PSTR("X"))) { json_out[F("X")] = uxy[0]; } + if (HasKeyCaseInsensitive(json, PSTR("Y"))) { json_out[F("Y")] = uxy[1]; } + } + break; +#endif + case 0x000A: // Time + if (HasKeyCaseInsensitive(json, PSTR("Time"))) { json_out[F("Time")] = Rtc.utc_time; } + if (HasKeyCaseInsensitive(json, PSTR("TimeStatus"))) { json_out[F("TimeStatus")] = (Rtc.utc_time > (60 * 60 * 24 * 365 * 10)) ? 0x02 : 0x00; } // if time is beyond 2010 then we are synchronized + if (HasKeyCaseInsensitive(json, PSTR("TimeZone"))) { json_out[F("TimeZone")] = Settings.toffset[0] * 60; } // seconds + break; + } + + if (json_out.size() > 0) { + // we have a non-empty output + + // log first + String msg(""); + msg.reserve(100); + json_out.printTo(msg); + AddLog_P2(LOG_LEVEL_INFO, PSTR("ZIG: Auto-responder: ZbSend {\"Device\":\"0x%04X\"" + ",\"Cluster\":\"0x%04X\"" + ",\"Endpoint\":%d" + ",\"Response\":%s}" + ), + srcaddr, cluster, endpoint, + msg.c_str()); + + // send + const JsonVariant &json_out_v = json_out; + ZbSendReportWrite(json_out_v, srcaddr, 0 /* group */,cluster, endpoint, 0 /* manuf */, ZCL_READ_ATTRIBUTES_RESPONSE); + } +} + #endif // USE_ZIGBEE diff --git a/tasmota/xdrv_23_zigbee_9_impl.ino b/tasmota/xdrv_23_zigbee_9_impl.ino index 38627f91f..0ea39c194 100644 --- a/tasmota/xdrv_23_zigbee_9_impl.ino +++ b/tasmota/xdrv_23_zigbee_9_impl.ino @@ -393,19 +393,25 @@ void zigbeeZCLSendStr(uint16_t shortaddr, uint16_t groupaddr, uint8_t endpoint, } } -// Parse "Report" or "Write" attribute -void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, bool write) { +// Parse "Report", "Write" or "Response" attribute +// Operation is one of: ZCL_REPORT_ATTRIBUTES (0x0A), ZCL_WRITE_ATTRIBUTES (0x02) or ZCL_READ_ATTRIBUTES_RESPONSE (0x01) +void ZbSendReportWrite(const JsonObject &val_pubwrite, uint16_t device, uint16_t groupaddr, uint16_t cluster, uint8_t endpoint, uint16_t manuf, uint32_t operation) { SBuffer buf(200); // buffer to store the binary output of attibutes - const JsonObject &attrs = val_pubwrite.as(); + if (nullptr == XdrvMailbox.command) { + XdrvMailbox.command = (char*) ""; // prevent a crash when calling ReponseCmndChar and there was no previous command + } + // iterate on keys - for (JsonObject::const_iterator it=attrs.begin(); it!=attrs.end(); ++it) { + for (JsonObject::const_iterator it=val_pubwrite.begin(); it!=val_pubwrite.end(); ++it) { const char *key = it->key; const JsonVariant &value = it->value; uint16_t attr_id = 0xFFFF; uint16_t cluster_id = 0xFFFF; uint8_t type_id = Znodata; + int16_t multiplier = 1; // multiplier to adjust the key value + float val_f = 0.0f; // alternative value if multiplier is used // check if the name has the format "XXXX/YYYY" where XXXX is the cluster, YYYY the attribute id // alternative "XXXX/YYYY%ZZ" where ZZ is the type (for unregistered attributes) @@ -431,6 +437,7 @@ void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_ uint16_t local_attr_id = pgm_read_word(&converter->attribute); uint16_t local_cluster_id = CxToCluster(pgm_read_byte(&converter->cluster_short)); uint8_t local_type_id = pgm_read_byte(&converter->type); + int16_t local_multiplier = pgm_read_word(&converter->multiplier); // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("Try cluster = 0x%04X, attr = 0x%04X, type_id = 0x%02X"), local_cluster_id, local_attr_id, local_type_id); if (delimiter) { @@ -445,12 +452,14 @@ void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_ cluster_id = local_cluster_id; attr_id = local_attr_id; type_id = local_type_id; + multiplier = local_multiplier; break; } } } } + // Buffer ready, do some sanity checks // AddLog_P2(LOG_LEVEL_DEBUG, PSTR("cluster_id = 0x%04X, attr_id = 0x%04X, type_id = 0x%02X"), cluster_id, attr_id, type_id); if ((0xFFFF == attr_id) || (0xFFFF == cluster_id)) { Response_P(PSTR("{\"%s\":\"%s'%s'\"}"), XdrvMailbox.command, PSTR("Unknown attribute "), key); @@ -467,8 +476,19 @@ void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_ ResponseCmndChar_P(PSTR("No more than one cluster id per command")); return; } + // apply multiplier if needed + bool use_val = true; + if ((0 != multiplier) && (1 != multiplier)) { + val_f = value; + if (multiplier > 0) { // inverse of decoding + val_f = val_f / multiplier; + } else { + val_f = val_f * multiplier; + } + use_val = false; + } // push the value in the buffer - int32_t res = encodeSingleAttribute(buf, value, attr_id, type_id); + int32_t res = encodeSingleAttribute(buf, use_val ? value : *(const JsonVariant*)nullptr, val_f, attr_id, type_id, operation == ZCL_READ_ATTRIBUTES_RESPONSE); // force status if Reponse if (res < 0) { Response_P(PSTR("{\"%s\":\"%s'%s' 0x%02X\"}"), XdrvMailbox.command, PSTR("Unsupported attribute type "), key, type_id); return; @@ -482,7 +502,7 @@ void ZbSendReportWrite(const JsonVariant &val_pubwrite, uint16_t device, uint16_ } // all good, send the packet - ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, write ? ZCL_WRITE_ATTRIBUTES : ZCL_REPORT_ATTRIBUTES, false /* not cluster specific */, manuf, buf.getBuffer(), buf.len(), false /* noresponse */, zigbee_devices.getNextSeqNumber(device)); + ZigbeeZCLSend_Raw(device, groupaddr, cluster, endpoint, operation, false /* not cluster specific */, manuf, buf.getBuffer(), buf.len(), false /* noresponse */, zigbee_devices.getNextSeqNumber(device)); ResponseCmndDone(); } @@ -511,14 +531,22 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, const JsonVariant& value = it->value; uint32_t x = 0, y = 0, z = 0; uint16_t cmd_var; + uint16_t local_cluster_id; - const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &cluster, &cmd_var); + const __FlashStringHelper* tasmota_cmd = zigbeeFindCommand(key.c_str(), &local_cluster_id, &cmd_var); if (tasmota_cmd) { cmd_str = tasmota_cmd; } else { Response_P(PSTR("Unrecognized zigbee command: %s"), key.c_str()); return; } + // check cluster + if (0xFFFF == cluster) { + cluster = local_cluster_id; + } else if (cluster != local_cluster_id) { + ResponseCmndChar_P(PSTR("No more than one cluster id per command")); + return; + } // parse the JSON value, depending on its type fill in x,y,z if (value.is()) { @@ -570,7 +598,15 @@ void ZbSendSend(const JsonVariant &val_cmd, uint16_t device, uint16_t groupaddr, // where AA is the cluster number, BBBB the command number, CCCC... the payload // First delimiter is '_' for a global command, or '!' for a cluster specific command const char * data = cmd_str.c_str(); - cluster = parseHex(&data, 4); + uint16_t local_cluster_id = parseHex(&data, 4); + + // check cluster + if (0xFFFF == cluster) { + cluster = local_cluster_id; + } else if (cluster != local_cluster_id) { + ResponseCmndChar_P(PSTR("No more than one cluster id per command")); + return; + } // delimiter if (('_' == *data) || ('!' == *data)) { @@ -650,6 +686,14 @@ void ZbSendRead(const JsonVariant &val_attr, uint16_t device, uint16_t groupaddr attrs[actual_attr_len++] = local_attr_id & 0xFF; attrs[actual_attr_len++] = local_attr_id >> 8; found = true; + // check cluster + if (0xFFFF == cluster) { + cluster = local_cluster_id; + } else if (cluster != local_cluster_id) { + ResponseCmndChar_P(PSTR("No more than one cluster id per command")); + if (attrs) { delete[] attrs; } + return; + } break; // found, exit loop } } @@ -728,6 +772,8 @@ void CmndZbSend(void) { return; } } + // from here, either device has a device shortaddr, or if BAD_SHORTADDR then use group address + // Note: groupaddr == 0 is valid // read other parameters const JsonVariant &val_cluster = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_CLUSTER)); @@ -739,44 +785,64 @@ void CmndZbSend(void) { // infer endpoint if (BAD_SHORTADDR == device) { - endpoint = 0xFF; // endpoint not used for group addresses - } else if (0 == endpoint) { + endpoint = 0xFF; // endpoint not used for group addresses, so use a dummy broadcast endpoint + } else if (0 == endpoint) { // if it was not already specified, try to guess it endpoint = zigbee_devices.findFirstEndpoint(device); AddLog_P2(LOG_LEVEL_DEBUG, PSTR("ZIG: guessing endpoint %d"), endpoint); } + if (0 == endpoint) { // after this, if it is still zero, then it's an error + ResponseCmndChar_P(PSTR("Missing endpoint")); + return; + } + // from here endpoint is valid and non-zero + // cluster may be already specified or 0xFFFF const JsonVariant &val_cmd = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_SEND)); const JsonVariant &val_read = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_READ)); const JsonVariant &val_write = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_WRITE)); const JsonVariant &val_publish = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_REPORT)); - uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish); + const JsonVariant &val_response = GetCaseInsensitive(json, PSTR(D_CMND_ZIGBEE_RESPONSE)); + uint32_t multi_cmd = (nullptr != &val_cmd) + (nullptr != &val_read) + (nullptr != &val_write) + (nullptr != &val_publish)+ (nullptr != &val_response); if (multi_cmd > 1) { - ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write' or 'Report'")); + ResponseCmndChar_P(PSTR("Can only have one of: 'Send', 'Read', 'Write', 'Report' or 'Reponse'")); return; } + // from here we have one and only one command if (nullptr != &val_cmd) { // "Send":{...commands...} + // we accept either a string or a JSON object ZbSendSend(val_cmd, device, groupaddr, cluster, endpoint, manuf); } else if (nullptr != &val_read) { // "Read":{...attributes...}, "Read":attribute or "Read":[...attributes...] + // we accept eitehr a number, a string, an array of numbers/strings, or a JSON object ZbSendRead(val_read, device, groupaddr, cluster, endpoint, manuf); } else if (nullptr != &val_write) { - if ((0 == endpoint) || (!val_write.is())) { + // only KSON object + if (!val_write.is()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } // "Write":{...attributes...} - ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, true /* write */); + ZbSendReportWrite(val_write, device, groupaddr, cluster, endpoint, manuf, ZCL_WRITE_ATTRIBUTES); } else if (nullptr != &val_publish) { - if ((0 == endpoint) || (!val_publish.is())) { + // "Report":{...attributes...} + // only KSON object + if (!val_publish.is()) { ResponseCmndChar_P(PSTR("Missing parameters")); return; } + ZbSendReportWrite(val_publish, device, groupaddr, cluster, endpoint, manuf, ZCL_REPORT_ATTRIBUTES); + } else if (nullptr != &val_response) { // "Report":{...attributes...} - ZbSendReportWrite(val_publish, device, groupaddr, cluster, endpoint, manuf, false /* report */); + // only KSON object + if (!val_response.is()) { + ResponseCmndChar_P(PSTR("Missing parameters")); + return; + } + ZbSendReportWrite(val_response, device, groupaddr, cluster, endpoint, manuf, ZCL_READ_ATTRIBUTES_RESPONSE); } else { - Response_P(PSTR("Missing zigbee 'Send', 'Write' or 'Report'")); + Response_P(PSTR("Missing zigbee 'Send', 'Write', 'Report' or 'Response'")); return; } } From 5558da527abf2a3b0cb5c06d0593981c934f177c Mon Sep 17 00:00:00 2001 From: Alexey Kardashevskiy Date: Wed, 3 Jun 2020 16:09:28 +1000 Subject: [PATCH 44/48] Add MAX6675 sensor This is basically a cut down version of MAX31855 without reference temperature reading and lower resolution (only positive, 12bit only). This implements 16bit protocol (31855 uses 32bit). SetOption94 enables the new behavior. Signed-off-by: Alexey Kardashevskiy --- Changes: v2: * treat occasional 0xfff as an error * do not add new sensor pins, use SetOption94 instead --- tasmota/settings.h | 2 +- tasmota/xsns_39_max31855.ino | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tasmota/settings.h b/tasmota/settings.h index 0ad7a3533..1b0cc0951 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -113,7 +113,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu uint32_t fade_at_startup : 1; // bit 9 (v8.2.0.3) - SetOption91 - Enable light fading at start/power on uint32_t pwm_ct_mode : 1; // bit 10 (v8.2.0.4) - SetOption92 - Set PWM Mode from regular PWM to ColorTemp control (Xiaomi Philips ...) uint32_t compress_rules_cpu : 1; // bit 11 (v8.2.0.6) - SetOption93 - Keep uncompressed rules in memory to avoid CPU load of uncompressing at each tick - uint32_t spare12 : 1; + uint32_t max6675 : 1; // bit 12 (v8.3.1.2) - SetOption94 - Implement simpler MAX6675 protocol instead of MAX31855 uint32_t spare13 : 1; uint32_t spare14 : 1; uint32_t spare15 : 1; diff --git a/tasmota/xsns_39_max31855.ino b/tasmota/xsns_39_max31855.ino index c8cdfc7aa..e80f879c5 100644 --- a/tasmota/xsns_39_max31855.ino +++ b/tasmota/xsns_39_max31855.ino @@ -50,6 +50,20 @@ void MAX31855_Init(void){ * Acquires the raw data via SPI, checks for MAX31855 errors and fills result structure */ void MAX31855_GetResult(void){ + // Controlled via SetOption94 + if (Settings.flag4.max6675) { + int32_t RawData = MAX31855_ShiftIn(16); + int32_t temp = (RawData >> 3) & ((1 << 12) - 1); + + /* Occasionally the sensor returns 0xfff, consider it an error */ + if (temp == ((1 << 12) - 1)) + return; + + MAX31855_Result.ErrorCode = 0; + MAX31855_Result.ReferenceTemperature = NAN; + MAX31855_Result.ProbeTemperature = ConvertTemp(0.25 * temp); + return; + } int32_t RawData = MAX31855_ShiftIn(32); uint8_t probeerror = RawData & 0x7; From 41f77f688cf0ed9133f5d29248d99bc125c65291 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Thu, 4 Jun 2020 11:36:58 +0200 Subject: [PATCH 45/48] Add command ``SetOption94 0/1`` Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616) --- RELEASENOTES.md | 2 + tasmota/CHANGELOG.md | 1 + tasmota/my_user_config.h | 10 +- tasmota/xsns_39_max31855.ino | 229 ++++++++++++++++++----------------- 4 files changed, 127 insertions(+), 115 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2453fd5d4..5e4429dde 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -61,6 +61,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Fix escape of non-JSON received serial data (#8329) - Add command ``Rule0`` to change global rule parameters - Add command ``Time 4`` to display timestamp using milliseconds (#8537) +- Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616) - Add commands ``LedPwmOn 0..255``, ``LedPwmOff 0..255`` and ``LedPwmMode1 0/1`` to control led brightness by George (#8491) - Add support for unique MQTTClient (and inherited fallback topic) by full Mac address using ``mqttclient DVES_%12X`` (#8300) - Add more functionality to ``Switchmode`` 11 and 12 (#8450) @@ -69,6 +70,7 @@ The following binary downloads have been compiled with ESP8266/Arduino library c - Add support for VEML7700 Ambient light intensity Sensor by device111 (#8432) - Add Three Phase Export Active Energy to SDM630 driver - Add Zigbee options to ``ZbSend`` to write and report attributes +- Add Zigbee auto-responder for common attributes - Add ``CpuFrequency`` to ``status 2`` - Add ``FlashFrequency`` to ``status 4`` - Add support for up to two BH1750 sensors controlled by commands ``BH1750Resolution`` and ``BH1750MTime`` (#8139) diff --git a/tasmota/CHANGELOG.md b/tasmota/CHANGELOG.md index e12680d36..54e1a5f63 100644 --- a/tasmota/CHANGELOG.md +++ b/tasmota/CHANGELOG.md @@ -7,6 +7,7 @@ - Change Adafruit_SGP30 library from v1.0.3 to v1.2.0 (#8519) - Fix escape of non-JSON received serial data (#8329) - Add command ``Time 4`` to display timestamp using milliseconds (#8537) +- Add command ``SetOption94 0/1`` to select MAX31855 or MAX6675 thermocouple support (#8616) - Add commands ``LedPwmOn 0..255``, ``LedPwmOff 0..255`` and ``LedPwmMode1 0/1`` to control led brightness by George (#8491) - Add Three Phase Export Active Energy to SDM630 driver - Add wildcard pattern ``?`` for JSON matching in rules diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 0d968b1f8..050c2e390 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -612,12 +612,12 @@ // -- Low level interface devices ----------------- #define USE_DHT // Add support for DHT11, AM2301 (DHT21, DHT22, AM2302, AM2321) and SI7021 Temperature and Humidity sensor (1k6 code) -//#define USE_MAX31855 // Add support for MAX31855 K-Type thermocouple sensor using softSPI +//#define USE_MAX31855 // Add support for MAX31855/MAX6675 K-Type thermocouple sensor using softSPI //#define USE_MAX31865 // Add support for MAX31865 RTD sensors using softSPI - #define MAX31865_PTD_WIRES 2 // PTDs come in several flavors. Pick yours - #define MAX31865_PTD_RES 100 // Nominal PTD resistance at 0°C (100Ω for a PT100, 1000Ω for a PT1000, YMMV!) - #define MAX31865_REF_RES 430 // Reference resistor (Usually 430Ω for a PT100, 4300Ω for a PT1000) - #define MAX31865_PTD_BIAS 0 // To calibrate your not-so-good PTD + #define MAX31865_PTD_WIRES 2 // PTDs come in several flavors. Pick yours + #define MAX31865_PTD_RES 100 // Nominal PTD resistance at 0°C (100Ω for a PT100, 1000Ω for a PT1000, YMMV!) + #define MAX31865_REF_RES 430 // Reference resistor (Usually 430Ω for a PT100, 4300Ω for a PT1000) + #define MAX31865_PTD_BIAS 0 // To calibrate your not-so-good PTD // -- IR Remote features - all protocols from IRremoteESP8266 -------------------------- // IR Full Protocols mode is activated through platform.io only. diff --git a/tasmota/xsns_39_max31855.ino b/tasmota/xsns_39_max31855.ino index e80f879c5..fcdb69094 100644 --- a/tasmota/xsns_39_max31855.ino +++ b/tasmota/xsns_39_max31855.ino @@ -18,20 +18,27 @@ */ #ifdef USE_MAX31855 +/*********************************************************************************************\ + * MAX31855 and MAX6675 - Thermocouple + * + * SetOption94 0 - MAX31855 + * SetOption94 1 - MAX6675 +\*********************************************************************************************/ #define XSNS_39 39 -bool initialized = false; +const char kMax31855Types[] PROGMEM = "MAX31855|MAX6675"; -struct MAX31855_ResultStruct{ - uint8_t ErrorCode; // Error Codes: 0 = No Error / 1 = TC open circuit / 2 = TC short to GND / 4 = TC short to VCC - float ProbeTemperature; // Measured temperature of the 'hot' TC junction (probe temp) - float ReferenceTemperature; // Measured temperature of the 'cold' TC junction (reference temp) +bool max31855_initialized = false; + +struct MAX31855_ResultStruct { + uint8_t ErrorCode; // Error Codes: 0 = No Error / 1 = TC open circuit / 2 = TC short to GND / 4 = TC short to VCC + float ProbeTemperature; // Measured temperature of the 'hot' TC junction (probe temp) + float ReferenceTemperature; // Measured temperature of the 'cold' TC junction (reference temp) } MAX31855_Result; -void MAX31855_Init(void){ - if(initialized) - return; +void MAX31855_Init(void) { + if (PinUsed(GPIO_MAX31855CS) && PinUsed(GPIO_MAX31855CLK) && PinUsed(GPIO_MAX31855DO)) { // Set GPIO modes for SW-SPI pinMode(Pin(GPIO_MAX31855CS), OUTPUT); @@ -42,120 +49,122 @@ void MAX31855_Init(void){ digitalWrite(Pin(GPIO_MAX31855CS), HIGH); digitalWrite(Pin(GPIO_MAX31855CLK), LOW); - initialized = true; -} - -/* -* MAX31855_GetResult(void) -* Acquires the raw data via SPI, checks for MAX31855 errors and fills result structure -*/ -void MAX31855_GetResult(void){ - // Controlled via SetOption94 - if (Settings.flag4.max6675) { - int32_t RawData = MAX31855_ShiftIn(16); - int32_t temp = (RawData >> 3) & ((1 << 12) - 1); - - /* Occasionally the sensor returns 0xfff, consider it an error */ - if (temp == ((1 << 12) - 1)) - return; - - MAX31855_Result.ErrorCode = 0; - MAX31855_Result.ReferenceTemperature = NAN; - MAX31855_Result.ProbeTemperature = ConvertTemp(0.25 * temp); - return; - } - int32_t RawData = MAX31855_ShiftIn(32); - uint8_t probeerror = RawData & 0x7; - - MAX31855_Result.ErrorCode = probeerror; - MAX31855_Result.ReferenceTemperature = MAX31855_GetReferenceTemperature(RawData); - if(probeerror) - MAX31855_Result.ProbeTemperature = NAN; // Return NaN if MAX31855 reports an error - else - MAX31855_Result.ProbeTemperature = MAX31855_GetProbeTemperature(RawData); -} - - -/* -* MAX31855_GetProbeTemperature(int32_t RawData) -* Decodes and returns the temperature of TCs 'hot' junction from RawData -*/ -float MAX31855_GetProbeTemperature(int32_t RawData){ - if(RawData & 0x80000000) - RawData = (RawData >> 18) | 0xFFFFC000; // Negative value - Drop lower 18 bits and extend to negative number - else - RawData >>= 18; // Positiv value - Drop lower 18 bits - - float result = (RawData * 0.25); // MAX31855 LSB resolution is 0.25°C for probe temperature - - return ConvertTemp(result); // Check if we have to convert to Fahrenheit -} - -/* -* MAX31855_GetReferenceTemperature(int32_t RawData) -* Decodes and returns the temperature of TCs 'cold' junction from RawData -*/ -float MAX31855_GetReferenceTemperature(int32_t RawData){ - if(RawData & 0x8000) - RawData = (RawData >> 4) | 0xFFFFF000; // Negative value - Drop lower 4 bits and extend to negative number - else - RawData = (RawData >> 4) & 0x00000FFF; // Positiv value - Drop lower 4 bits and mask out remaining bits (probe temp, error bit, etc.) - - float result = (RawData * 0.0625); // MAX31855 LSB resolution is 0.0625°C for reference temperature - - return ConvertTemp(result); // Check if we have to convert to Fahrenheit + max31855_initialized = true; + } } /* * MAX31855_ShiftIn(uint8_t Length) * Communicates with MAX31855 via SW-SPI and returns the raw data read from the chip */ -int32_t MAX31855_ShiftIn(uint8_t Length){ - int32_t dataIn = 0; +int32_t MAX31855_ShiftIn(uint8_t Length) { + int32_t dataIn = 0; - digitalWrite(Pin(GPIO_MAX31855CS), LOW); // CS = LOW -> Start SPI communication - delayMicroseconds(1); // CS fall to output enable = max. 100ns + digitalWrite(Pin(GPIO_MAX31855CS), LOW); // CS = LOW -> Start SPI communication + delayMicroseconds(1); // CS fall to output enable = max. 100ns - for (uint32_t i = 0; i < Length; i++) - { - digitalWrite(Pin(GPIO_MAX31855CLK), LOW); - delayMicroseconds(1); // CLK pulse width low = min. 100ns / CLK fall to output valid = max. 40ns - dataIn <<= 1; - if(digitalRead(Pin(GPIO_MAX31855DO))) - dataIn |= 1; - digitalWrite(Pin(GPIO_MAX31855CLK), HIGH); - delayMicroseconds(1); // CLK pulse width high = min. 100ns - } - - digitalWrite(Pin(GPIO_MAX31855CS), HIGH); // CS = HIGH -> End SPI communication + for (uint32_t i = 0; i < Length; i++) { digitalWrite(Pin(GPIO_MAX31855CLK), LOW); - return dataIn; + delayMicroseconds(1); // CLK pulse width low = min. 100ns / CLK fall to output valid = max. 40ns + dataIn <<= 1; + if (digitalRead(Pin(GPIO_MAX31855DO))) { + dataIn |= 1; + } + digitalWrite(Pin(GPIO_MAX31855CLK), HIGH); + delayMicroseconds(1); // CLK pulse width high = min. 100ns + } + + digitalWrite(Pin(GPIO_MAX31855CS), HIGH); // CS = HIGH -> End SPI communication + digitalWrite(Pin(GPIO_MAX31855CLK), LOW); + return dataIn; } -void MAX31855_Show(bool Json){ - char probetemp[33]; - char referencetemp[33]; - dtostrfd(MAX31855_Result.ProbeTemperature, Settings.flag2.temperature_resolution, probetemp); - dtostrfd(MAX31855_Result.ReferenceTemperature, Settings.flag2.temperature_resolution, referencetemp); +/* +* MAX31855_GetProbeTemperature(int32_t RawData) +* Decodes and returns the temperature of TCs 'hot' junction from RawData +*/ +float MAX31855_GetProbeTemperature(int32_t RawData) { + if (RawData & 0x80000000) { + RawData = (RawData >> 18) | 0xFFFFC000; // Negative value - Drop lower 18 bits and extend to negative number + } else { + RawData >>= 18; // Positiv value - Drop lower 18 bits + } + float result = (RawData * 0.25); // MAX31855 LSB resolution is 0.25°C for probe temperature - if(Json){ - ResponseAppend_P(PSTR(",\"MAX31855\":{\"" D_JSON_PROBETEMPERATURE "\":%s,\"" D_JSON_REFERENCETEMPERATURE "\":%s,\"" D_JSON_ERROR "\":%d}"), \ - probetemp, referencetemp, MAX31855_Result.ErrorCode); + return ConvertTemp(result); // Check if we have to convert to Fahrenheit +} + +/* +* MAX31855_GetReferenceTemperature(int32_t RawData) +* Decodes and returns the temperature of TCs 'cold' junction from RawData +*/ +float MAX31855_GetReferenceTemperature(int32_t RawData) { + if (RawData & 0x8000) { + RawData = (RawData >> 4) | 0xFFFFF000; // Negative value - Drop lower 4 bits and extend to negative number + } else { + RawData = (RawData >> 4) & 0x00000FFF; // Positiv value - Drop lower 4 bits and mask out remaining bits (probe temp, error bit, etc.) + } + float result = (RawData * 0.0625); // MAX31855 LSB resolution is 0.0625°C for reference temperature + + return ConvertTemp(result); // Check if we have to convert to Fahrenheit +} + +/* +* MAX31855_GetResult(void) +* Acquires the raw data via SPI, checks for MAX31855 errors and fills result structure +*/ +void MAX31855_GetResult(void) { + if (Settings.flag4.max6675) { // SetOption94 - Implement simpler MAX6675 protocol instead of MAX31855 + int32_t RawData = MAX31855_ShiftIn(16); + int32_t temp = (RawData >> 3) & ((1 << 12) - 1); + + /* Occasionally the sensor returns 0xfff, consider it an error */ + if (temp == ((1 << 12) - 1)) { return; } + + MAX31855_Result.ErrorCode = 0; + MAX31855_Result.ReferenceTemperature = NAN; + MAX31855_Result.ProbeTemperature = ConvertTemp(0.25 * temp); + } else { + int32_t RawData = MAX31855_ShiftIn(32); + uint8_t probeerror = RawData & 0x7; + + MAX31855_Result.ErrorCode = probeerror; + MAX31855_Result.ReferenceTemperature = MAX31855_GetReferenceTemperature(RawData); + if (probeerror) { + MAX31855_Result.ProbeTemperature = NAN; // Return NaN if MAX31855 reports an error + } else { + MAX31855_Result.ProbeTemperature = MAX31855_GetProbeTemperature(RawData); + } + } +} + +void MAX31855_Show(bool Json) { + char probetemp[33]; + char referencetemp[33]; + dtostrfd(MAX31855_Result.ProbeTemperature, Settings.flag2.temperature_resolution, probetemp); + dtostrfd(MAX31855_Result.ReferenceTemperature, Settings.flag2.temperature_resolution, referencetemp); + + char sensor_name[10]; + GetTextIndexed(sensor_name, sizeof(sensor_name), Settings.flag4.max6675, kMax31855Types); + + if (Json) { + ResponseAppend_P(PSTR(",\"%s\":{\"" D_JSON_PROBETEMPERATURE "\":%s,\"" D_JSON_REFERENCETEMPERATURE "\":%s,\"" D_JSON_ERROR "\":%d}"), \ + sensor_name, probetemp, referencetemp, MAX31855_Result.ErrorCode); #ifdef USE_DOMOTICZ - if (0 == tele_period) { - DomoticzSensor(DZ_TEMP, probetemp); - } + if (0 == tele_period) { + DomoticzSensor(DZ_TEMP, probetemp); + } #endif // USE_DOMOTICZ #ifdef USE_KNX - if (0 == tele_period) { - KnxSensor(KNX_TEMPERATURE, MAX31855_Result.ProbeTemperature); - } -#endif // USE_KNX - } else { -#ifdef USE_WEBSERVER - WSContentSend_PD(HTTP_SNS_TEMP, "MAX31855", probetemp, TempUnit()); -#endif // USE_WEBSERVER + if (0 == tele_period) { + KnxSensor(KNX_TEMPERATURE, MAX31855_Result.ProbeTemperature); } +#endif // USE_KNX +#ifdef USE_WEBSERVER + } else { + WSContentSend_PD(HTTP_SNS_TEMP, sensor_name, probetemp, TempUnit()); +#endif // USE_WEBSERVER + } } /*********************************************************************************************\ @@ -165,12 +174,12 @@ void MAX31855_Show(bool Json){ bool Xsns39(uint8_t function) { bool result = false; - if(PinUsed(GPIO_MAX31855CS) && PinUsed(GPIO_MAX31855CLK) && PinUsed(GPIO_MAX31855DO)){ + if (FUNC_INIT == function) { + MAX31855_Init(); + } + else if (max31855_initialized) { switch (function) { - case FUNC_INIT: - MAX31855_Init(); - break; case FUNC_EVERY_SECOND: MAX31855_GetResult(); break; From dac14073c99056fdb7f506b19dd0cdac8321877a Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Thu, 4 Jun 2020 12:28:01 +0200 Subject: [PATCH 46/48] Fix windmeter interrupt service routine Fix windmeter interrupt service routine (#8614) --- tasmota/xsns_68_windmeter.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/xsns_68_windmeter.ino b/tasmota/xsns_68_windmeter.ino index c0e5de2aa..3faa7ecb7 100644 --- a/tasmota/xsns_68_windmeter.ino +++ b/tasmota/xsns_68_windmeter.ino @@ -83,7 +83,7 @@ void WindMeterUpdateSpeed(void) if (time_diff > Settings.windmeter_pulse_debounce * 1000) { WindMeter.counter_time = time; WindMeter.counter++; - AddLog_P2(LOG_LEVEL_DEBUG, PSTR("WMET: Counter %d"), WindMeter.counter); +// AddLog_P2(LOG_LEVEL_DEBUG, PSTR("WMET: Counter %d"), WindMeter.counter); } } From c20bb9ab58f85819a0640894e91865c6756e3e7d Mon Sep 17 00:00:00 2001 From: Janusz Kostorz <48957313+jkostorz@users.noreply.github.com> Date: Thu, 4 Jun 2020 22:27:30 +0200 Subject: [PATCH 47/48] increasing in color temperature change speed --- tasmota/support_rotary.ino | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasmota/support_rotary.ino b/tasmota/support_rotary.ino index 25c064a18..6d79744f4 100644 --- a/tasmota/support_rotary.ino +++ b/tasmota/support_rotary.ino @@ -117,7 +117,7 @@ void RotaryHandler(void) Rotary.changed = 1; // button1 is pressed: set color temperature int16_t t = LightGetColorTemp(); - t = t + (Rotary.position - Rotary.last_position); + t = t + ((Rotary.position - Rotary.last_position) * 4); if (t < 153) { t = 153; } From 9d86c15685db3f4103e2a47bc3eeb747c830d700 Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Fri, 5 Jun 2020 11:53:53 +0200 Subject: [PATCH 48/48] Some webserver code saving --- tasmota/xdrv_01_webserver.ino | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tasmota/xdrv_01_webserver.ino b/tasmota/xdrv_01_webserver.ino index e4f2bd4a4..6bf15230e 100644 --- a/tasmota/xdrv_01_webserver.ino +++ b/tasmota/xdrv_01_webserver.ino @@ -34,8 +34,8 @@ const uint16_t CHUNKED_BUFFER_SIZE = (MESSZ / 2) - 100; // Chunk buffer size (should be smaller than half mqtt_data size = MESSZ) const uint16_t HTTP_REFRESH_TIME = 2345; // milliseconds -#define HTTP_RESTART_RECONNECT_TIME 9000 // milliseconds -#define HTTP_OTA_RESTART_RECONNECT_TIME 20000 // milliseconds +const uint16_t HTTP_RESTART_RECONNECT_TIME = 9000; // milliseconds - Allow time for restart and wifi reconnect +const uint16_t HTTP_OTA_RESTART_RECONNECT_TIME = 28000; // milliseconds - Allow time for uploading binary, unzip/write to final destination and wifi reconnect #include #include @@ -170,12 +170,8 @@ const char HTTP_SCRIPT_WIFI[] PROGMEM = "eb('p1').focus();" "}"; -const char HTTP_SCRIPT_RELOAD[] PROGMEM = - "setTimeout(function(){location.href='.';}," STR(HTTP_RESTART_RECONNECT_TIME) ");"; - -// Local OTA upgrade requires more time to complete cp: before web ui should be reloaded -const char HTTP_SCRIPT_RELOAD_OTA[] PROGMEM = - "setTimeout(function(){location.href='.';}," STR(HTTP_OTA_RESTART_RECONNECT_TIME) ");"; +const char HTTP_SCRIPT_RELOAD_TIME[] PROGMEM = + "setTimeout(function(){location.href='.';},%d);"; const char HTTP_SCRIPT_CONSOL[] PROGMEM = "var sn=0,id=0;" // Scroll position, Get most of weblog initially @@ -980,7 +976,7 @@ void WebRestart(uint32_t type) bool reset_only = (HTTP_MANAGER_RESET_ONLY == Web.state); WSContentStart_P((type) ? S_SAVE_CONFIGURATION : S_RESTART, !reset_only); - WSContentSend_P(HTTP_SCRIPT_RELOAD); + WSContentSend_P(HTTP_SCRIPT_RELOAD_TIME, HTTP_RESTART_RECONNECT_TIME); WSContentSendStyle(); if (type) { WSContentSend_P(PSTR("

" D_CONFIGURATION_SAVED "
")); @@ -2346,7 +2342,7 @@ void HandleUpgradeFirmwareStart(void) } WSContentStart_P(S_INFORMATION); - WSContentSend_P(HTTP_SCRIPT_RELOAD_OTA); + WSContentSend_P(HTTP_SCRIPT_RELOAD_TIME, HTTP_OTA_RESTART_RECONNECT_TIME); WSContentSendStyle(); WSContentSend_P(PSTR("
" D_UPGRADE_STARTED " ...
")); WSContentSend_P(HTTP_MSG_RSTRT); @@ -2371,7 +2367,7 @@ void HandleUploadDone(void) WSContentStart_P(S_INFORMATION); if (!Web.upload_error) { - WSContentSend_P(HTTP_SCRIPT_RELOAD_OTA); // Refesh main web ui after OTA upgrade + WSContentSend_P(HTTP_SCRIPT_RELOAD_TIME, HTTP_OTA_RESTART_RECONNECT_TIME); // Refesh main web ui after OTA upgrade } WSContentSendStyle(); WSContentSend_P(PSTR("
" D_UPLOAD "