diff --git a/README.md b/README.md
index 498cd2f80..524ad49ab 100644
--- a/README.md
+++ b/README.md
@@ -93,8 +93,8 @@ The following devices are supported:
- [BlitzWolf BW-SHP2 Smart Socket with Energy Monitoring](https://www.banggood.com/BlitzWolf-BW-SHP2-Smart-WIFI-Socket-EU-Plug-220V-16A-Work-with-Amazon-Alexa-Google-Assistant-p-1292899.html)
- [Luani HVIO board](https://luani.de/projekte/esp8266-hvio/)
- [Wemos D1 mini](https://wiki.wemos.cc/products:d1:d1_mini)
-- [HuaFan Smart Socket](HuaFan-Smart-Socket)
-- [Hyleton-313 Smart Plug](Hyleton-313-Smart-Plug)
+- [HuaFan Smart Socket](https://github.com/arendst/Sonoff-Tasmota/wiki/HuaFan-Smart-Socket)
+- [Hyleton-313 Smart Plug](https://github.com/arendst/Sonoff-Tasmota/wiki/Hyleton-313-Smart-Plug)
- [Allterco Shelly 1](https://shelly.cloud/shelly1-open-source/)
- [Allterco Shelly 2 with Energy Monitoring](https://shelly.cloud/shelly2/)
- NodeMcu and Ledunia
diff --git a/sonoff/_changelog.ino b/sonoff/_changelog.ino
index abf4572f6..04f209e10 100644
--- a/sonoff/_changelog.ino
+++ b/sonoff/_changelog.ino
@@ -1,5 +1,16 @@
-/* 6.2.1.9 20180928
+/* 6.2.1.10 20180930
+ * Add command RGBWWTable to support color calibration (#3933)
+ * Add support for Michael Haustein ESP Switch
+ * Add support for EXS Relay V5.0 (#3810)
+ * Fix timer offset -00:00 causing 12:00 hour offset (#3923)
+ * Add support for OBI Power Socket (#1988, #3944)
+ * Add support for Teckin Power Socket with Energy Monitoring (#3950)
+ *
+ * 6.2.1.9 20180928
* Add Apparent Power and Reactive Power to Energy Monitoring devices (#251)
+ * Add RF Receiver control to module MagicHome to be used on Arilux LC10 (#3792)
+ * Fix I2CScan invalid JSON error message (#3925)
+ * Fix invalid configuration restores and decode_config.py crc error when savedata = 0 (#3918)
*
* 6.2.1.8 20180926
* Change status JSON message providing more switch and retain information
diff --git a/sonoff/i18n.h b/sonoff/i18n.h
index 2090f4793..bc161c078 100644
--- a/sonoff/i18n.h
+++ b/sonoff/i18n.h
@@ -317,6 +317,7 @@
#define D_CMND_LEDTABLE "LedTable"
#define D_CMND_FADE "Fade"
#define D_CMND_PIXELS "Pixels"
+#define D_CMND_RGBWWTABLE "RGBWWTable"
#define D_CMND_ROTATION "Rotation"
#define D_CMND_SCHEME "Scheme"
#define D_CMND_SPEED "Speed"
diff --git a/sonoff/settings.h b/sonoff/settings.h
index bf862dd51..978b72b24 100644
--- a/sonoff/settings.h
+++ b/sonoff/settings.h
@@ -322,7 +322,9 @@ struct SYSCFG {
uint16_t mcp230xx_int_timer; // 718
- byte free_71A[174]; // 71A
+ uint8_t rgbwwTable[5]; // 71A
+
+ byte free_71F[169]; // 71F
unsigned long energy_frequency_calibration; // 7C8
diff --git a/sonoff/settings.ino b/sonoff/settings.ino
index 95e6ce367..4116a1c6d 100644
--- a/sonoff/settings.ino
+++ b/sonoff/settings.ino
@@ -624,6 +624,10 @@ void SettingsDefaultSet2()
Settings.button_debounce = KEY_DEBOUNCE_TIME;
Settings.switch_debounce = SWITCH_DEBOUNCE_TIME;
+
+ for (byte j = 0; j < 5; j++) {
+ Settings.rgbwwTable[j] = 255;
+ }
}
/********************************************************************************************/
@@ -827,6 +831,11 @@ void SettingsDelta()
Settings.button_debounce = KEY_DEBOUNCE_TIME;
Settings.switch_debounce = SWITCH_DEBOUNCE_TIME;
}
+ if (Settings.version < 0x0602010A) {
+ for (byte j = 0; j < 5; j++) {
+ Settings.rgbwwTable[j] = 255;
+ }
+ }
Settings.version = VERSION;
SettingsSave(1);
diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino
index 6b354bb24..b89ffe138 100755
--- a/sonoff/sonoff.ino
+++ b/sonoff/sonoff.ino
@@ -296,20 +296,23 @@ char* GetStateText(byte state)
/********************************************************************************************/
-void SetLatchingRelay(power_t power, uint8_t state)
+void SetLatchingRelay(power_t lpower, uint8_t state)
{
- power &= 1;
- if (2 == state) { // Reset relay
- state = 0;
- latching_power = power;
- latching_relay_pulse = 0;
+ // power xx00 - toggle REL1 (Off) and REL3 (Off) - device 1 Off, device 2 Off
+ // power xx01 - toggle REL2 (On) and REL3 (Off) - device 1 On, device 2 Off
+ // power xx10 - toggle REL1 (Off) and REL4 (On) - device 1 Off, device 2 On
+ // power xx11 - toggle REL2 (On) and REL4 (On) - device 1 On, device 2 On
+
+ if (state && !latching_relay_pulse) { // Set latching relay to power if previous pulse has finished
+ latching_power = lpower;
+ latching_relay_pulse = 2; // max 200mS (initiated by stateloop())
}
- else if (state && !latching_relay_pulse) { // Set port power to On
- latching_power = power;
- latching_relay_pulse = 2; // max 200mS (initiated by stateloop())
- }
- if (pin[GPIO_REL1 +latching_power] < 99) {
- digitalWrite(pin[GPIO_REL1 +latching_power], bitRead(rel_inverted, latching_power) ? !state : state);
+
+ for (byte i = 0; i < devices_present; i++) {
+ uint8_t port = (i << 1) + ((latching_power >> i) &1);
+ if (pin[GPIO_REL1 +port] < 99) {
+ digitalWrite(pin[GPIO_REL1 +port], bitRead(rel_inverted, port) ? !state : state);
+ }
}
}
@@ -2453,6 +2456,10 @@ void GpioInit()
if (pin[GPIO_REL1 +i] < 99) {
pinMode(pin[GPIO_REL1 +i], OUTPUT);
devices_present++;
+ if (EXS_RELAY == Settings.module) {
+ digitalWrite(pin[GPIO_REL1 +i], bitRead(rel_inverted, i) ? 1 : 0);
+ if (i &1) { devices_present--; }
+ }
}
}
}
@@ -2493,10 +2500,6 @@ void GpioInit()
}
}
- if (EXS_RELAY == Settings.module) {
- SetLatchingRelay(0,2);
- SetLatchingRelay(1,2);
- }
SetLedPower(Settings.ledstate &8);
XdrvCall(FUNC_PRE_INIT);
diff --git a/sonoff/sonoff_template.h b/sonoff/sonoff_template.h
index 8374fa09d..aad498f3b 100644
--- a/sonoff/sonoff_template.h
+++ b/sonoff/sonoff_template.h
@@ -232,6 +232,9 @@ enum SupportedModules {
SHELLY2,
PHILIPS,
NEO_COOLCAM,
+ ESP_SWITCH,
+ OBI,
+ TECKIN,
MAXMODULE };
/********************************************************************************************/
@@ -355,31 +358,31 @@ const uint8_t kGpioNiceList[GPIO_SENSOR_END] PROGMEM = {
};
const uint8_t kModuleNiceList[MAXMODULE] PROGMEM = {
- SONOFF_BASIC,
+ SONOFF_BASIC, // Sonoff Relay Devices
SONOFF_RF,
SONOFF_TH,
SONOFF_DUAL,
SONOFF_DUAL_R2,
SONOFF_POW,
SONOFF_POW_R2,
- SONOFF_S31,
SONOFF_4CH,
SONOFF_4CHPRO,
- SONOFF_SV,
- SONOFF_DEV,
- SONOFF_S2X,
- SLAMPHER,
- SONOFF_TOUCH,
+ SONOFF_S31, // Sonoff Socket Relay Devices with Energy Monitoring
+ SONOFF_S2X, // Sonoff Socket Relay Devices
+ SONOFF_TOUCH, // Sonoff Switch Devices
SONOFF_T11,
SONOFF_T12,
SONOFF_T13,
- SONOFF_SC,
- SONOFF_B1,
- SONOFF_LED,
+ SONOFF_LED, // Sonoff Light Devices
SONOFF_BN,
- SONOFF_IFAN02,
- SONOFF_BRIDGE,
- CH1,
+ SONOFF_B1, // Sonoff Light Bulbs
+ SLAMPHER,
+ SONOFF_SC, // Sonoff Environmemtal Sensor
+ SONOFF_IFAN02, // Sonoff Fan
+ SONOFF_BRIDGE, // Sonoff Bridge
+ SONOFF_SV, // Sonoff Development Devices
+ SONOFF_DEV,
+ CH1, // Relay Devices
CH4,
MOTOR,
ELECTRODRAGON,
@@ -390,9 +393,12 @@ const uint8_t kModuleNiceList[MAXMODULE] PROGMEM = {
WION,
SHELLY1,
SHELLY2,
- BLITZWOLF_BWSHP2,
- NEO_COOLCAM,
- H801,
+ BLITZWOLF_BWSHP2, // Socket Relay Devices with Energy Monitoring
+ TECKIN,
+ NEO_COOLCAM, // Socket Relay Devices
+ OBI,
+ ESP_SWITCH, // Switch Devices
+ H801, // Light Devices
MAGICHOME,
ARILUX_LC01,
ARILUX_LC06,
@@ -400,9 +406,9 @@ const uint8_t kModuleNiceList[MAXMODULE] PROGMEM = {
ZENGGE_ZF_WF017,
HUAFAN_SS,
KMC_70011,
- AILIGHT,
+ AILIGHT, // Light Bulbs
PHILIPS,
- WITTY,
+ WITTY, // Development Devices
WEMOS
};
@@ -598,21 +604,22 @@ const mytmplt kModules[MAXMODULE] PROGMEM = {
GPIO_LED1, // GPIO16 Green/Blue Led (1 = On, 0 = Off)
GPIO_ADC0 // ADC0 A0 Analog input
},
- { "EXS Relay", // Latching relay (ESP8266)
+ { "EXS Relay(s)", // ES-Store Latching relay(s) (ESP8266)
// https://ex-store.de/ESP8266-WiFi-Relay-V31
- // Module Pin 1 VCC 3V3, Module Pin 6 GND
- GPIO_KEY1, // GPIO00 Module Pin 8 - Button (firmware flash)
- GPIO_USER, // GPIO01 Module Pin 2 = UART0_TXD
- GPIO_USER, // GPIO02 Module Pin 7
- GPIO_USER, // GPIO03 Module Pin 3 = UART0_RXD
- GPIO_USER, // GPIO04 Module Pin 10
- GPIO_USER, // GPIO05 Module Pin 9
+ // V3.1 Module Pin 1 VCC 3V3, Module Pin 6 GND
+ // https://ex-store.de/2-Kanal-WiFi-WLan-Relay-V5-Blackline-fuer-Unterputzmontage
+ GPIO_USER, // GPIO00 V3.1 Module Pin 8 - V5.0 Module Pin 4
+ GPIO_USER, // GPIO01 UART0_TXD V3.1 Module Pin 2 - V5.0 Module Pin 3
+ GPIO_USER, // GPIO02 V3.1 Module Pin 7
+ GPIO_USER, // GPIO03 UART0_RXD V3.1 Module Pin 3
+ GPIO_USER, // GPIO04 V3.1 Module Pin 10 - V5.0 Module Pin 2
+ GPIO_USER, // GPIO05 V3.1 Module Pin 9 - V5.0 Module Pin 1
0, 0, 0, 0, 0, 0, // Flash connection
GPIO_REL1, // GPIO12 Relay1 ( 1 = Off)
GPIO_REL2, // GPIO13 Relay1 ( 1 = On)
- GPIO_USER, // GPIO14 Module Pin 5
- 0,
- GPIO_USER, // GPIO16 Module Pin 4
+ GPIO_USER, // GPIO14 V3.1 Module Pin 5 - V5.0 GPIO_REL3_INV Relay2 ( 1 = Off)
+ GPIO_LED1, // GPIO15 V5.0 LED1
+ GPIO_USER, // GPIO16 V3.1 Module Pin 4 - V5.0 GPIO_REL4_INV Relay2 ( 1 = On)
0
},
{ "WiOn", // Indoor Tap (ESP8266)
@@ -855,19 +862,20 @@ const mytmplt kModules[MAXMODULE] PROGMEM = {
0, 0, 0, 0, 0, 0, // Flash connection
0, 0, 0, 0, 0
},
- { "MagicHome", // Magic Home (aka Flux-light) (ESP8266)
+ { "MagicHome", // Magic Home (aka Flux-light) (ESP8266) and Arilux LC10 (ESP8285)
// https://www.aliexpress.com/item/Magic-Home-Mini-RGB-RGBW-Wifi-Controller-For-Led-Strip-Panel-light-Timing-Function-16million-colors/32686853650.html
0,
GPIO_USER, // GPIO01 Serial RXD and Optional sensor
GPIO_LED1_INV, // GPIO02 Blue onboard LED
GPIO_USER, // GPIO03 Serial TXD and Optional sensor
- GPIO_USER, // GPIO04 IR receiver (optional)
+ GPIO_ARIRFRCV, // GPIO04 IR or RF receiver (optional) (Arilux LC10)
GPIO_PWM2, // GPIO05 RGB LED Green
0, 0, 0, 0, 0, 0, // Flash connection
GPIO_PWM3, // GPIO12 RGB LED Blue
- GPIO_USER, // GPIO13 RGBW LED White (optional - set to PWM4 for Cold White or Warm White)
+ GPIO_USER, // GPIO13 RGBW LED White (optional - set to PWM4 for Cold White or Warm White as used on Arilux LC10)
GPIO_PWM1, // GPIO14 RGB LED Red
- 0, 0, 0
+ GPIO_LED2_INV, // GPIO15 RF receiver control (Arilux LC10)
+ 0, 0
},
{ "Luani HVIO", // ESP8266_HVIO
// https://luani.de/projekte/esp8266-hvio/
@@ -1060,18 +1068,70 @@ const mytmplt kModules[MAXMODULE] PROGMEM = {
{ "Neo Coolcam", // Neo Coolcam (ESP8266)
// https://www.banggood.com/NEO-COOLCAM-WiFi-Mini-Smart-Plug-APP-Remote-Control-Timing-Smart-Socket-EU-Plug-p-1288562.html?cur_warehouse=CN
0, 0, 0, 0,
- GPIO_LED1_INV, // GPIO13 Red Led (0 = On, 1 = Off)
+ GPIO_LED1_INV, // GPIO04 Red Led (0 = On, 1 = Off)
0,
0, 0, 0, 0, 0, 0, // Flash connection
GPIO_REL1, // GPIO12 Red Led and Relay (0 = Off, 1 = On)
GPIO_KEY1, // GPIO13 Button
0, 0, 0, 0
+ },
+ { "ESP Switch", // Michael Haustein 4 channel wall switch (ESP07 = ESP8266)
+ // Use rules for further actions like - rule on power1#state do publish cmnd/other_device/power %value% endon
+ GPIO_KEY2, // GPIO00 Button 2
+ GPIO_USER, // GPIO01 Serial RXD and Optional sensor
+ GPIO_REL3_INV, // GPIO02 Yellow Led 3 (0 = On, 1 = Off)
+ GPIO_USER, // GPIO03 Serial TXD and Optional sensor
+ GPIO_KEY1, // GPIO04 Button 1
+ GPIO_REL2_INV, // GPIO05 Red Led 2 (0 = On, 1 = Off)
+ 0, 0, 0, 0, 0, 0, // Flash connection
+ GPIO_REL4_INV, // GPIO12 Blue Led 4 (0 = On, 1 = Off)
+ GPIO_KEY4, // GPIO13 Button 4
+ GPIO_KEY3, // GPIO14 Button 3
+ GPIO_LED1, // GPIO15 Optional sensor
+ GPIO_REL1_INV, // GPIO16 Green Led 1 (0 = On, 1 = Off)
+ },
+ { "OBI Socket", // OBI socket (ESP8266) - https://www.obi.de/hausfunksteuerung/wifi-stecker-schuko/p/2291706
+ 0, 0, 0, 0,
+ GPIO_LED1, // GPIO04 LED on top and in switch button
+ GPIO_REL1, // GPIO05 Relay 1 (0 = Off, 1 = On)
+ 0, 0, 0, 0, 0, 0, // Flash connection
+ GPIO_LED2, // GPIO12
+ 0, // GPIO13
+ GPIO_KEY1, // GPIO14 switch button
+ 0, 0, 0
+ },
+ { "Teckin", // https://www.amazon.de/gp/product/B07D5V139R
+ 0,
+ GPIO_KEY1, // GPIO01 Serial TXD and Button
+ 0,
+ GPIO_LED2_INV, // GPIO03 Serial RXD and Red Led (0 = On, 1 = Off)
+ GPIO_HLW_CF, // GPIO04 BL0937 or HJL-01 CF power
+ GPIO_HLW_CF1, // GPIO05 BL0937 or HJL-01 CF1 voltage / current
+ 0, 0, 0, 0, 0, 0, // Flash connection
+ GPIO_HLW_SEL, // GPIO12 BL0937 or HJL-01 Sel output
+ GPIO_LED1_INV, // GPIO13 Blue Led (0 = On, 1 = Off)
+ GPIO_REL1, // GPIO14 Relay (0 = Off, 1 = On)
+ 0, 0, 0
}
};
/*
Optionals
+ { "MagicHome", // Magic Home (aka Flux-light) (ESP8266)
+ // https://www.aliexpress.com/item/Magic-Home-Mini-RGB-RGBW-Wifi-Controller-For-Led-Strip-Panel-light-Timing-Function-16million-colors/32686853650.html
+ 0,
+ GPIO_USER, // GPIO01 Serial RXD and Optional sensor
+ GPIO_LED1_INV, // GPIO02 Blue onboard LED
+ GPIO_USER, // GPIO03 Serial TXD and Optional sensor
+ GPIO_USER, // GPIO04 IR receiver (optional)
+ GPIO_PWM2, // GPIO05 RGB LED Green
+ 0, 0, 0, 0, 0, 0, // Flash connection
+ GPIO_PWM3, // GPIO12 RGB LED Blue
+ GPIO_USER, // GPIO13 RGBW LED White (optional - set to PWM4 for Cold White or Warm White)
+ GPIO_PWM1, // GPIO14 RGB LED Red
+ 0, 0, 0
+ },
{ "Arilux LC10", // Arilux LC10 (ESP8285), RGBW + RF
// https://github.com/arendst/Sonoff-Tasmota/wiki/MagicHome-with-ESP8285
// https://www.aliexpress.com/item/DC5-24V-Wireless-WIFI-LED-RGB-Controller-RGBW-Controller-IR-RF-Remote-Control-IOS-Android-for/32827253255.html
diff --git a/sonoff/sonoff_version.h b/sonoff/sonoff_version.h
index 4f2b5c0ec..c5765ec38 100644
--- a/sonoff/sonoff_version.h
+++ b/sonoff/sonoff_version.h
@@ -20,7 +20,7 @@
#ifndef _SONOFF_VERSION_H_
#define _SONOFF_VERSION_H_
-#define VERSION 0x06020109
+#define VERSION 0x0602010A
#define D_PROGRAMNAME "Sonoff-Tasmota"
#define D_AUTHOR "Theo Arends"
diff --git a/sonoff/support.ino b/sonoff/support.ino
index fc73e726e..3665650dd 100644
--- a/sonoff/support.ino
+++ b/sonoff/support.ino
@@ -1775,27 +1775,35 @@ int8_t I2cWriteBuffer(uint8_t addr, uint8_t reg, uint8_t *reg_data, uint16_t len
void I2cScan(char *devs, unsigned int devs_len)
{
- byte error;
- byte address;
+ // Return error codes defined in twi.h and core_esp8266_si2c.c
+ // I2C_OK 0
+ // I2C_SCL_HELD_LOW 1 = SCL held low by another device, no procedure available to recover
+ // I2C_SCL_HELD_LOW_AFTER_READ 2 = I2C bus error. SCL held low beyond slave clock stretch time
+ // I2C_SDA_HELD_LOW 3 = I2C bus error. SDA line held low by slave/another_master after n bits
+ // I2C_SDA_HELD_LOW_AFTER_INIT 4 = line busy. SDA again held low by another device. 2nd master?
+
+ byte error = 0;
+ byte address = 0;
byte any = 0;
- char tstr[10];
snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_DEVICES_FOUND_AT));
for (address = 1; address <= 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (0 == error) {
- snprintf_P(tstr, sizeof(tstr), PSTR(" 0x%2x"), address);
- strncat(devs, tstr, devs_len);
any = 1;
+ snprintf_P(devs, devs_len, PSTR("%s 0x%02x"), devs, address);
}
- else if (4 == error) {
- snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_UNKNOWN_ERROR_AT " 0x%2x\"}"), address);
+ else if (error != 2) { // Seems to happen anyway using this scan
+ any = 2;
+ snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"Error %d at 0x%02x"), error, address);
+ break;
}
}
if (any) {
strncat(devs, "\"}", devs_len);
- } else {
+ }
+ else {
snprintf_P(devs, devs_len, PSTR("{\"" D_CMND_I2CSCAN "\":\"" D_JSON_I2CSCAN_NO_DEVICES_FOUND "\"}"));
}
}
diff --git a/sonoff/user_config.h b/sonoff/user_config.h
index eceadc68d..72aa97d53 100644
--- a/sonoff/user_config.h
+++ b/sonoff/user_config.h
@@ -302,6 +302,7 @@
// #define USE_MCP230xx_DISPLAYOUTPUT // Enable MCP23008/MCP23017 to display state of OUTPUT pins on Web UI (+0k2 code)
// #define USE_PCA9685 // Enable PCA9685 I2C HW PWM Driver - Must define I2C Address in #define USE_PCA9685_ADDR below - range 0x40 - 0x47 (+1k4 code)
// #define USE_PCA9685_ADDR 0x40 // Enable PCA9685 I2C Address to use (Must be within range 0x40 through 0x47 - set according to your wired setup)
+// #define USE_PCA9685_FREQ 50 // Define default PWM frequency in Hz to be used (must be within 24 to 1526) - If other value is used, it will rever to 50Hz
// #define USE_MPR121 // Enable MPR121 controller (I2C addresses 0x5A, 0x5B, 0x5C and 0x5D) in input mode for touch buttons (+1k3 code)
// #define USE_CCS811 // Enable CCS811 sensor (I2C address 0x5A) (+2k2 code)
// #define USE_MPU6050 // Enable MPU6050 sensor (I2C address 0x68 AD0 low or 0x69 AD0 high) (+2k6 code)
diff --git a/sonoff/xdrv_02_webserver.ino b/sonoff/xdrv_02_webserver.ino
index 5182b7051..6a939dc24 100644
--- a/sonoff/xdrv_02_webserver.ino
+++ b/sonoff/xdrv_02_webserver.ino
@@ -1013,6 +1013,10 @@ void HandleBackupConfiguration()
WebServer->sendHeader(F("Content-Disposition"), attachment);
WebServer->send(200, FPSTR(HDR_CTYPE_STREAM), "");
+
+ uint16_t cfg_crc = Settings.cfg_crc;
+ Settings.cfg_crc = GetSettingsCrc(); // Calculate crc (again) as it might be wrong when savedata = 0 (#3918)
+
memcpy(settings_buffer, &Settings, sizeof(Settings));
if (config_xor_on_set) {
for (uint16_t i = 2; i < sizeof(Settings); i++) {
@@ -1030,6 +1034,8 @@ void HandleBackupConfiguration()
#endif
SettingsBufferFree();
+
+ Settings.cfg_crc = cfg_crc; // Restore crc in case savedata = 0 to make sure settings will be noted as changed
}
void HandleSaveSettings()
diff --git a/sonoff/xdrv_04_light.ino b/sonoff/xdrv_04_light.ino
index 2a5a15b0c..6c8aa938d 100644
--- a/sonoff/xdrv_04_light.ino
+++ b/sonoff/xdrv_04_light.ino
@@ -55,11 +55,11 @@
enum LightCommands {
CMND_COLOR, CMND_COLORTEMPERATURE, CMND_DIMMER, CMND_LED, CMND_LEDTABLE, CMND_FADE,
- CMND_PIXELS, CMND_ROTATION, CMND_SCHEME, CMND_SPEED, CMND_WAKEUP, CMND_WAKEUPDURATION,
+ CMND_PIXELS, CMND_RGBWWTABLE, CMND_ROTATION, CMND_SCHEME, CMND_SPEED, CMND_WAKEUP, CMND_WAKEUPDURATION,
CMND_WIDTH, CMND_CHANNEL, CMND_HSBCOLOR, CMND_UNDOCA };
const char kLightCommands[] PROGMEM =
D_CMND_COLOR "|" D_CMND_COLORTEMPERATURE "|" D_CMND_DIMMER "|" D_CMND_LED "|" D_CMND_LEDTABLE "|" D_CMND_FADE "|"
- D_CMND_PIXELS "|" D_CMND_ROTATION "|" D_CMND_SCHEME "|" D_CMND_SPEED "|" D_CMND_WAKEUP "|" D_CMND_WAKEUPDURATION "|"
+ D_CMND_PIXELS "|" D_CMND_RGBWWTABLE "|" D_CMND_ROTATION "|" D_CMND_SCHEME "|" D_CMND_SPEED "|" D_CMND_WAKEUP "|" D_CMND_WAKEUPDURATION "|"
D_CMND_WIDTH "|" D_CMND_CHANNEL "|" D_CMND_HSBCOLOR "|UNDOCA" ;
struct LRgbColor {
@@ -799,7 +799,8 @@ void LightAnimate()
light_update = 0;
for (byte i = 0; i < light_subtype; i++) {
light_last_color[i] = light_new_color[i];
- cur_col[i] = (Settings.light_correction) ? ledTable[light_last_color[i]] : light_last_color[i];
+ cur_col[i] = light_last_color[i]*Settings.rgbwwTable[i]/255;
+ cur_col[i] = (Settings.light_correction) ? ledTable[cur_col[i]] : cur_col[i];
if (light_type < LT_PWM6) {
if (pin[GPIO_PWM1 +i] < 99) {
if (cur_col[i] > 0xFC) {
@@ -1279,6 +1280,33 @@ boolean LightCommand()
}
snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_SVALUE, command, GetStateText(Settings.light_correction));
}
+ else if (CMND_RGBWWTABLE == command_code) {
+ bool validtable = (XdrvMailbox.data_len > 0);
+ char scolor[25];
+ if (validtable) {
+ uint16_t HSB[3];
+ if (strstr(XdrvMailbox.data, ",")) { // Command with up to 5 comma separated parameters
+ for (int i = 0; i < LST_RGBWC; i++) {
+ char *substr;
+
+ if (0 == i) {
+ substr = strtok(XdrvMailbox.data, ",");
+ } else {
+ substr = strtok(NULL, ",");
+ }
+ if (substr != NULL) {
+ Settings.rgbwwTable[i] = atoi(substr);
+ }
+ }
+ }
+ light_update = 1;
+ }
+ scolor[0] = '\0';
+ for (byte i = 0; i < LST_RGBWC; i++) {
+ snprintf_P(scolor, 25, PSTR("%s%s%d"), scolor, (i > 0) ? "," : "", Settings.rgbwwTable[i]);
+ }
+ snprintf_P(mqtt_data, sizeof(mqtt_data), S_JSON_COMMAND_INDEX_SVALUE, command, XdrvMailbox.index, scolor);
+ }
else if (CMND_FADE == command_code) {
switch (XdrvMailbox.payload) {
case 0: // Off
diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino
index 66bf93614..124b41534 100644
--- a/sonoff/xdrv_09_timers.ino
+++ b/sonoff/xdrv_09_timers.ino
@@ -194,7 +194,7 @@ void ApplyTimerOffsets(Timer *duskdawn)
// apply offsets, check for over- and underflows
uint16_t timeBuffer;
- if ((uint16_t)stored.time > 720) {
+ if ((uint16_t)stored.time > 719) {
// negative offset, time after 12:00
timeBuffer = (uint16_t)stored.time - 720;
// check for underflow
diff --git a/sonoff/xdrv_12_home_assistant.ino b/sonoff/xdrv_12_home_assistant.ino
index c4e5d833b..3c38cbc46 100644
--- a/sonoff/xdrv_12_home_assistant.ino
+++ b/sonoff/xdrv_12_home_assistant.ino
@@ -50,7 +50,7 @@ const char HASS_DISCOVER_LIGHT_DIMMER[] PROGMEM =
"\"brightness_value_template\":\"{{value_json." D_CMND_DIMMER "}}\"";
const char HASS_DISCOVER_LIGHT_COLOR[] PROGMEM =
- "%s,\"rgb_command_topic\":\"%s\"," // cmnd/led2/Color
+ "%s,\"rgb_command_topic\":\"%s2\"," // cmnd/led2/Color2
"\"rgb_state_topic\":\"%s\"," // stat/led2/RESULT
"\"rgb_value_template\":\"{{value_json." D_CMND_COLOR "}}\"";
// "\"rgb_value_template\":\"{{value_json." D_CMND_COLOR " | join(',')}}\"";
diff --git a/sonoff/xdrv_15_pca9685.ino b/sonoff/xdrv_15_pca9685.ino
index e53b8d17f..9cb9292a0 100644
--- a/sonoff/xdrv_15_pca9685.ino
+++ b/sonoff/xdrv_15_pca9685.ino
@@ -26,8 +26,13 @@
#define PCA9685_REG_LED0_ON_L 0x06
#define PCA9685_REG_PRE_SCALE 0xFE
+#ifndef USE_PCA9685_FREQ
+ #define USE_PCA9685_FREQ 50
+#endif
+
uint8_t pca9685_detected = 0;
-uint16_t pca9685_freq = 50;
+uint16_t pca9685_freq = USE_PCA9685_FREQ;
+uint16_t pca9685_pin_pwm_value[16];
void PCA9685_Detect(void)
{
@@ -51,9 +56,10 @@ void PCA9685_Detect(void)
void PCA9685_Reset(void)
{
I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_MODE1, 0x80);
- PCA9685_SetPWMfreq(50);
+ PCA9685_SetPWMfreq(USE_PCA9685_FREQ);
for (uint8_t pin=0;pin<16;pin++) {
- PCA9685_SetPWM(pin,0,false);
+ PCA9685_SetPWM(pin,0,false);
+ pca9685_pin_pwm_value[pin] = 0;
}
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"RESET\":\"OK\"}}"));
}
@@ -63,9 +69,13 @@ void PCA9685_SetPWMfreq(double freq) {
7.3.5 from datasheet
prescale value = round(25000000/(4096*freq))-1;
*/
- pca9685_freq=freq;
- uint8_t pre_scale_osc = round(25000000/(4096*freq))-1;
- if (1526 == freq) pre_scale_osc=0xFF; // force setting for 24hz because rounding causes 1526 to be 254
+ if (freq > 23 && freq < 1527) {
+ pca9685_freq=freq;
+ } else {
+ pca9685_freq=50;
+ }
+ uint8_t pre_scale_osc = round(25000000/(4096*pca9685_freq))-1;
+ if (1526 == pca9685_freq) pre_scale_osc=0xFF; // force setting for 24hz because rounding causes 1526 to be 254
uint8_t current_mode1 = I2cRead8(USE_PCA9685_ADDR, PCA9685_REG_MODE1); // read current value of MODE1 register
uint8_t sleep_mode1 = (current_mode1&0x7F) | 0x10; // Determine register value to put PCA to sleep
I2cWrite8(USE_PCA9685_ADDR, PCA9685_REG_MODE1, sleep_mode1); // Let's sleep a little
@@ -107,6 +117,7 @@ bool PCA9685_Command(void)
if (',' == XdrvMailbox.data[ca]) { paramcount++; }
}
UpperCase(XdrvMailbox.data,XdrvMailbox.data);
+
if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 1),"RESET")) { PCA9685_Reset(); return serviced; }
if (!strcmp(subStr(sub_string, XdrvMailbox.data, ",", 1),"PWMF")) {
@@ -114,7 +125,7 @@ bool PCA9685_Command(void)
uint16_t new_freq = atoi(subStr(sub_string, XdrvMailbox.data, ",", 2));
if ((new_freq >= 24) && (new_freq <= 1526)) {
PCA9685_SetPWMfreq(new_freq);
- snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PWMF\":%i, \"Result\":\"OK\"}}"));
+ snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"PCA9685\":{\"PWMF\":%i, \"Result\":\"OK\"}}"),new_freq);
return serviced;
}
} else { // No parameter was given for setfreq, so we return current setting
@@ -151,28 +162,34 @@ bool PCA9685_Command(void)
return serviced;
}
+void PCA9685_OutputTelemetry(void) {
+ if (0 == pca9685_detected) { return; } // We do not do this if the PCA9685 has not been detected
+ snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_JSON_TIME "\":\"%s\",\"PCA9685\": {"), GetDateAndTime(DT_LOCAL).c_str());
+ snprintf_P(mqtt_data,sizeof(mqtt_data), PSTR("%s\"PWM_FREQ\":%i,"),mqtt_data,pca9685_freq);
+ for (uint8_t pin=0;pin<16;pin++) {
+ snprintf_P(mqtt_data,sizeof(mqtt_data), PSTR("%s\"PWM%i\":%i,"),mqtt_data,pin,pca9685_pin_pwm_value[pin]);
+ }
+ snprintf_P(mqtt_data,sizeof(mqtt_data),PSTR("%s\"END\":1}}"),mqtt_data);
+ MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain);
+}
+
boolean Xdrv15(byte function)
{
boolean result = false;
if (i2c_flg) {
switch (function) {
- case FUNC_MQTT_DATA:
- break;
case FUNC_EVERY_SECOND:
PCA9685_Detect();
- break;
- case FUNC_EVERY_50_MSECOND:
- break;
- case FUNC_JSON_APPEND:
+ if (tele_period == 0) {
+ PCA9685_OutputTelemetry();
+ }
break;
case FUNC_COMMAND:
if (XDRV_15 == XdrvMailbox.index) {
PCA9685_Command();
}
break;
- case FUNC_WEB_APPEND:
- break;
default:
break;
}
diff --git a/tools/decode-config.py b/tools/decode-config.py
index dc51e0f42..284904fb9 100644
--- a/tools/decode-config.py
+++ b/tools/decode-config.py
@@ -19,23 +19,25 @@
You should have received a copy of the GNU General Public License
along with this program. If not, see .
+
Requirements:
- Python
- pip install json pycurl urllib2 configargparse
+
Instructions:
- Execute command with option -d to retrieve config data from device or
- use -f to read out a previously saved configuration file.
+ Execute command with option -d to retrieve config data from a host
+ or use -f to read out a configuration file saved using Tasmota Web-UI
For help execute command with argument -h
Usage:
decode-config.py [-h] [-f ] [-d ] [-u ]
- [-p ] [--format ]
- [--json-indent ] [--json-compact]
- [--sort ] [--raw] [--unhide-pw] [-o ]
- [-c ] [-V]
+ [-p ] [--json-indent ]
+ [--json-compact] [--unsort] [--raw] [--unhide-pw]
+ [-o ] [--output-file-format ]
+ [-c ] [--exit-on-error-only] [-V]
Decode configuration of Sonoff-Tasmota device. Args that start with '--' (eg.
-f) can also be set in a config file (specified via -c). Config file syntax
@@ -48,89 +50,84 @@ Usage:
-c , --config
Config file, can be used instead of command parameter
(default: None)
+ --exit-on-error-only exit on error only (default: exit on ERROR and
+ WARNING). Not recommended, used by your own
+ responsibility!
source:
-f , --file
file to retrieve Tasmota configuration from (default:
- None)
+ None)'
-d , --device
hostname or IP address to retrieve Tasmota
configuration from (default: None)
-u , --username
- host http access username (default: admin)
+ host HTTP access username (default: admin)
-p , --password
- host http access password (default: None)
+ host HTTP access password (default: None)
output:
- --format output format ("json" or "text", default: "json")
--json-indent
pretty-printed JSON output using indent level
- (default: "None")
+ (default: 'None')
--json-compact compact JSON output by eliminate whitespace (default:
- "not compact")
- --sort sort result - can be "none" or "name" (default:
- "name")
- --raw output raw values (default: processed)
+ normal)
+ --unsort do not sort results (default: sort)
+ --raw output raw values (default: process)
--unhide-pw unhide passwords (default: hide)
-o , --output-file
- file to store decrypted raw binary configuration to
- (default: None)
+ file to store configuration to (default: None) Macros:
+ @v=Tasmota version, @f=friendly name
+ --output-file-format
+ output format ('json' or 'binary', default: 'json')
info:
-V, --version show program's version number and exit
Either argument -d or -f must be given.
-
-Examples:
- Read configuration from hostname 'sonoff1' and output default json config
- ./decode-config.py -d sonoff1
-
- Read configuration from file 'Config__6.2.1.dmp' and output default json config
- ./decode-config.py -f Config__6.2.1.dmp
-
- Read configuration from hostname 'sonoff1' using web login data
- ./decode-config.py -d sonoff1 -u admin -p xxxx
-
- Read configuration from hostname 'sonoff1' using web login data and unhide passwords
- ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw
-
- Read configuration from hostname 'sonoff1' using web login data, unhide passwords
- and sort key names
- ./decode-config.py -d sonoff1 -u admin -p xxxx --unhide-pw --sort name
"""
import os.path
import io
import sys
-import configargparse
-import collections
import struct
import re
-import json
+import math
+from datetime import datetime
+try:
+ import json
+except ImportError:
+ print("module not found. Try 'pip install json' to install it")
+ sys.exit(9)
+try:
+ import configargparse
+except ImportError:
+ print("module not found. Try 'pip install configargparse' to install it")
+ sys.exit(9)
try:
import pycurl
except ImportError:
- print("module not found. Try 'pip pycurl' to install it")
+ print("module not found. Try 'pip install pycurl' to install it")
sys.exit(9)
try:
import urllib2
except ImportError:
- print("module not found. Try 'pip urllib2' to install it")
+ print("module not found. Try 'pip install urllib2' to install it")
sys.exit(9)
-VER = '1.5.0009'
+VER = '1.5.0011'
PROG='{} v{} by Norbert Richter'.format(os.path.basename(sys.argv[0]),VER)
CONFIG_FILE_XOR = 0x5A
-
args = {}
DEFAULTS = {
'DEFAULT':
{
'configfile': None,
+ 'exitonwarning':True,
},
'source':
{
@@ -141,15 +138,16 @@ DEFAULTS = {
},
'output':
{
- 'format': 'json',
'jsonindent': None,
'jsoncompact': False,
- 'sort': 'name',
+ 'unsort': False,
'raw': False,
'unhide-pw': False,
'outputfile': None,
+ 'outputfileformat': 'json',
},
}
+exitcode = 0
"""
@@ -163,11 +161,35 @@ Settings dictionary describes the config file fields definition:
format
Define the data interpretation.
- For details see struct module format string
- https://docs.python.org/2.7/library/struct.html#format-strings
+ It is either a string or a tuple containing a string and a
+ sub-Settings dictionary.
+ 'xxx':
+ A string is used to interpret the data at
+ The string defines the format interpretion as described
+ in 'struct module format string', see
+ https://docs.python.org/2.7/library/struct.html#format-strings
+ In addition to this format string there is as special
+ meaning of a dot '.' - this means a bit with an optional
+ prefix length. If no prefix is given, 1 is assumed.
+ {}:
+ A dictionary describes itself a 'Settings' dictonary (recursive)
baseaddr
- The address (starting from 0) within config data
+ The address (starting from 0) within config data.
+ For bit fields must be a tuple.
+ n:
+ Defines a simple address within config data.
+ must be a positive integer.
+ (n, b, s):
+ A tuple defines a bit field:
+
+ is the address within config data (integer)
+
+ how many bits are used (positive integer)
+
+ bit shift (integer)
+ positive shift the result right bits
+ negative shift the result left bits
datadef
Define the field interpretation different from simple
@@ -182,91 +204,74 @@ Settings dictionary describes the config file fields definition:
Defines a one-dimensional array of size
[n, n <,n...>]
Defines a multi-dimensional array
- [{} <,{}...]
- Defines a bit struct. The items are simply dict
- {'bitname', bitlen}, the dict order is important.
convert (optional)
Define an output/conversion methode, can be a simple string
or a previously defined function name.
- 'xxx':
- a string defines a format specification of the string
- formatter, see
- https://docs.python.org/2.7/library/string.html#format-string-syntax
+ 'xxx?':
+ a string will be evaluate as is replacing all '?' chars
+ with the current value. This can also be contain pyhton
+ code.
func:
a function defines the name of a formating function
"""
# config data conversion function and helper
-def baudrate(value):
- return value * 1200
-
def int2ip(value):
return '{:d}.{:d}.{:d}.{:d}'.format(value & 0xff, value>>8 & 0xff, value>>16 & 0xff, value>>24 & 0xff)
-def int2geo(value):
- return float(value) / 1000000
-
def password(value):
if args.unhidepw:
return value
return '********'
-def fingerprintstr(value):
- s = list(value)
- result = ''
- for c in s:
- if c in '0123456789abcdefABCDEF':
- result += c
- return result
-
-
Setting_6_2_1 = {
'cfg_holder': (' from fielddef[0]
"""
+ return fielddef[0]
- length=0
- if fielddef[2] is not None:
- # fielddef[2] contains a array or int
- # calc size recursive by sum of all elements
+def GetFieldBaseAddr(fielddef):
+ """
+ Return the format item of field definition
- # tuple 2 contains a list with integer or an integer value
- if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int):
- for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ):
- # multidimensional array
- if isinstance(fielddef[2], list) and len(fielddef[2])>1:
- length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) )
- else:
- length += GetFieldLength( (fielddef[0], fielddef[1], None) )
- else:
- if fielddef[0][-1:].lower() in ['b','c','?']:
- length=1
- elif fielddef[0][-1:].lower() in ['h']:
- length=2
- elif fielddef[0][-1:].lower() in ['i','l','f']:
- length=4
- elif fielddef[0][-1:].lower() in ['q','d']:
- length=8
- elif fielddef[0][-1:].lower() in ['s','p']:
- # s and p needs prefix as length
- match = re.search("\s*(\d+)", fielddef[0])
- if match:
- length=int(match.group(0))
+ @param fielddef:
+ field format - see "Settings dictionary" above
+
+ @return: ,, from fielddef[1]
+
+ """
+ baseaddr = fielddef[1]
+ if isinstance(baseaddr, tuple):
+ return baseaddr[0], baseaddr[1], baseaddr[2]
+
+ return baseaddr, 0, 0
+
+
+def MakeFieldBaseAddr(baseaddr, bitlen, bitshift):
+ """
+ Return a based on given arguments
+
+ @param baseaddr:
+ baseaddr from Settings definition
+ @param bitlen:
+ 0 or bitlen
+ @param bitshift:
+ 0 or bitshift
+
+ @return: (,,) if bitlen != 0
+ baseaddr if bitlen == 0
+
+ """
+ if bitlen!=0:
+ return (baseaddr, bitlen, bitshift)
+ return baseaddr
- # it's a single value
- return length
def ConvertFieldValue(value, fielddef, raw=False):
"""
@@ -1682,21 +1850,94 @@ def ConvertFieldValue(value, fielddef, raw=False):
@param value:
original value read from binary data
@param fielddef
- field definition (contains possible conversion defiinition)
+ field definition - see "Settings dictionary" above
@param raw
return raw values (True) or converted values (False)
@return: (un)converted value
"""
if not raw and len(fielddef)>3:
- if isinstance(fielddef[3],str): # use a format string
- return fielddef[3].format(value)
- elif callable(fielddef[3]): # use a format function
- return fielddef[3](value)
+ convert = fielddef[3]
+ if isinstance(convert,str): # evaluate strings
+ try:
+ return eval(convert.replace('?','value'))
+ except:
+ return value
+ elif callable(convert): # use as format function
+ return convert(value)
return value
-def GetField(dobj, fieldname, fielddef, raw=False):
+def GetFieldLength(fielddef):
+ """
+ Return length of a field in bytes based on field format definition
+
+ @param fielddef:
+ field format - see "Settings dictionary" above
+
+ @return: length of field in bytes
+
+ """
+
+ length=0
+ format_ = GetFieldFormat(fielddef)
+
+ # get datadef from field definition
+ datadef = None
+ if len(fielddef)>2:
+ datadef = fielddef[2]
+
+ if datadef is not None:
+ # fielddef[2] contains a array or int
+ # calc size recursive by sum of all elements
+
+ # contains a integer list or an single integer value
+ if (isinstance(datadef, list) \
+ and len(datadef)>0 \
+ and isinstance(datadef[0], int)) \
+ or isinstance(datadef, int):
+
+ for i in range(0, datadef[0] if isinstance(datadef, list) else datadef ):
+
+ # multidimensional array
+ if isinstance(datadef, list) and len(datadef)>1:
+ length += GetFieldLength( (fielddef[0], fielddef[1], fielddef[2][1:]) )
+
+ # single array
+ else:
+ length += GetFieldLength( (fielddef[0], fielddef[1], None) )
+
+ else:
+ if isinstance(fielddef[0], dict):
+ # -> iterate through format_
+ addr = -1
+ setting = fielddef[0]
+ for name in setting:
+ baseaddr, bitlen, bitshift = GetFieldBaseAddr(setting[name])
+ len_ = GetFieldLength(setting[name])
+ if addr != baseaddr:
+ addr = baseaddr
+ length += len_
+
+ else:
+ if format_[-1:].lower() in ['b','c','?']:
+ length=1
+ elif format_[-1:].lower() in ['h']:
+ length=2
+ elif format_[-1:].lower() in ['i','l','f']:
+ length=4
+ elif format_[-1:].lower() in ['q','d']:
+ length=8
+ elif format_[-1:].lower() in ['s','p']:
+ # s and p may have a prefix as length
+ match = re.search("\s*(\d+)", format_)
+ if match:
+ length=int(match.group(0))
+
+ return length
+
+
+def GetField(dobj, fieldname, fielddef, raw=False, addroffset=0):
"""
Get field value from definition
@@ -1714,45 +1955,78 @@ def GetField(dobj, fieldname, fielddef, raw=False):
result = None
- if fielddef[2] is not None:
+ # get format from field definition
+ format_ = GetFieldFormat(fielddef)
+
+ # get baseaddr from field definition
+ baseaddr, bitlen, bitshift = GetFieldBaseAddr(fielddef)
+
+ # get datadef from field definition
+ datadef = None
+ if len(fielddef)>2:
+ datadef = fielddef[2]
+
+ if datadef is not None:
result = []
- # tuple 2 contains a list with integer or an integer value
- if (isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], int)) or isinstance(fielddef[2], int):
- addr = fielddef[1]
- for i in range(0, fielddef[2][0] if isinstance(fielddef[2], list) else fielddef[2] ):
+ # contains a integer list or an single integer value
+ if (isinstance(datadef, list) \
+ and len(datadef)>0 \
+ and isinstance(datadef[0], int)) \
+ or isinstance(datadef, int):
+
+ offset = 0
+ for i in range(0, datadef[0] if isinstance(datadef, list) else datadef):
+
# multidimensional array
- if isinstance(fielddef[2], list) and len(fielddef[2])>1:
- subfielddef = (fielddef[0], addr, fielddef[2][1:], None if len(fielddef)<4 else fielddef[3])
- else: # single array
- subfielddef = (fielddef[0], addr, None, None if len(fielddef)<4 else fielddef[3])
+ if isinstance(datadef, list) and len(datadef)>1:
+ if len(fielddef)<4:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:])
+ else:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), datadef[1:], fielddef[3])
+
+ # single array
+ else:
+ if len(fielddef)<4:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None)
+ else:
+ subfielddef = (fielddef[0], MakeFieldBaseAddr(baseaddr, bitlen, bitshift), None, fielddef[3])
+
length = GetFieldLength(subfielddef)
if length != 0:
- result.append(GetField(dobj, fieldname, subfielddef, raw))
- addr += length
- # tuple 2 contains a list with dict
- elif isinstance(fielddef[2], list) and len(fielddef[2])>0 and isinstance(fielddef[2][0], dict):
- d = {}
- value = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0]
- d['base'] = ConvertFieldValue(value, fielddef, raw);
- union = fielddef[2]
- i = 0
- for l in union:
- for name,bits in l.items():
- bitval = (value & ( ((1<> i
- d[name] = bitval
- i += bits
- result = d
+ result.append(GetField(dobj, fieldname, subfielddef, raw=raw, addroffset=addroffset+offset))
+ offset += length
+
else:
- # it's a single value
- if GetFieldLength(fielddef) != 0:
- result = struct.unpack_from(fielddef[0], dobj, fielddef[1])[0]
- if fielddef[0][-1:].lower() in ['s','p']:
- if ord(result[:1])==0x00 or ord(result[:1])==0xff:
- result = ''
- s = str(result).split('\0')[0]
- result = unicode(s, errors='replace')
- result = ConvertFieldValue(result, fielddef, raw)
+ # contains a dict
+ if isinstance(fielddef[0], dict):
+ # -> iterate through format_
+ setting = fielddef[0]
+ config = {}
+ for name in setting:
+ config[name] = GetField(dobj, name, setting[name], raw=args.raw, addroffset=addroffset)
+ result = config
+ else:
+ # a simple value
+ if GetFieldLength(fielddef) != 0:
+ result = struct.unpack_from(format_, dobj, baseaddr+addroffset)[0]
+
+ if not format_[-1:].lower() in ['s','p']:
+ if bitshift>=0:
+ result >>= bitshift
+ else:
+ result <<= abs(bitshift)
+ if bitlen>0:
+ result &= (1< 127
+ result = unicode(s, errors='ignore')
+
+ result = ConvertFieldValue(result, fielddef, raw)
return result
@@ -1779,6 +2053,8 @@ def Decode(obj):
@param obj:
binary config data (decrypted)
+
+ @return: configuration dictionary
"""
# get header data
version = GetField(obj, 'version', Setting_6_2_1['version'], raw=True)
@@ -1792,16 +2068,20 @@ def Decode(obj):
# if we did not found a mathching setting
if template is None:
- exit(2, "Can't handle Tasmota configuration data for version 0x{:x}".format(version) )
-
+ exit(2, "Tasmota configuration version 0x{:x} not supported".format(version) )
+
setting = template[2]
# check size if exists
if 'cfg_size' in setting:
cfg_size = GetField(obj, 'cfg_size', setting['cfg_size'], raw=True)
- # if we did not found a mathching setting
- if cfg_size != template[1]:
- exit(2, "Data size does not match. Expected {} bytes, read {} bytes.".format(template[1], cfg_size) )
+ # read size should be same as definied in template
+ if cfg_size > template[1]:
+ # may be processed
+ exit(3, "Number of bytes read does ot match - read {}, expected {} byte".format(cfg_size, template[1]), typ='WARNING', doexit=args.exitonwarning)
+ elif cfg_size < template[1]:
+ # less number of bytes can not be processed
+ exit(3, "Number of bytes read to small to process - read {}, expected {} byte".format(cfg_size, template[1]), typ='ERROR')
# check crc if exists
if 'cfg_crc' in setting:
@@ -1809,25 +2089,34 @@ def Decode(obj):
else:
cfg_crc = GetSettingsCrc(obj)
if cfg_crc != GetSettingsCrc(obj):
- exit(3, 'Data crc error' )
+ exit(4, 'Data CRC error, read 0x{:x} should be 0x{:x}'.format(cfg_crc, GetSettingsCrc(obj)), typ='WARNING', doexit=args.exitonwarning)
config = {}
- config['version_template'] = '0x{:x}'.format(template[0])
for name in setting:
- config[name] = GetField(obj, name, setting[name], args.raw)
+ config[name] = GetField(obj, name, setting[name], raw=args.raw)
- if args.sort == 'name':
- config = collections.OrderedDict(sorted(config.items()))
-
- if args.format == 'json':
- print json.dumps(config, sort_keys=args.sort=='name', indent=args.jsonindent, separators=(',', ':') if args.jsoncompact else (', ', ': ') )
- else:
- for key,value in config.items():
- print '{} = {}'.format(key, repr(value))
+ # add header info
+ timestamp = datetime.now()
+ config['header'] = { 'timestamp': timestamp.strftime("%Y-%m-%d %H:%M:%S"),
+ 'data': {
+ 'crc': hex(GetSettingsCrc(obj)),
+ 'size': len(obj),
+ 'template_version': hex(template[0]),
+ 'content': {
+ 'crc': hex(cfg_crc),
+ 'size': cfg_size,
+ 'version': hex(version),
+ },
+ },
+ 'scriptname': os.path.basename(__file__),
+ 'scriptversion': VER,
+ }
+ return config
if __name__ == "__main__":
+ # program argument processing
parser = configargparse.ArgumentParser(description='Decode configuration of Sonoff-Tasmota device.',
epilog='Either argument -d or -f must be given.')
@@ -1836,80 +2125,89 @@ if __name__ == "__main__":
metavar='',
dest='tasmotafile',
default=DEFAULTS['source']['tasmotafile'],
- help='file to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['tasmotafile']))
+ help="file to retrieve Tasmota configuration from (default: {})'".format(DEFAULTS['source']['tasmotafile']))
source.add_argument('-d', '--device',
metavar='',
dest='device',
default=DEFAULTS['source']['device'],
- help='hostname or IP address to retrieve Tasmota configuration from (default: {})'.format(DEFAULTS['source']['device']) )
+ help="hostname or IP address to retrieve Tasmota configuration from (default: {})".format(DEFAULTS['source']['device']) )
source.add_argument('-u', '--username',
metavar='',
dest='username',
default=DEFAULTS['source']['username'],
- help='host http access username (default: {})'.format(DEFAULTS['source']['username']))
+ help="host HTTP access username (default: {})".format(DEFAULTS['source']['username']))
source.add_argument('-p', '--password',
metavar='',
dest='password',
default=DEFAULTS['source']['password'],
- help='host http access password (default: {})'.format(DEFAULTS['source']['password']))
+ help="host HTTP access password (default: {})".format(DEFAULTS['source']['password']))
output = parser.add_argument_group('output')
- output.add_argument('--format',
- metavar='',
- dest='format',
- choices=['json', 'text'],
- default=DEFAULTS['output']['format'],
- help='output format ("json" or "text", default: "{}")'.format(DEFAULTS['output']['format']) )
output.add_argument('--json-indent',
metavar='',
dest='jsonindent',
type=int,
default=DEFAULTS['output']['jsonindent'],
- help='pretty-printed JSON output using indent level (default: "{}")'.format(DEFAULTS['output']['jsonindent']) )
+ help="pretty-printed JSON output using indent level (default: '{}')".format(DEFAULTS['output']['jsonindent']) )
output.add_argument('--json-compact',
dest='jsoncompact',
action='store_true',
default=DEFAULTS['output']['jsoncompact'],
- help='compact JSON output by eliminate whitespace (default: "{}")'.format('compact' if DEFAULTS['output']['jsoncompact'] else 'not compact') )
- output.add_argument('--sort',
- metavar='',
- dest='sort',
- choices=['none', 'name'],
- default=DEFAULTS['output']['sort'],
- help='sort result - can be "none" or "name" (default: "{}")'.format(DEFAULTS['output']['sort']) )
+ help="compact JSON output by eliminate whitespace (default: {})".format('normal' if not DEFAULTS['output']['jsoncompact'] else 'compact') )
+ output.add_argument('--unsort',
+ dest='unsort',
+ action='store_true',
+ default=DEFAULTS['output']['unsort'],
+ help="do not sort results (default: {})".format('sort' if not DEFAULTS['output']['unsort'] else 'unsort') )
output.add_argument('--raw',
dest='raw',
action='store_true',
default=DEFAULTS['output']['raw'],
- help='output raw values (default: {})'.format('raw' if DEFAULTS['output']['raw'] else 'processed') )
+ help="output raw values (default: {})".format('raw' if DEFAULTS['output']['raw'] else 'process') )
output.add_argument('--unhide-pw',
dest='unhidepw',
action='store_true',
default=DEFAULTS['output']['unhide-pw'],
- help='unhide passwords (default: {})'.format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') )
+ help="unhide passwords (default: {})".format('unhide' if DEFAULTS['output']['unhide-pw'] else 'hide') )
output.add_argument('-o', '--output-file',
metavar='',
dest='outputfile',
default=DEFAULTS['output']['outputfile'],
- help='file to store decrypted raw binary configuration to (default: {})'.format(DEFAULTS['output']['outputfile']))
+ help="file to store configuration to (default: {}) Macros: @v=Tasmota version, @f=friendly name".format(DEFAULTS['output']['outputfile']))
+ output.add_argument('--output-file-format',
+ metavar='',
+ dest='outputfileformat',
+ choices=['json', 'binary'],
+ default=DEFAULTS['output']['outputfileformat'],
+ help="output format ('json' or 'binary', default: '{}')".format(DEFAULTS['output']['outputfileformat']) )
parser.add_argument('-c', '--config',
metavar='',
dest='configfile',
default=DEFAULTS['DEFAULT']['configfile'],
is_config_file=True,
- help='Config file, can be used instead of command parameter (default: {})'.format(DEFAULTS['DEFAULT']['configfile']) )
+ help="Config file, can be used instead of command parameter (default: {})".format(DEFAULTS['DEFAULT']['configfile']) )
+ parser.add_argument('--exit-on-error-only',
+ dest='exitonwarning',
+ action='store_false',
+ default=DEFAULTS['DEFAULT']['exitonwarning'],
+ help="exit on error only (default: {}). Not recommended, used by your own responsibility!".format('exit on ERROR and WARNING' if DEFAULTS['DEFAULT']['exitonwarning'] else 'exit on ERROR') )
info = parser.add_argument_group('info')
info.add_argument('-V', '--version', action='version', version=PROG)
args = parser.parse_args()
-
+
+ # default no configuration available
configobj = None
-
+
+ # check source args
+ if args.device is not None and args.tasmotafile is not None:
+ exit(6, "Only one source allowed. Do not use -d and -f together")
+
+ # read config direct from device via http
if args.device is not None:
- # read config direct from device via http
buffer = io.BytesIO()
url = str("http://{}/dl".format(args.device))
c = pycurl.Curl()
@@ -1930,11 +2228,11 @@ if __name__ == "__main__":
configobj = buffer.getvalue()
+ # read config from a file
elif args.tasmotafile is not None:
- # read config from a file
if not os.path.isfile(args.tasmotafile): # check file exists
- exit(1, "file '{}' not found".format(args.tasmotafile))
+ exit(1, "File '{}' not found".format(args.tasmotafile))
try:
tasmotafile = open(args.tasmotafile, "rb")
configobj = tasmotafile.read()
@@ -1942,22 +2240,59 @@ if __name__ == "__main__":
except Exception, e:
exit(e[0], e[1])
+ # no config source given
else:
parser.print_help()
sys.exit(0)
+
if configobj is not None and len(configobj)>0:
cfg = DeEncrypt(configobj)
- if args.outputfile is not None:
- outputfile = open(args.outputfile, "wb")
- outputfile.write(cfg)
- outputfile.close()
+ config = Decode(cfg)
- Decode(cfg)
+ # output to file
+ if args.outputfile is not None:
+ outputfilename = args.outputfile
+ v = f1 = f2 = f3 = f4 = ''
+ if 'version' in config:
+ ver = int(str(config['version']), 0)
+ major = ((ver>>24) & 0xff)
+ minor = ((ver>>16) & 0xff)
+ release = ((ver>> 8) & 0xff)
+ subrelease = (ver & 0xff)
+ if major>=6:
+ if subrelease>0:
+ subreleasestr = str(subrelease)
+ else:
+ subreleasestr = ''
+ else:
+ if subrelease>0:
+ subreleasestr = str(chr(subrelease+ord('a')-1))
+ else:
+ subreleasestr = ''
+ v = "{:d}.{:d}.{:d}{}{}".format( major, minor, release, '.' if major>=6 else '', subreleasestr)
+ outputfilename = outputfilename.replace('@v', v)
+ if 'friendlyname' in config:
+ outputfilename = outputfilename.replace('@f', config['friendlyname'][0] )
+
+ if args.outputfileformat == 'binary':
+ outputfile = open(outputfilename, "wb")
+ outputfile.write(struct.pack('