From b19d1c580f6f7d055d7578fdde32d81514ab3e1a Mon Sep 17 00:00:00 2001 From: Theo Arends <11044339+arendst@users.noreply.github.com> Date: Wed, 17 Nov 2021 17:25:16 +0100 Subject: [PATCH] Add Sonoff SPM POC Add Sonoff SPM POC (#13447) --- CHANGELOG.md | 1 + RELEASENOTES.md | 1 + tasmota/xdrv_86_esp32_sonoff_spm.ino | 1129 ++++++++++++++++++++++++++ 3 files changed, 1131 insertions(+) create mode 100644 tasmota/xdrv_86_esp32_sonoff_spm.ino diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c68385d0..0d3711803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Autoconfiguration for ESP32 and variants - ESP32 fix leftover GPIO configuration after restart +- ESP32 Proof of Concept Sonoff SPM with limited functionality (switching and energy monitoring) (#13447) ### Changed - ESP8266 Gratuitous ARP enabled and set to 60 seconds (#13623) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 858ab7ac7..c73e1da40 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -104,6 +104,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl http://ota.tasmo ### Added - 1 second heartbeat GPIO - ESP32 Berry add module ``python_compat`` to be closer to Python syntax [#13428](https://github.com/arendst/Tasmota/issues/13428) +- ESP32 Proof of Concept Sonoff SPM with limited functionality (switching and energy monitoring) [#13447](https://github.com/arendst/Tasmota/issues/13447) - Command ``TcpConfig`` for TCPBridge protocol configuration [#13565](https://github.com/arendst/Tasmota/issues/13565) - Support for HDC2010 temperature/humidity sensor by Luc Boudreau [#13633](https://github.com/arendst/Tasmota/issues/13633) diff --git a/tasmota/xdrv_86_esp32_sonoff_spm.ino b/tasmota/xdrv_86_esp32_sonoff_spm.ino new file mode 100644 index 000000000..f40c824fa --- /dev/null +++ b/tasmota/xdrv_86_esp32_sonoff_spm.ino @@ -0,0 +1,1129 @@ +/* + xdrv_86_esp32_sonoff_spm.ino - Sonoff SPM support for Tasmota + + Copyright (C) 2021 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 . +*/ + +//#define USE_SONOFF_SPM + +#ifdef ESP32 +#ifdef USE_SONOFF_SPM +/*********************************************************************************************\ + * Sonoff Stackable Power Manager (Current state: PROOF OF CONCEPT) + * + * {"NAME":"Sonoff SPM (POC1)","GPIO":[1,1,1,1,3200,1,1,1,1,1,1,1,3232,1,1,1,0,1,1,1,0,1,1,1,0,0,0,0,544,1,1,32,1,0,0,1],"FLAG":0,"BASE":1} + * + * Things to know: + * Bulk of the action is handled by ARM processors present in every unit communicating over modbus RS-485. + * Each SPM-4Relay has 4 bistable relays with their own CSE7761 energy monitoring device handled by an ARM processor. + * Green led is controlled by ARM processor indicating SD-Card access. + * ESP32 is used as interface between Welink and ARM processor in SPM-Main unit communicating over proprietary serial protocol. + * Inductive/Capacitive loads are not reported correctly. + * Power on sequence for two SPM-4Relay modules is 00-00-15-10-(0F)-(13)-(13)-(19)-0C-09-04-09-04-0B-0B + * + * Tasmota POC1: + * Up to 7 SPM-4Relay units supporting up to 28 relays. + * Gui rotating energy display for 4 relays at a time. + * Button on SPM-Main initiates re-scan of SPM-4Relay units. + * Blue led equals Tasmota WiFi status. + * + * Todo: + * Ethernet support (Find correct MDIO, MDC, POWER GPIO's and ETH_ parameters). + * Gui optimization for energy display. + * Gui for Overload Protection entry (is handled by ARM processor). + * Gui for Scheduling entry (is handled by ARM processor). + * Yellow led functionality. + * Interpretation of reset sequence on GPIO's 12-14. + * + * Nice to have: + * Support for all 32 SPM4Relay units equals 128 relays + * + * GPIO's used: + * GPIO00 - Bootmode / serial flash + * GPIO01 - Serial console TX (921600bps8N1 originally) + * GPIO03 - Serial console RX + * GPIO04 - ARM processor TX (115200bps8N1) + * GPIO12 - SPI MISO ARM pulsetrain code (input?) + * GPIO13 - SPI CLK + * GPIO14 - SPI CS ARM pulsetrain eoc (input?) + * GPIO15 - ARM reset (output) - 18ms low active 125ms after restart esp32 + * GPIO16 - ARM processor RX + * GPIO17 - EMAC_CLK_OUT_180 + * GPIO18 - ??ETH MDIO + * GPIO19 - EMAC_TXD0(RMII) + * GPIO21 - EMAC_TX_EN(RMII) + * GPIO22 - EMAC_TXD1(RMII) + * GPIO23 - ??ETH MDC + * GPIO25 - EMAC_RXD0(RMII) + * GPIO26 - EMAC_RXD1(RMII) + * GPIO27 - EMAC_RX_CRS_DV + * GPIO?? - ??ETH POWER + * GPIO32 - Blue status led2 + * GPIO33 - Yellow error led3 + * GPIO35 - Button + * #define ETH_TYPE ETH_PHY_LAN8720 + * #define ETH_CLKMODE ETH_CLOCK_GPIO0_IN + * #define ETH_ADDRESS 0 + * + * Variables used: + * module = 0 to 31 SPM-4Relays + * channel = 0 to 3 or 01, 02, 04, 08 Bitmask of four relays in module + * relay = 0 to 127 Relays +\*********************************************************************************************/ + +#define XDRV_86 86 + +#define SSPM_MAX_MODULES 7 // Currently supports up to 7 SPM-4RELAY units for a total of 28 relays restricted by power_t size +#define SSPM_SERIAL_BUFFER_SIZE 512 // Needs to accomodate Energy total history for six months (408 bytes) + +// Send +#define SSPM_FUNC_FIND 0 // 0x00 +#define SSPM_FUNC_SET_OPS 3 // 0x03 - Overload Protection +#define SSPM_FUNC_GET_OPS 4 // 0x04 +#define SSPM_FUNC_SET_RELAY 8 // 0x08 +#define SSPM_FUNC_GET_MODULE_STATE 9 // 0x09 - State of four channels +#define SSPM_FUNC_SET_SCHEME 10 // 0x0A +#define SSPM_FUNC_GET_SCHEME 11 // 0x0B +#define SSPM_FUNC_SET_TIME 12 // 0x0C +#define SSPM_FUNC_INIT_SCAN 16 // 0x10 +#define SSPM_FUNC_UNITS 21 // 0x15 +#define SSPM_FUNC_GET_ENERGY_TOTAL 22 // 0x16 +#define SSPM_FUNC_GET_ENERGY 24 // 0x18 +#define SSPM_FUNC_GET_LOG 26 // 0x1A + +#define SSPM_FUNC_ENERGY_PERIOD 27 // 0x1B + +// Receive +#define SSPM_FUNC_ENERGY_RESULT 6 // 0x06 +#define SSPM_FUNC_KEY_PRESS 7 // 0x07 +#define SSPM_FUNC_SCAN_START 15 // 0x0F +#define SSPM_FUNC_SCAN_RESULT 19 // 0x13 +#define SSPM_FUNC_SCAN_DONE 25 // 0x19 + +#define SSPM_GPIO_ARM_TX 4 +#define SSPM_GPIO_ARM_RX 16 +#define SSPM_GPIO_ARM_RESET 15 +#define SSPM_GPIO_PULSE_OUT 13 +#define SSPM_GPIO_PULSE_IN1 12 +#define SSPM_GPIO_PULSE_IN2 14 +#define SSPM_GPIO_LED_STATUS 32 +#define SSPM_GPIO_LED_ERROR 33 + +#define SSPM_MODULE_NAME_SIZE 12 + +enum SspmInitSteps { SPM_NONE, SPM_WAIT, SPM_RESET, SPM_POLL, SPM_POLL_ACK, SPM_SEND_FUNC_UNITS, SPM_STEP_WAIT2, + SPM_DEVICES_FOUND, + SPM_GET_ENERGY_TOTALS, + SPM_ALLOW_LOOP}; + +#include +TasmotaSerial *SspmSerial; + +typedef struct { + float voltage[SSPM_MAX_MODULES][4]; // 123.12 V + float current[SSPM_MAX_MODULES][4]; // 123.12 A + float active_power[SSPM_MAX_MODULES][4]; // 123.12 W + float apparent_power[SSPM_MAX_MODULES][4]; // 123.12 VA + float reactive_power[SSPM_MAX_MODULES][4]; // 123.12 VAr + float power_factor[SSPM_MAX_MODULES][4]; // 0.12 + float total[SSPM_MAX_MODULES][4]; // 12345 kWh total energy + + uint32_t timeout; + power_t old_power; + uint16_t serial_in_byte_counter; + uint16_t expected_bytes; + uint8_t module[SSPM_MAX_MODULES][SSPM_MODULE_NAME_SIZE]; + + uint8_t allow_updates; + uint8_t get_energy_relay; + uint8_t rotate; + uint8_t module_max; + uint8_t module_selected; + uint8_t no_send_key; + uint8_t counter; + uint8_t command_sequence; + uint8_t loop_step; + uint8_t init_step; + uint8_t last_button; + bool discovery_triggered; +} TSspm; + +uint8_t *SspmBuffer = nullptr; +TSspm *Sspm = nullptr; + +void SSPMSetLock(uint32_t seconds) { + Sspm->timeout = seconds * 10; // Decremented every 100mSec + Sspm->allow_updates = 0; // Disable requests from 100mSec loop +} + +uint16_t SSPMCalculateCRC(uint8_t *frame, uint32_t num) { + // CRC-16/ARC (polynomial 0x8005 reflected as 0xA001) + uint16_t crc = 0; + for (uint32_t i = 2; i < num; i++) { + crc ^= frame[i]; + for (uint32_t i = 0; i < 8; i++) { + crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1; + } + } + return crc ^ 0; +} + +void SSPMTime(uint8_t *frame) { + /* + 0 1 2 3 4 5 6 + YY YY MM DD HH MM SS + 07 e5 0b 06 0c 39 01 + */ + TIME_T time; + BreakTime(Rtc.utc_time, time); + uint16_t year = time.year + 1970; + frame[0] = year >> 8; + frame[1] = year; + frame[2] = time.month; + frame[3] = time.day_of_month; + frame[4] = time.hour; + frame[5] = time.minute; + frame[6] = time.second; +} + +void SSPMSend(uint32_t size) { + uint16_t crc = SSPMCalculateCRC(SspmBuffer, size -2); + SspmBuffer[size -2] = (uint8_t)(crc >> 8); + SspmBuffer[size -1] = (uint8_t)crc; + + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("SPM: ESP %*_H"), size, SspmBuffer); + + SspmSerial->write(SspmBuffer, size); +} + +void SSPMInitSend(void) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + Marker |Module id |Ac|Cm|Size | + */ + memset(SspmBuffer, 0, 19); + SspmBuffer[0] = 0xAA; + SspmBuffer[1] = 0x55; + SspmBuffer[2] = 0x01; +} + +void SSPMSendCmnd(uint32_t command) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FC 51 + Marker |Module id |Ac|Cm|Size |Ix|Chksm| + */ + SSPMInitSend(); + SspmBuffer[16] = command; + if (0 == command) { + Sspm->command_sequence = 0; + } else { + Sspm->command_sequence++; + } + SspmBuffer[19] = Sspm->command_sequence; + + SSPMSend(22); +} + +void SSPMSendInitScan(void) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + AA 55 01 ff ff ff ff ff ff ff ff ff ff ff ff 00 10 00 00 02 cd f0 + Marker |Module id |Ac|Cm|Size |Ix|Chksm| + + Acknowledge: + AA 55 01 ff ff ff ff ff ff ff ff ff ff ff ff 80 10 00 01 00 02 e5 03 + |Ac|Cm|Size |Rt|Ix|Chksm| + */ + SSPMSetLock(30); // Disable requests from 100mSec loop + + memset(SspmBuffer, 0xFF, 15); + SspmBuffer[0] = 0xAA; + SspmBuffer[1] = 0x55; + SspmBuffer[2] = 0x01; + + SspmBuffer[15] = 0; + SspmBuffer[16] = SSPM_FUNC_INIT_SCAN; // 0x10 + SspmBuffer[17] = 0; + SspmBuffer[18] = 0; + Sspm->command_sequence++; + SspmBuffer[19] = Sspm->command_sequence; + + SSPMSend(22); + + AddLog(LOG_LEVEL_DEBUG, PSTR("SPM: Start relay scan...")); +} + +void SSPMSendOPS(uint32_t relay_num) { + /* + Overload Protection + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 00 03 00 12 04 00 11 30 00 00 00 0a 00 f0 00 00 00 0a 00 14 00 00 fb a6 f8 = Default settings + Marker |Module id |Ac|Cm|Size |Ch|Ra|Max P |Min P |Max U |Min U |Max I |De|Ix|Chksm| + | | | 4400W| 0.1W| 240V| 0.1V| 20A| | + Ch - Bitmask channel 01 = 1, 02 = 2, 04 = 3, 08 = 4 + Ra - Bitmask enabled features xxxxxxx1 Enable Max current + Ra - Bitmask enabled features xxxxxx1x Enable Min voltage + Ra - Bitmask enabled features xxxxx1xx Enable Max voltage + Ra - Bitmask enabled features xxxx1xxx Enable Min power + Ra - Bitmask enabled features xxx1xxxx Enable Max power + De - 0 to 255 seconds Overload detection delay + Values are XX XX - number + XX - decimals + + Acknowledge: + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 80 03 00 01 00 14 08 bc + |Ac|Cm|Size |Rt|Ix|Chksm| + Ac - Acknowledge or error number + Rt - Return code + */ + + SspmBuffer[16] = SSPM_FUNC_SET_OPS; // 0x03 + +} + +void SSPMSendScheme(uint32_t relay) { + /* + Time scheme + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 + One time + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 1e 01 01 01 07 e5 0b 0e 0b 38 08 00 6b 01 00 ea 60 20 23 1b 04 fd 7a 83 05 63 ee dd a9 b9 3a 7e 14 95 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 1e 01 01 01 07 e5 0b 0e 0c 04 35 00 55 01 02 46 76 0e 0c 20 e1 22 7c 67 ab 9c 66 73 6d bd e8 7f 50 d4 + Marker |Module id |Ac|Cm|Size |No| |Mo| YYYY|MM|DD|HH|MM |St|Re|Scheme id | + No - Number of schemes defined + Mo - Scheme type (1 = temporarly, 2 = scheduled) + Re - Relay 0 to 3 + St - State (0 = off, 1 = On) + + Scheduled On + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 15 0c 0c 01 03 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 80 5b 48 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 15 0c 0c 01 03 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 82 9a c9 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 53 0c 0c 01 03 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 83 44 aa + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 53 0d 0b 00 02 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 84 e0 22 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 0e 0d 3b 01 03 84 fb ea 35 ca 16 51 b5 b8 10 a1 1c d0 1a 3f 7a 86 e3 fa + + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 2f 02 01 02 0e 0d 3b 01 03 84 fb ea 35 ca 16 51 b5 b8 10 a1 1c d0 1a 3f 7a + 01 02 53 0d 0b 00 02 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 87 e8 02 + + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 2f 02 01 02 53 0d 0b 00 02 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 + 01 02 0e 0d 3b 01 03 84 fb ea 35 ca 16 51 b5 b8 10 a1 1c d0 1a 3f 7a 89 6e e6 + + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 4c 03 01 01 07 e5 0b 0e 0e 0e 26 00 e7 01 00 e6 b2 48 8e ef be ce 78 3e 5d a8 3a c0 c5 6f 5e = One time + 01 02 53 0d 0b 00 02 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 = 14:11 OFF CH3 SuMoThSa + 01 02 0e 0d 3b 01 03 84 fb ea 35 ca 16 51 b5 b8 10 a1 1c d0 1a 3f 7a 8a 2f f8 = 14:59 ON CH4 MoTuWe + + Marker |Module id |Ac|Cm|Size |No| |Mo|Dy|HH|MM|St|Re|Scheme id | + Dy - Bitmask days xxxxxxx1 sunday + xxxxxx1x monday + xxxxx1xx tuesday + xxxx1xxx wednesday + xxx1xxxx thursday + xx1xxxxx friday + x1xxxxxx saturday + + Scheduled Off + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 01 00 81 26 9f + Schedule 2 off + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 0a 00 18 01 01 02 53 0d 0b 00 02 99 65 93 dc f8 d0 b0 29 a8 66 ba 8f 41 66 29 24 88 e5 22 + Marker |Module id |Ac|Cm|Size | + */ + + SspmBuffer[16] = SSPM_FUNC_SET_SCHEME; // 0x0A + +} + +void SSPMSendSetTime(void) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 0c 00 0b 07 e5 0b 06 0c 39 01 00 00 02 00 04 8a 37 + Marker |Module id |Ac|Cm|Size |YY YY MM DD HH MM SS|Ln|St|Tr| |Ix|Chksm| + */ + SSPMInitSend(); + SspmBuffer[16] = SSPM_FUNC_SET_TIME; + SspmBuffer[18] = 0x0B; + SSPMTime(SspmBuffer + 19); + SspmBuffer[26] = 0x00; + SspmBuffer[27] = 0x00; + SspmBuffer[28] = 0x02; + SspmBuffer[29] = 0x00; + Sspm->command_sequence++; + SspmBuffer[30] = Sspm->command_sequence; + + SSPMSend(33); +} + +void SSPMSendSetRelay(uint32_t relay, uint32_t state) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 00 08 00 01 44 08 c0 34 + Marker |Module id |Ac|Cm|Size |Pl|Ix|Chksm| + */ + uint8_t channel = 1 << (relay & 0x03); // Channel relays are bit masked + if (state) { + channel |= (channel << 4); + } + uint8_t module = relay >> 2; + SSPMInitSend(); + memcpy(SspmBuffer +3, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[16] = SSPM_FUNC_SET_RELAY; + SspmBuffer[18] = 0x01; + SspmBuffer[19] = channel; + Sspm->command_sequence++; + SspmBuffer[20] = Sspm->command_sequence; + + SSPMSend(23); +} + +void SSPMSendGetModuleState(uint32_t module) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 00 09 00 01 0f 05 b5 de + Marker |Module id |Ac|Cm|Size |Pl|Ix|Chksm| + */ + SSPMInitSend(); + memcpy(SspmBuffer +3, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[16] = SSPM_FUNC_GET_MODULE_STATE; + SspmBuffer[18] = 0x01; + SspmBuffer[19] = 0x0F; // State of all four relays + Sspm->command_sequence++; + SspmBuffer[20] = Sspm->command_sequence; + + SSPMSend(23); +} + +void SSPMSendGetOps(uint32_t module) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + aa 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 00 04 00 00 08 c0 0a + Marker |Module id |Ac|Cm|Size |Ix|Chksm| + */ + SSPMInitSend(); + memcpy(SspmBuffer +3, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[16] = SSPM_FUNC_GET_OPS; + Sspm->command_sequence++; + SspmBuffer[19] = Sspm->command_sequence; + + SSPMSend(22); +} + +void SSPMSendGetScheme(uint32_t module) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 00 0b 00 00 09 14 c8 + Marker |Module id |Ac|Cm|Size |Ix|Chksm| + */ + SSPMInitSend(); + memcpy(SspmBuffer +3, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[16] = SSPM_FUNC_GET_SCHEME; + Sspm->command_sequence++; + SspmBuffer[19] = Sspm->command_sequence; + + SSPMSend(22); +} + +void SSPMSendGetEnergy(uint32_t relay) { + /* + relay_num = 1..8 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 18 00 10 6b 7e 32 37 39 37 34 13 4b 35 36 37 01 01 00 3c 2a db d1 + */ + uint8_t module = relay >> 2; + uint8_t channel = 1 << (relay & 0x03); // Channel relays are bit masked + SSPMInitSend(); + SspmBuffer[16] = SSPM_FUNC_GET_ENERGY; + SspmBuffer[18] = 0x10; + memcpy(SspmBuffer +19, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[31] = 0x01; + SspmBuffer[32] = channel; + SspmBuffer[33] = 0; + SspmBuffer[34] = 0x3C; + Sspm->command_sequence++; + SspmBuffer[35] = Sspm->command_sequence; + + SSPMSend(38); +} + +void SSPMSendGetEnergyTotal(uint32_t relay) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 16 00 0d 6b 7e 32 37 39 37 34 13 4b 35 36 37 01 14 e6 93 + */ + uint8_t module = relay >> 2; + uint8_t channel = relay & 0x03; // Channel relays are NOT bit masked this time + SSPMInitSend(); + SspmBuffer[16] = SSPM_FUNC_GET_ENERGY_TOTAL; + SspmBuffer[18] = 0x0D; + memcpy(SspmBuffer +19, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[31] = channel; + Sspm->command_sequence++; + SspmBuffer[32] = Sspm->command_sequence; + + SSPMSend(35); +} + +void SSPMSendGetLog(uint32_t relay, uint32_t entries) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 1a 00 10 6b 7e 32 37 39 37 34 13 4b 35 36 37 00 00 00 1d 09 8c cd + */ + uint8_t module = relay >> 2; + SSPMInitSend(); + SspmBuffer[16] = SSPM_FUNC_GET_LOG; + SspmBuffer[18] = 0x10; + memcpy(SspmBuffer +19, Sspm->module[module], SSPM_MODULE_NAME_SIZE); + SspmBuffer[31] = 0; + SspmBuffer[32] = 0; + SspmBuffer[33] = 0; + SspmBuffer[34] = entries; // Number of logs + Sspm->command_sequence++; + SspmBuffer[35] = Sspm->command_sequence; + + SSPMSend(38); +} + +void SSPMSendAck(uint32_t command_sequence) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 0f 00 01 00 01 3d e6 + Marker |Module id |Ac|Cm|Size |Pl|Ix|Chksm| + */ + SspmBuffer[15] = 0x80; + SspmBuffer[17] = 0x00; + SspmBuffer[18] = 0x01; + SspmBuffer[19] = 0x00; + SspmBuffer[20] = command_sequence; + + SSPMSend(23); +} + +void SSPMHandleReceivedData(void) { + uint8_t command = SspmBuffer[16]; + bool ack = (0x80 == SspmBuffer[15]); + uint8_t command_sequence = SspmBuffer[19 + Sspm->expected_bytes]; + +// AddLog(LOG_LEVEL_DEBUG, PSTR("SPM: Rcvd ack %d, cmnd %d, seq %d, size %d"), +// ack, command, command_sequence, Sspm->expected_bytes); + + if (ack) { + // Responses from ARM (Acked) + switch(command) { + case SSPM_FUNC_FIND: + /* 0x00 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 01 00 00 fc 73 + |Er| |St| + */ + if ((1 == Sspm->expected_bytes) && (0 == SspmBuffer[19])) { + Sspm->init_step++; + } + break; + case SSPM_FUNC_GET_OPS: + /* 0x04 - Overload Protection + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 80 04 00 02 00 00 06 98 06 + Marker |Module id |Ac|Cm|Size | |Ch|Ra|Max P |Min P |Max U |Min U |Max I |De|Ix|Chksm| + | | | 4400W| 0.1W| 240V| 0.1V| 20A| | + AA 55 01 6B 7E 32 37 39 37 34 13 4B 35 36 37 80 04 00 35 00 07 00 11 30 00 00 00 0A 00 F0 00 00 00 0A 00 14 00 00 + 00 11 30 00 00 00 0A 00 F0 00 00 00 0A 00 14 00 00 + 00 11 30 00 00 00 0A 00 F0 00 00 00 0A 00 14 00 00 07 8A 86 + */ + if (0x02 == Sspm->expected_bytes) { + + } + + Sspm->module_selected--; + if (Sspm->module_selected > 0) { + SSPMSendGetModuleState(Sspm->module_selected -1); + } else { + SSPMSendGetScheme(Sspm->module_selected); + } + break; + case SSPM_FUNC_GET_MODULE_STATE: + /* 0x09 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 + AA 55 01 8b 34 32 37 39 37 34 13 4b 35 36 37 80 09 00 06 00 0f 01 01 01 01 05 fe 35 + |OS|4RelayMasks| + */ + if (0x06 == Sspm->expected_bytes) { + // SspmBuffer[20] & 0x0F // Relays operational + power_t current_state = SspmBuffer[20] >> 4; // Relays state + power_t mask = 0x0000000F; + for (uint32_t i = 0; i < Sspm->module_max; i++) { + if ((SspmBuffer[3] == Sspm->module[i][0]) && (SspmBuffer[4] == Sspm->module[i][1])) { + current_state <<= (i * 4); + mask <<= (i * 4); + TasmotaGlobal.power &= (POWER_MASK ^ mask); + TasmotaGlobal.power |= current_state; + break; + } + } + Sspm->old_power = TasmotaGlobal.power; + TasmotaGlobal.devices_present += 4; + } + SSPMSendGetOps(Sspm->module_selected -1); + break; + case SSPM_FUNC_GET_SCHEME: + /* 0x0B + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 80 0b 00 02 00 00 09 bb c7 + |?? ??| + */ + if (0x02 == Sspm->expected_bytes) { + + } + Sspm->module_selected++; + if (Sspm->module_selected < Sspm->module_max) { + SSPMSendGetScheme(Sspm->module_selected); + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("SPM: Relay scan done")); + + Sspm->init_step = SPM_DEVICES_FOUND; +// Sspm->get_energy_relay = 1; +// Sspm->allow_updates = 1; // Enable requests from 100mSec loop + } + break; + case SSPM_FUNC_SET_TIME: + /* 0x0C + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 0c 00 01 00 04 3e 62 + */ + SSPMSendGetModuleState(Sspm->module_selected -1); + break; + case SSPM_FUNC_INIT_SCAN: + /* 0x10 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + AA 55 01 ff ff ff ff ff ff ff ff ff ff ff ff 80 10 00 01 00 02 e5 03 + */ + Sspm->module_max = 0; + break; + case SSPM_FUNC_UNITS: + /* 0x15 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 15 00 04 00 01 00 00 01 81 b1 + |?? ?? ?? ??| + */ + SSPMSendInitScan(); + break; + case SSPM_FUNC_GET_ENERGY_TOTAL: + /* 0x16 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 16 01 7e 00 8b 34 32 37 39 37 34 13 4b 35 36 37 + 03 <- L4 + 07 e5 0b 0d <- End date (Today) 2021 nov 13 + 07 e5 05 11 <- Start date 2021 may 17 + 00 05 <- 5kWh (13/11 Today) + 00 00 <- 0 (12/11 Yesterday) + 00 04 <- 4kWh (11/11 etc) + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 + 42 67 46 + */ + { + uint32_t total_energy = 0; + uint32_t entries = (Sspm->expected_bytes - 22) / 2; + for (uint32_t i = 0; i < entries; i++) { + total_energy += (SspmBuffer[41 + (i*2)] << 8) + SspmBuffer[42 + (i*2)]; + } + uint32_t channel = SspmBuffer[32]; + for (uint32_t module = 0; module < Sspm->module_max; module++) { + if ((SspmBuffer[20] == Sspm->module[module][0]) && (SspmBuffer[21] == Sspm->module[module][1])) { + Sspm->total[module][channel] = total_energy; // xkWh + break; + } + } + Sspm->allow_updates = 1; + } + break; + case SSPM_FUNC_ENERGY_PERIOD: + /* 0x1B + Response after start energy period + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 1b 00 0e [00] 8b 34 32 37 39 37 34 13 4b 35 36 37 [03] f7 b1 bc L4 + Response after refresh or stop energy period + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 1b 00 11 [00] 8b 34 32 37 39 37 34 13 4b 35 36 37 [03] [00 00 00] f8 94 15 L4, kWh start period (0) + */ + + break; + } + } else { + // Initiated by ARM + switch(command) { + case SSPM_FUNC_ENERGY_RESULT: + /* 0x06 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1c 6b 7e 32 37 39 37 34 13 4b 35 36 37 01 00 00 00 e3 5b 00 00 00 00 00 00 00 00 00 6b 1f 95 1e + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1C 8B 34 32 37 39 37 34 13 4B 35 36 37 01 00 0B 00 E4 37 00 19 0E 00 00 02 00 19 09 4B 28 1D 71 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1C 8B 34 32 37 39 37 34 13 4B 35 36 37 08 00 0A 00 E3 61 00 18 2E 00 00 00 00 18 33 4B 27 D3 0D + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1C 8B 34 32 37 39 37 34 13 4B 35 36 37 08 02 04 00 DC 14 01 C1 3D 00 10 19 01 C2 29 4B 37 6B 26 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1c 8b 34 32 37 39 37 34 13 4b 35 36 37 08 00 44 00 e1 35 00 9a 3e 00 01 45 00 9a 38 00 08 8b ae + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 06 00 1c 8b 34 32 37 39 37 34 13 4b 35 36 37 08 00 4a 00 e1 22 00 61 4d 00 2c 38 00 a8 28 20 26 21 70 + |Ch|Curre|Voltage |ActivePo|Reactive|Apparent|??| + Values are XX XX - number + XX - decimals + + + + Curr Voltag Active Reacti Appare + 0 1 2 3 4 5 6 7 8 910111213141516171819202122232425262728293031 3233 343536 373839 404142 434445 46 47 4849 + AA55010000000000000000000000000006001C8B343237393734134B35363708 000A 00E05B 001817 00013B 001825 4B BC 3DDA 0.100A 224.91V 24.23W <-- 25W bulb + AA55010000000000000000000000000006001C8B343237393734134B35363708 000A 00E115 00181A 00013D 001823 4B BE 6209 + AA55010000000000000000000000000006001C8B343237393734134B35363708 0044 00E260 009C1C 000000 009C1B 00 36 FD69 0.680A 226.96V 156.28W <-- 150W bulb + + AA55010000000000000000000000000006001C8B343237393734134B35363708 0054 00E134 007525 00220A 00BD5D 20 34 55D6 + AA55010000000000000000000000000006001C8B343237393734134B35363708 0054 00E10A 007519 002126 00BD27 20 36 77EA + AA55010000000000000000000000000006001C8B343237393734134B35363708 0053 00DE40 00731F 001604 00B952 4B 12 9255 + + AA55010000000000000000000000000006001C8B343237393734134B35363708 075B 00D502 06940F 001863 069830 4B 1C E0DE + + AA55010000000000000000000000000006001c8b343237393734134b35363708 0044 00e025 009920 00010f 00993b 00 b3 07 a2 0.68A 223.25V 152.66W 0.54 Rea 152.5 Schijn + + + */ + { + uint32_t channel = 0; + for (channel = 0; channel < 4; channel++) { + if (SspmBuffer[31] & 1) { break; } + SspmBuffer[31] >>= 1; + } + for (uint32_t module = 0; module < Sspm->module_max; module++) { + if ((SspmBuffer[19] == Sspm->module[module][0]) && (SspmBuffer[20] == Sspm->module[module][1])) { + Sspm->current[module][channel] = SspmBuffer[32] + (float)SspmBuffer[33] / 100; // x.xxA + Sspm->voltage[module][channel] = (SspmBuffer[34] << 8) + SspmBuffer[35] + (float)SspmBuffer[36] / 100; // x.xxV + Sspm->active_power[module][channel] = (SspmBuffer[37] << 8) + SspmBuffer[38] + (float)SspmBuffer[39] / 100; // x.xxW + Sspm->reactive_power[module][channel] = (SspmBuffer[40] << 8) + SspmBuffer[41] + (float)SspmBuffer[42] / 100; // x.xxVAr + Sspm->apparent_power[module][channel] = (SspmBuffer[43] << 8) + SspmBuffer[44] + (float)SspmBuffer[45] / 100; // x.xxVA + break; + } + } + SSPMSendAck(command_sequence); + Sspm->allow_updates = 1; + } + break; + case SSPM_FUNC_KEY_PRESS: + /* 0x07 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 0d 6b 7e 32 37 39 37 34 13 4b 35 36 37 11 04 bf c3 + |AS| + */ + if (!Sspm->no_send_key) { + power_t relay = SspmBuffer[31] & 0x0F; // Relays active + power_t relay_state = SspmBuffer[31] >> 4; // Relays state + for (uint32_t i = 0; i < Sspm->module_max; i++) { + if ((SspmBuffer[19] == Sspm->module[i][0]) && (SspmBuffer[20] == Sspm->module[i][1])) { + relay <<= (i * 4); + relay_state <<= (i * 4); + break; + } + } + for (uint32_t i = 1; i <= TasmotaGlobal.devices_present; i++) { + if (relay &1) { + ExecuteCommandPower(i, relay_state &1, SRC_BUTTON); + } + relay >>= 1; + relay_state >>= 1; + } + Sspm->old_power = TasmotaGlobal.power; + } + SSPMSendAck(command_sequence); + break; + case SSPM_FUNC_SCAN_START: + /* 0x0F + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 0f 00 01 02 01 9d f8 + */ +// Sspm->module_max = 0; + SSPMSendAck(command_sequence); + break; + case SSPM_FUNC_SCAN_RESULT: + /* 0x13 + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 13 00 24 6b 7e 32 37 39 37 34 13 4b 35 36 37 04 00 00 00 82 01 00 00 14 00 00 0a 00 f0 00 00 00 0a 11 30 00 00 00 0a 02 8f cd + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 13 00 24 8b 34 32 37 39 37 34 13 4b 35 36 37 04 00 00 00 82 01 00 00 14 00 00 0a 00 f0 00 00 00 0a 11 30 00 00 00 0a 02 a0 6f + */ + if ((0x24 == Sspm->expected_bytes) && (Sspm->module_max < SSPM_MAX_MODULES)) { + memcpy(Sspm->module[1], Sspm->module[0], (SSPM_MAX_MODULES -1) * SSPM_MODULE_NAME_SIZE); + memcpy(Sspm->module[0], SspmBuffer + 19, SSPM_MODULE_NAME_SIZE); + Sspm->module_max++; + } + SSPMSendAck(command_sequence); + break; + case SSPM_FUNC_SCAN_DONE: + /* 0x19 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 19 00 00 03 a1 16 + */ + SSPMSendAck(command_sequence); + Sspm->module_selected = Sspm->module_max; + SSPMSendSetTime(); + break; + } + } +} + +void SSPMSerialInput(void) { + /* + 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 00 19 00 00 03 a1 16 + Marker |Module id |Ac|Cm|Size |Ix|Chksm| + AA 55 01 00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 01 00 00 fc 73 + Marker |Module id |Ac|Cm|Size |Pl|Ix|Chksm| + AA 55 01 6b 7e 32 37 39 37 34 13 4b 35 36 37 80 09 00 06 00 0f 01 01 01 01 05 f9 9d + Marker |Module id |Ac|Cm|Size |Payload |Ix|Chksm| + 00 Request + 80 Response (Ack) + */ + while (SspmSerial->available()) { + yield(); + uint8_t serial_in_byte = SspmSerial->read(); + + if ((0x55 == serial_in_byte) && (0xAA == SspmBuffer[Sspm->serial_in_byte_counter -1])) { + Sspm->expected_bytes = 0; + SspmBuffer[0] = 0xAA; + Sspm->serial_in_byte_counter = 1; + } + if (Sspm->serial_in_byte_counter < SSPM_SERIAL_BUFFER_SIZE -1) { + SspmBuffer[Sspm->serial_in_byte_counter++] = serial_in_byte; + if (19 == Sspm->serial_in_byte_counter) { + Sspm->expected_bytes = (SspmBuffer[Sspm->serial_in_byte_counter -2] << 8) + SspmBuffer[Sspm->serial_in_byte_counter -1]; + } + if (Sspm->serial_in_byte_counter == (22 + Sspm->expected_bytes)) { + + AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("SPM: ARM %*_H"), Sspm->serial_in_byte_counter, SspmBuffer); + + uint16_t crc_rcvd = (SspmBuffer[Sspm->serial_in_byte_counter -2] << 8) + SspmBuffer[Sspm->serial_in_byte_counter -1]; + uint16_t crc_calc = SSPMCalculateCRC(SspmBuffer, Sspm->serial_in_byte_counter -2); + if (crc_rcvd == crc_calc) { + SSPMHandleReceivedData(); + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("SPM: CRC error")); + } + Sspm->serial_in_byte_counter = 0; + Sspm->expected_bytes = 0; + } + } else { + AddLog(LOG_LEVEL_DEBUG, PSTR("SPM: Serial input buffer overflow")); + Sspm->serial_in_byte_counter = 0; + Sspm->expected_bytes = 0; + } + } +} + +void SSPMInit(void) { + if (!ValidTemplate(PSTR("Sonoff SPM (POC1)"))) { return; } + if (!PinUsed(GPIO_RXD) || !PinUsed(GPIO_TXD)) { return; } + + Sspm = (TSspm*)calloc(sizeof(TSspm), 1); + if (!Sspm) { return; } + SspmBuffer = (uint8_t*)malloc(SSPM_SERIAL_BUFFER_SIZE); + if (!SspmBuffer) { + free(Sspm); + return; + } + SspmSerial = new TasmotaSerial(Pin(GPIO_RXD), Pin(GPIO_TXD), 1, 0, SSPM_SERIAL_BUFFER_SIZE); + if (!SspmSerial->begin(115200)) { + free(SspmBuffer); + free(Sspm); + return; + } + + pinMode(SSPM_GPIO_ARM_RESET, OUTPUT); + digitalWrite(SSPM_GPIO_ARM_RESET, 1); + + if (0 == Settings->flag2.voltage_resolution) { + Settings->flag2.voltage_resolution = 1; // SPM has only 2 decimals + Settings->flag2.current_resolution = 2; // SPM has only 2 decimals + Settings->flag2.wattage_resolution = 2; // SPM has only 2 decimals + Settings->flag2.energy_resolution = 0; // SPM has no decimals on total energy + } + + Sspm->old_power = TasmotaGlobal.power; + Sspm->init_step = SPM_WAIT; // Start init sequence +} + +void SSPMEvery100ms(void) { + if (Sspm->no_send_key) { Sspm->no_send_key--; } + + if (Sspm->timeout) { + Sspm->timeout--; + if (!Sspm->timeout) { + Sspm->allow_updates = 1; + } + } + + // Fix race condition if the ARM doesn't respond + if ((Sspm->init_step > SPM_NONE) && (Sspm->init_step < SPM_SEND_FUNC_UNITS)) { + Sspm->counter++; + if (Sspm->counter > 20) { + Sspm->init_step = SPM_NONE; + } + } + switch (Sspm->init_step) { + case SPM_NONE: + return; + case SPM_WAIT: + Sspm->init_step++; + break; + case SPM_RESET: + // Reset ARM + digitalWrite(SSPM_GPIO_ARM_RESET, 0); + delay(18); + digitalWrite(SSPM_GPIO_ARM_RESET, 1); + delay(18); + Sspm->init_step++; + case SPM_POLL: + SSPMSendCmnd(SSPM_FUNC_FIND); + break; + case SPM_POLL_ACK: + SSPMSendCmnd(SSPM_FUNC_FIND); + break; + case SPM_SEND_FUNC_UNITS: + Sspm->init_step++; + SSPMSendCmnd(SSPM_FUNC_UNITS); + break; + case SPM_DEVICES_FOUND: + TasmotaGlobal.discovery_counter = 1; // force TasDiscovery() + Sspm->get_energy_relay = 1; + Sspm->allow_updates = 1; // Enable requests from 100mSec loop + Sspm->init_step++; + break; + case SPM_GET_ENERGY_TOTALS: + // Retrieve Energy total status from up to 128 relays + if (Sspm->allow_updates && (Sspm->get_energy_relay > 0)) { + SSPMSetLock(4); + SSPMSendGetEnergyTotal(Sspm->get_energy_relay -1); + Sspm->get_energy_relay++; + if (Sspm->get_energy_relay > TasmotaGlobal.devices_present) { + Sspm->get_energy_relay = 1; + Sspm->init_step++; + } + } + break; + case SPM_ALLOW_LOOP: + // Retrieve Energy status from up to 128 relays + if (Sspm->allow_updates && (Sspm->get_energy_relay > 0)) { + power_t powered_on = TasmotaGlobal.power >> (Sspm->get_energy_relay -1); + if (0 == Sspm->loop_step) { + // Get energy total only once in any 256 requests to safe comms + if (powered_on &1) { + SSPMSetLock(4); + SSPMSendGetEnergyTotal(Sspm->get_energy_relay -1); + } + Sspm->get_energy_relay++; + if (Sspm->get_energy_relay > TasmotaGlobal.devices_present) { + Sspm->get_energy_relay = 1; + Sspm->loop_step++; + } + } else { + if (powered_on &1) { + SSPMSetLock(4); + SSPMSendGetEnergy(Sspm->get_energy_relay -1); + } else { + uint32_t relay_set = (Sspm->get_energy_relay -1) >> 2; + uint32_t relay_num = (Sspm->get_energy_relay -1) &3; + if (Sspm->voltage[relay_set][relay_num]) { + Sspm->voltage[relay_set][relay_num] = 0; + Sspm->current[relay_set][relay_num] = 0; + Sspm->active_power[relay_set][relay_num] = 0; + Sspm->apparent_power[relay_set][relay_num] = 0; + Sspm->reactive_power[relay_set][relay_num] = 0; + Sspm->power_factor[relay_set][relay_num] = 0; + } + } + Sspm->loop_step++; // Rolls over after 256 so allows for scanning at least all relays twice + Sspm->get_energy_relay++; + if ((Sspm->get_energy_relay > TasmotaGlobal.devices_present) || !Sspm->loop_step) { + Sspm->get_energy_relay = 1; + } + } + } + break; + } +} + +bool SSPMSetDevicePower(void) { + power_t new_power = XdrvMailbox.index; + if (new_power != Sspm->old_power) { + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + uint8_t new_state = (new_power >> i) &1; + if (new_state != ((Sspm->old_power >> i) &1)) { + SSPMSendSetRelay(i, new_state); + Sspm->no_send_key = 10; // Disable buttons for 10 * 0.1 second + } + } + Sspm->old_power = new_power; + } + return true; +} + +bool SSPMButton(void) { + bool result = false; + uint32_t button = XdrvMailbox.payload; + if ((PRESSED == button) && (NOT_PRESSED == Sspm->last_button)) { // Button pressed + SSPMSendInitScan(); + result = true; // Disable further button processing + } + Sspm->last_button = button; + return result; +} + +const char kSSPMEnergyPhases[] PROGMEM = "%*_f / %*_f / %*_f / %*_f|[%*_f,%*_f,%*_f,%*_f]"; + +char* SSPMEnergyFormat(char* result, float* input, uint32_t resolution, bool json) { + char layout[32]; + GetTextIndexed(layout, sizeof(layout), json, kSSPMEnergyPhases); + ext_snprintf_P(result, FLOATSZ * 4, layout, resolution, &input[0], resolution, &input[1], resolution, &input[2], resolution, &input[3]); + return result; +} + +const char HTTP_SSPMENERGY_SNS[] PROGMEM = + "{s}" D_POWERUSAGE_APPARENT "{m}%s " D_UNIT_VA "{e}" + "{s}" D_POWERUSAGE_REACTIVE "{m}%s " D_UNIT_VAR "{e}" + "{s}" D_ENERGY_TOTAL "{m}%s " D_UNIT_KILOWATTHOUR "{e}"; // {s} = , {m} = , {e} = + +void SSPMEnergyShow(bool json) { + if (!TasmotaGlobal.devices_present) { return; } // Not ready yet + + if (json) { + ResponseAppend_P(PSTR(",\"SPM\":{\"" D_JSON_TOTAL "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", 0, &Sspm->total[i >>2][i &3]); + } + ResponseAppend_P(PSTR("],\"" D_JSON_POWERUSAGE "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", Settings->flag2.wattage_resolution, &Sspm->active_power[i >>2][i &3]); + } + ResponseAppend_P(PSTR("],\"" D_JSON_APPARENT_POWERUSAGE "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", Settings->flag2.wattage_resolution, &Sspm->apparent_power[i >>2][i &3]); + } + ResponseAppend_P(PSTR("],\"" D_JSON_REACTIVE_POWERUSAGE "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", Settings->flag2.wattage_resolution, &Sspm->reactive_power[i >>2][i &3]); + } + ResponseAppend_P(PSTR("],\"" D_JSON_VOLTAGE "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", Settings->flag2.voltage_resolution, &Sspm->voltage[i >>2][i &3]); + } + ResponseAppend_P(PSTR("],\"" D_JSON_CURRENT "\":[")); + for (uint32_t i = 0; i < TasmotaGlobal.devices_present; i++) { + ResponseAppend_P(PSTR("%s%*_f"), (i>0)?",":"", Settings->flag2.current_resolution, &Sspm->current[i >>2][i &3]); + } + ResponseAppend_P(PSTR("]}")); + } else { + Sspm->rotate++; + if ((Sspm->rotate >> 2) == Sspm->module_max) { + Sspm->rotate = 0; + } + + uint32_t module = Sspm->rotate >> 2; + uint32_t relay_base = module * 4; + WSContentSend_P(PSTR("{s}" D_SENSOR_RELAY "{m}L%02d / L%02d / L%02d / L%02d{e}"), relay_base +1, relay_base +2, relay_base +3, relay_base +4); + char value_chr[FLOATSZ * 4]; + WSContentSend_PD(HTTP_SNS_VOLTAGE, SSPMEnergyFormat(value_chr, Sspm->voltage[module], Settings->flag2.voltage_resolution, json)); + WSContentSend_PD(HTTP_SNS_CURRENT, SSPMEnergyFormat(value_chr, Sspm->current[module], Settings->flag2.current_resolution, json)); + WSContentSend_PD(HTTP_SNS_POWER, SSPMEnergyFormat(value_chr, Sspm->active_power[module], Settings->flag2.wattage_resolution, json)); + char valu2_chr[FLOATSZ * 4]; + char valu3_chr[FLOATSZ * 4]; + WSContentSend_PD(HTTP_SSPMENERGY_SNS, SSPMEnergyFormat(value_chr, Sspm->apparent_power[module], Settings->flag2.wattage_resolution, json), + SSPMEnergyFormat(valu2_chr, Sspm->reactive_power[module], Settings->flag2.wattage_resolution, json), + SSPMEnergyFormat(valu3_chr, Sspm->total[module], Settings->flag2.energy_resolution, json)); + } +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +const char kSSPMCommands[] PROGMEM = "SSPM|" // Prefix + "Log|Energy|History|Scan" ; + +void (* const SSPMCommand[])(void) PROGMEM = { + &CmndSSPMLog, &CmndSSPMEnergy, &CmndSSPMEnergyHistory, &CmndSSPMScan }; + +void CmndSSPMLog(void) { + if ((XdrvMailbox.index < 1) || (XdrvMailbox.index > TasmotaGlobal.devices_present)) { XdrvMailbox.index = 1; } + XdrvMailbox.payload &= 0x1F; // Max 32 entries + SSPMSendGetLog(XdrvMailbox.index -1, XdrvMailbox.payload +1); + + ResponseCmndDone(); +} + +void CmndSSPMEnergy(void) { + if ((XdrvMailbox.index < 1) || (XdrvMailbox.index > TasmotaGlobal.devices_present)) { XdrvMailbox.index = 1; } + SSPMSendGetEnergy(XdrvMailbox.index -1); + + ResponseCmndDone(); +} + +void CmndSSPMEnergyHistory(void) { + if ((XdrvMailbox.index < 1) || (XdrvMailbox.index > TasmotaGlobal.devices_present)) { XdrvMailbox.index = 1; } + SSPMSendGetEnergyTotal(XdrvMailbox.index -1); + + ResponseCmndDone(); +} + +void CmndSSPMScan(void) { + SSPMSendInitScan(); + + ResponseCmndDone(); +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +bool Xdrv86(uint8_t function) +{ + bool result = false; + + if (FUNC_INIT == function) { + SSPMInit(); + } + else if (Sspm) { + switch (function) { + case FUNC_LOOP: + if (SspmSerial) { SSPMSerialInput(); } + break; + case FUNC_EVERY_100_MSECOND: + SSPMEvery100ms(); + break; + case FUNC_SET_DEVICE_POWER: + result = SSPMSetDevicePower(); + break; + case FUNC_EVERY_SECOND: + break; + case FUNC_JSON_APPEND: + SSPMEnergyShow(true); + break; +#ifdef USE_WEBSERVER + case FUNC_WEB_SENSOR: + SSPMEnergyShow(false); + break; +#endif // USE_WEBSERVER + case FUNC_COMMAND: + result = DecodeCommand(kSSPMCommands, SSPMCommand); + break; + case FUNC_BUTTON_PRESSED: + result = SSPMButton(); + break; + } + } + return result; +} + +#endif // USE_SONOFF_SPM +#endif // ESP32