Browse Source

Merge branch 'dev' into sensors

rfm69
Xose Pérez 6 years ago
parent
commit
5e1fe97def
29 changed files with 3606 additions and 648 deletions
  1. +58
    -4
      CHANGELOG.md
  2. +16
    -5
      README.md
  3. +6
    -0
      code/espurna/alexa.ino
  4. +5
    -0
      code/espurna/api.ino
  5. +14
    -1
      code/espurna/button.ino
  6. +1
    -0
      code/espurna/config/arduino.h
  7. +5
    -3
      code/espurna/config/general.h
  8. +30
    -0
      code/espurna/config/hardware.h
  9. +3
    -0
      code/espurna/config/prototypes.h
  10. +1
    -1
      code/espurna/config/version.h
  11. +5
    -0
      code/espurna/domoticz.ino
  12. +5
    -0
      code/espurna/homeassistant.ino
  13. +5
    -0
      code/espurna/influxdb.ino
  14. +7
    -0
      code/espurna/led.ino
  15. +257
    -277
      code/espurna/light.ino
  16. +5
    -0
      code/espurna/mqtt.ino
  17. +9
    -0
      code/espurna/nofuss.ino
  18. +9
    -0
      code/espurna/ntp.ino
  19. +5
    -0
      code/espurna/relay.ino
  20. +5
    -0
      code/espurna/scheduler.ino
  21. +15
    -2
      code/espurna/sensor.ino
  22. +3047
    -351
      code/espurna/static/index.html.gz.h
  23. +9
    -0
      code/espurna/telnet.ino
  24. +9
    -0
      code/espurna/thinkspeak.ino
  25. +12
    -0
      code/espurna/wifi.ino
  26. +27
    -0
      code/espurna/ws.ino
  27. +11
    -3
      code/html/index.html
  28. +25
    -1
      code/platformio.ini
  29. BIN
      images/devices/swifitch.png

+ 58
- 4
CHANGELOG.md View File

@ -3,6 +3,60 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.12.5] 2018-04-08
### Fixed
- Fixed expected power calibration ([#676](https://github.com/xoseperez/espurna/issues/676))
- Do not show empty time strings ([#691](https://github.com/xoseperez/espurna/issues/691), thanks to @PieBru)
- Fix load average calculation when system check is disabled ([#707](https://github.com/xoseperez/espurna/issues/707))
- Fixed unstability issues with NtpClientLib using temporary fork ([#743](https://github.com/xoseperez/espurna/issues/743))
- Fixed typos in homeassistant module (thanks to @Cabalist)
- Fixed default HLW8012 calibration for KMC devices (thanks to @gn0st1c)
- Fix MQTT query request
- Fix scheduler debug message
- Fix NTP offset value
### Added
- Option to change NTP timeout via compile-time setting ([#452](https://github.com/xoseperez/espurna/issues/452))
- Added humidity correction to web UI ([#626](https://github.com/xoseperez/espurna/issues/626), tahnks to @ManuelW77)
- Added support for USA DST calculation ([#664](https://github.com/xoseperez/espurna/issues/664))
- Option to reset energy count ([#671](https://github.com/xoseperez/espurna/issues/671))
- Added Sonoff SV prebuild image ([#698](https://github.com/xoseperez/espurna/issues/698), thanks to @akasma74)
- Check and remove unused config keys ([#730](https://github.com/xoseperez/espurna/issues/730))
- Visual Studio metadata files added to .gitignore ([#731](https://github.com/xoseperez/espurna/issues/731), thanks to @gn0st1c)
- Added default MQTT and SSL settings to web UI ([#732](https://github.com/xoseperez/espurna/issues/732), thanks to @mcspr)
- Added option to the web UI to set the light transition length in milliseconds ([#739](https://github.com/xoseperez/espurna/issues/739))
- Improved testing with Travis (thanks to @lobradov)
- Change dimmers using schedule (thanks to @wysiwyng)
- Debug console in web UI (thanks to @lobradov), including command execution
- Option to reset relays in MQTT disconection (thanks to @a-tom-s)
- Option to disable system check from custom header (thanks to @phuonglm)
- Added "board" topic to the heartbeat messages (thanks to @mcspr)
- Added methods to create hierarchical MQTT JSON responses
- Added RESET.SAFE command to reboot into safe mode
- Added SDK and Core versions to the web UI
- Added revision to web UI (only when built from build.sh)
- Support for OBI Powerplug Adapter ([#622](https://github.com/xoseperez/espurna/issues/622), thanks to @Geitde)
- Support for Tunbox Powerstrip02 (thanks to @gn0st1c)
- Support for Lingan SWA1 (thanks to @gn0st1c)
- Support for Heygo HY02 (thanks to @gn0st1c)
- Support for Maxcio WUS0025 (thanks to @gn0st1c)
- Support for Yidian XSSSA05 SWA1 (thanks to @gn0st1c)
- Support for ArnieX Swifitch (thanks to @LubergAlexander)
- Support for IKE ESPIKE board
- Support for AM2320 sensors via I2C (thanks to @gn0st1c)
- Support for GUVAS12SD sensor (thanks to @gn0st1c)
### Changed
- Removed hostname size limit ([#576](https://github.com/xoseperez/espurna/issues/576), [#659](https://github.com/xoseperez/espurna/issues/659))
- Reworked RGBW implementation (thanks to @Skaronator)
- Several web UI layout changes (thanks to @lobradov & @mcspr)
- Button MQTT messages will not have the retain flag (thanks to @lobradov)
- Remove unnecessary code from boot log (thanks to @gn0st1c)
- Updated logo and favicon, added gitter channel
- Force reporting power values as 0 if relay is off
- Using gulp-crass for CSS minification
- Using WIFI_NONE_SLEEP by default
## [1.12.4] 2018-03-05
### Fixed
- Adding a 1ms delay after UDP send to avoid loosing packets ([#438](https://github.com/xoseperez/espurna/issues/438))
@ -12,7 +66,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fix inline documentation for Sonoff 4CH Pro button modes ([#551](https://github.com/xoseperez/espurna/issues/551))
- Prevent resending messages from rfin in RF Bridge ([#561](https://github.com/xoseperez/espurna/issues/561))
- Fix AnalogSensor description ([#601](https://github.com/xoseperez/espurna/issues/601))
- Fixed missing setting in HASS WS callback (thanks to Maxim Prokhorov)
- Fixed missing setting in HASS WS callback (thanks to @mcspr)
- ECH1560 call sync from tick method
- Fixed several issues reported by codacy
@ -93,7 +147,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Fixed pulse and pulse_ms order in relay_t structure ([#424](https://github.com/xoseperez/espurna/issues/424))
- Use same buffer size across all terminal-realted classes/methods. Set to 128 chars ([#477](https://github.com/xoseperez/espurna/issues/477), [#478](https://github.com/xoseperez/espurna/issues/478))
- Fix WiFi scan status in web UI
- Several code quality fixes (thanks to Lazar Obradovic)
- Several code quality fixes (thanks to @lobradov)
- Fixed error message on first command over telnet
### Changed
@ -126,7 +180,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Scheduler (contributed by Stefano Cotterli, thank you!, [#131](https://github.com/xoseperez/espurna/issues/131))
- Added "wifi.scan" command to terminal
- Added ESPurna Switch board support
- Added support for python3 in memanalyzer and ota scripts (thanks to Ryan Jarvis)
- Added support for python3 in memanalyzer and ota scripts (thanks to @Cabalist)
- Added BSSID, RSSI, channels and distance to web UI status tab
- Added mDNS name resolving to MQTT, InfluxDB and NoFUSS modules ([#129](https://github.com/xoseperez/espurna/issues/129), disabled by default)
@ -143,7 +197,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.11.4] 2018-01-09
### Fixed
- Fix bug in RF Bridge when RF code contains the stop byte. Check overflow ([#357](https://github.com/xoseperez/espurna/issues/357))
- Fixed typos in code and wiki (Thanks to Ryan Jarvis)
- Fixed typos in code and wiki (Thanks to @Cabalist)
- Fix bug in magnitude topic and units ([#355](https://github.com/xoseperez/espurna/issues/355))
### Added


+ 16
- 5
README.md View File

@ -3,7 +3,7 @@
ESPurna ("spark" in Catalan) is a custom firmware for ESP8285/ESP8266 based smart switches, lights and sensors.
It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries.
[![version](https://img.shields.io/badge/version-1.12.5b-brightgreen.svg)](CHANGELOG.md)
[![version](https://img.shields.io/badge/version-1.12.5-brightgreen.svg)](CHANGELOG.md)
![branch](https://img.shields.io/badge/branch-sensors-orange.svg)
[![travis](https://travis-ci.org/xoseperez/espurna.svg?branch=sensors)](https://travis-ci.org/xoseperez/espurna)
[![codacy](https://img.shields.io/codacy/grade/c9496e25cf07434cba786b462cb15f49/sensors.svg)](https://www.codacy.com/app/xoseperez/espurna/dashboard)
@ -15,6 +15,10 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
---
## Notice
> Please use the [gitter ESPurna channel](https://gitter.im/tinkerman-cat/espurna) for support and questions, you have better chances to get fast answers from me or other ESPurna users. Open an issue here only if you feel there is a bug or you want to request an enhancement. Thank you.
## Features
* *KRACK* vulnerability free (when built with Arduino Core >= 2.4.0)
@ -44,7 +48,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Change LED notification mode
* Remote reset the board
* Fully configurable in webUI (broker, user, password, QoS, keep alive time, retain flag, client ID)
* **Scheduler** to automatically turn on, off or toggle any relay at a given time and day
* **Scheduler** to automatically turn on, off or toggle any relay at a given time and day, also change light intensity for dimmers
* **Alexa** integration using the [FauxmoESP Library](https://bitbucket.org/xoseperez/fauxmoesp)
* [**Google Assistant**](http://tinkerman.cat/using-google-assistant-control-your-esp8266-devices/) integration using IFTTT and Webhooks (Google Home, Allo)
* [**Domoticz**](https://domoticz.com/) integration via MQTT
@ -65,15 +69,18 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* **BMP280** and **BME280** temperature, humidity (BME280) and pressure sensor by Bosch
* **SI7021** temperature and humidity sensor
* **SHT3X** temperature and humidity sensor over I2C (Wemos shield)
* **AM2320** temperature and humidity sensor over I2C
* **Dallas OneWire sensors** like the DS18B20
* **MHZ19** CO2 sensor
* **PMSX003** dust sensor
* **BH1750** luminosity sensor
* **GUVAS12SD** UV sensor
* Power monitoring
* **HLW8012** using the [HLW8012 Library](https://bitbucket.org/xoseperez/hlw8012) (Sonoff POW)
* Non-invasive **current sensor** using **internal ADC** or **ADC121** or **ADS1115**
* **ECH1560** power monitor chip
* **V9261F** power monitor chip
* **PZEM0004T** power monitor board
* Raw analog and digital sensors
* Simple pulse counter
* Support for different units (Fahrenheit or Celsius, Watts or Kilowatts, Joules or kWh)
@ -117,7 +124,7 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
* Shows debug info and allows to run terminal commands
* **NTP** for time synchronization
* Supports worldwide time zones
* Compatible with DST
* Compatible with DST (EU and USA)
* **Unstable system check**
* Detects unstable system (crashes on boot continuously) and defaults to a stable system
* Only WiFi AP, OTA and Telnet available if system is flagged as unstable
@ -131,6 +138,10 @@ It uses the Arduino Core for ESP8266 framework and a number of 3rd party librari
## Notices
---
> **2018-04-08**<br />
> Please use [gitter](https://gitter.im/tinkerman-cat/espurna) for support and questions, you have better chances to get fast answers by me or other ESPurna users. Open an issue here only if you feel there is a bug or you want to request an enhancement. Thank you.
---
> **2018-03-09**<br />
> Default branch in GitHub is now the development branch "dev".<br>
@ -204,8 +215,8 @@ Here is the list of supported hardware. For more information please refer to the
|**Maxcio W-US002S**|**HEYGO HY02**|**YiDian XS-SSA05**|
|![WiOn 50055](images/devices/wion-50055.jpg)|![LINGAN SWA1](images/devices/lingan-swa1.jpg)|![Tonbux PowerStrip02](images/devices/tonbux-powerstrip02.jpg)|
|**WiOn 50055**|**LINGAN SWA1**|**Tonbux PowerStrip02**
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)||
|**Itead Sonoff Touch**|**Itead Sonoff T1**||
|![Itead Sonoff Touch](images/devices/itead-sonoff-touch.jpg)|![Itead Sonoff T1](images/devices/itead-sonoff-t1.jpg)|![Swifitch](images/devices/swifitch.png)|
|**Itead Sonoff Touch**|**Itead Sonoff T1**|Swifitch|
|![Itead Slampher](images/devices/itead-slampher.jpg)|||
|**Itead Slampher**|||
|![Itead Sonoff B1](images/devices/itead-sonoff-b1.jpg)|![AI-Thinker Wifi Light / Noduino OpenLight](images/devices/aithinker-ai-light.jpg)|![Authometion LYT8266](images/devices/authometion-lyt8266.jpg)|


+ 6
- 0
code/espurna/alexa.ino View File

@ -22,6 +22,11 @@ static std::queue<AlexaDevChange> _alexa_dev_changes;
// -----------------------------------------------------------------------------
// ALEXA
// -----------------------------------------------------------------------------
bool _alexaWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "alexa", 5) == 0);
}
void _alexaWebSocketOnSend(JsonObject& root) {
root["alexaVisible"] = 1;
root["alexaEnabled"] = getSetting("alexaEnabled", ALEXA_ENABLED).toInt() == 1;
@ -46,6 +51,7 @@ void alexaSetup() {
// Websockets
wsOnSendRegister(_alexaWebSocketOnSend);
wsOnAfterParseRegister(_alexaConfigure);
wsOnReceiveRegister(_alexaWebSocketOnReceive);
#endif


+ 5
- 0
code/espurna/api.ino View File

@ -22,6 +22,10 @@ std::vector<web_api_t> _apis;
// -----------------------------------------------------------------------------
bool _apiWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "api", 3) == 0);
}
void _apiWebSocketOnSend(JsonObject& root) {
root["apiEnabled"] = getSetting("apiEnabled", API_ENABLED).toInt() == 1;
root["apiKey"] = getSetting("apiKey");
@ -187,6 +191,7 @@ void apiSetup() {
webServer()->on("/apis", HTTP_GET, _onAPIs);
webServer()->on("/rpc", HTTP_GET, _onRPC);
wsOnSendRegister(_apiWebSocketOnSend);
wsOnReceiveRegister(_apiWebSocketOnReceive);
}
#endif // WEB_SUPPORT

+ 14
- 1
code/espurna/button.ino View File

@ -27,7 +27,15 @@ void buttonMQTT(unsigned char id, uint8_t event) {
if (id >= _buttons.size()) return;
char payload[2];
itoa(event, payload, 10);
mqttSend(MQTT_TOPIC_BUTTON, id, payload, false, false); // 1st bool = force, 2nd = retain
mqttSend(MQTT_TOPIC_BUTTON, id, payload, false, false); // 1st bool = force, 2nd = retain
}
#endif
#if WEB_SUPPORT
bool _buttonWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "btn", 3) == 0);
}
#endif
@ -182,6 +190,11 @@ void buttonSetup() {
DEBUG_MSG_P(PSTR("[BUTTON] Number of buttons: %u\n"), _buttons.size());
// Websocket Callbacks
#if WEB_SUPPORT
wsOnReceiveRegister(_buttonWebSocketOnReceive);
#endif
// Register loop
espurnaRegisterLoop(buttonLoop);


+ 1
- 0
code/espurna/config/arduino.h View File

@ -71,6 +71,7 @@
//#define TONBUX_XSSSA06
//#define GREEN_ESP8266RELAY
//#define IKE_ESPIKE
//#define ARNIEX_SWIFITCH
//--------------------------------------------------------------------------------
// Features (values below are non-default values)


+ 5
- 3
code/espurna/config/general.h View File

@ -622,7 +622,6 @@ PROGMEM const char* const custom_reset_string[] = {
// Light module
#define MQTT_TOPIC_CHANNEL "channel"
#define MQTT_TOPIC_COLOR "color" // DEPRECATED, use RGB instead
#define MQTT_TOPIC_COLOR_RGB "rgb"
#define MQTT_TOPIC_COLOR_HSV "hsv"
#define MQTT_TOPIC_ANIM_MODE "anim_mode"
@ -717,6 +716,9 @@ PROGMEM const char* const custom_reset_string[] = {
#endif
#define LIGHT_MAX_BRIGHTNESS 255 // Maximun brightness value
//#define LIGHT_MIN_MIREDS 153 // NOT USED (yet)! // Default to the Philips Hue value that HA has always assumed
//#define LIGHT_MAX_MIREDS 500 // NOT USED (yet)! // https://developers.meethue.com/documentation/core-concepts
#define LIGHT_DEFAULT_MIREDS 153 // Default value used by MQTT. This value is __NEVRER__ applied!
#define LIGHT_STEP 32 // Step size
#define LIGHT_USE_COLOR 1 // Use 3 first channels as RGB
#define LIGHT_USE_WHITE 0 // Use white channel whenever RGB have the same value
@ -726,7 +728,7 @@ PROGMEM const char* const custom_reset_string[] = {
#define LIGHT_USE_TRANSITIONS 1 // Transitions between colors
#define LIGHT_TRANSITION_STEP 10 // Time in millis between each transtion step
#define LIGHT_TRANSITION_STEPS 50 // Number of steps to acomplish transition
#define LIGHT_TRANSITION_TIME 500 // Time in millis from color to color
// -----------------------------------------------------------------------------
// DOMOTICZ
@ -839,7 +841,7 @@ PROGMEM const char* const custom_reset_string[] = {
#define NTP_SERVER "pool.ntp.org" // Default NTP server
#define NTP_TIMEOUT 2000 // Set NTP request timeout to 2 seconds (issue #452)
#define NTP_TIME_OFFSET 1 // Default timezone offset (GMT+1)
#define NTP_TIME_OFFSET 60 // Default timezone offset (GMT+1)
#define NTP_DAY_LIGHT true // Enable daylight time saving by default
#define NTP_SYNC_INTERVAL 60 // NTP initial check every minute
#define NTP_UPDATE_INTERVAL 1800 // NTP check every 30 minutes


+ 30
- 0
code/espurna/config/hardware.h View File

@ -1794,6 +1794,36 @@
#define LED1_PIN 2
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// SWIFITCH
// https://github.com/ArnieX/swifitch
// -----------------------------------------------------------------------------
#elif defined(ARNIEX_SWIFITCH)
// Info
#define MANUFACTURER "ARNIEX"
#define DEVICE "SWIFITCH"
// Buttons
#define BUTTON1_PIN 4 // D2
#define BUTTON1_MODE BUTTON_SWITCH | BUTTON_SET_PULLUP | BUTTON_DEFAULT_HIGH
#define BUTTON1_RELAY 1
#define BUTTON1_PRESS BUTTON_MODE_NONE
#define BUTTON1_CLICK BUTTON_MODE_TOGGLE
#define BUTTON1_DBLCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGCLICK BUTTON_MODE_NONE
#define BUTTON1_LNGLNGCLICK BUTTON_MODE_NONE
// Relays
#define RELAY1_PIN 5 // D1
#define RELAY1_TYPE RELAY_TYPE_INVERSE
// LEDs
#define LED1_PIN 12 // D6
#define LED1_PIN_INVERSE 1
// -----------------------------------------------------------------------------
// TEST boards (do not use!!)
// -----------------------------------------------------------------------------


+ 3
- 0
code/espurna/config/prototypes.h View File

@ -33,6 +33,9 @@ void wsOnActionRegister(ws_on_action_callback_f callback);
typedef std::function<void(void)> ws_on_after_parse_callback_f;
void wsOnAfterParseRegister(ws_on_after_parse_callback_f callback);
typedef std::function<bool(const char *, JsonVariant&)> ws_on_receive_callback_f;
void wsOnReceiveRegister(ws_on_receive_callback_f callback);
// -----------------------------------------------------------------------------
// WIFI
// -----------------------------------------------------------------------------


+ 1
- 1
code/espurna/config/version.h View File

@ -1,5 +1,5 @@
#define APP_NAME "ESPURNA"
#define APP_VERSION "1.12.5b"
#define APP_VERSION "1.12.5"
#define APP_REVISION ""
#define APP_AUTHOR "xose.perez@gmail.com"
#define APP_WEBSITE "http://tinkerman.cat"


+ 5
- 0
code/espurna/domoticz.ino View File

@ -76,6 +76,10 @@ void _domoticzMqtt(unsigned int type, const char * topic, const char * payload)
#if WEB_SUPPORT
bool _domoticzWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "dcz", 3) == 0);
}
void _domoticzWebSocketOnSend(JsonObject& root) {
root["dczVisible"] = 1;
@ -145,6 +149,7 @@ void domoticzSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_domoticzWebSocketOnSend);
wsOnAfterParseRegister(_domoticzConfigure);
wsOnReceiveRegister(_domoticzWebSocketOnReceive);
#endif
mqttRegister(_domoticzMqtt);
}


+ 5
- 0
code/espurna/homeassistant.ino View File

@ -224,6 +224,10 @@ void _haConfigure() {
#if WEB_SUPPORT
bool _haWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "ha", 2) == 0);
}
void _haWebSocketOnSend(JsonObject& root) {
root["haVisible"] = 1;
root["haPrefix"] = getSetting("haPrefix", HOMEASSISTANT_PREFIX);
@ -275,6 +279,7 @@ void haSetup() {
wsOnSendRegister(_haWebSocketOnSend);
wsOnAfterParseRegister(_haConfigure);
wsOnActionRegister(_haWebSocketOnAction);
wsOnReceiveRegister(_haWebSocketOnReceive);
#endif
// On MQTT connect check if we have something to send


+ 5
- 0
code/espurna/influxdb.ino View File

@ -16,6 +16,10 @@ SyncClient _idb_client;
// -----------------------------------------------------------------------------
bool _idbWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "idb", 3) == 0);
}
void _idbWebSocketOnSend(JsonObject& root) {
root["idbVisible"] = 1;
root["idbEnabled"] = getSetting("idbEnabled", INFLUXDB_ENABLED).toInt() == 1;
@ -100,6 +104,7 @@ void idbSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_idbWebSocketOnSend);
wsOnAfterParseRegister(_idbConfigure);
wsOnReceiveRegister(_idbWebSocketOnReceive);
#endif
}


+ 7
- 0
code/espurna/led.ino View File

@ -58,11 +58,17 @@ void _ledBlink(unsigned char id, unsigned long delayOff, unsigned long delayOn)
}
#if WEB_SUPPORT
bool _ledWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "led", 3) == 0);
}
void _ledWebSocketOnSend(JsonObject& root) {
if (_ledCount() == 0) return;
root["ledVisible"] = 1;
root["ledMode0"] = _ledMode(0);
}
#endif
#if MQTT_SUPPORT
@ -163,6 +169,7 @@ void ledSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_ledWebSocketOnSend);
wsOnAfterParseRegister(_ledConfigure);
wsOnReceiveRegister(_ledWebSocketOnReceive);
#endif
DEBUG_MSG_P(PSTR("[LED] Number of leds: %d\n"), _leds.size());


+ 257
- 277
code/espurna/light.ino View File

@ -28,7 +28,8 @@ typedef struct {
unsigned char pin;
bool reverse;
bool state;
unsigned char value; // target or nominal value
unsigned char inputValue; // value that has been inputted
unsigned char value; // normalized value including brightness
unsigned char shadow; // represented value
double current; // transition value
} channel_t;
@ -36,11 +37,13 @@ std::vector<channel_t> _light_channel;
bool _light_state = false;
bool _light_use_transitions = false;
unsigned int _light_transition_time = LIGHT_TRANSITION_TIME;
bool _light_has_color = false;
bool _light_use_white = false;
bool _light_use_gamma = false;
unsigned long _light_steps_left = 1;
unsigned int _light_brightness = LIGHT_MAX_BRIGHTNESS;
unsigned char _light_brightness = LIGHT_MAX_BRIGHTNESS;
unsigned int _light_mireds = LIGHT_DEFAULT_MIREDS;
#if LIGHT_PROVIDER == LIGHT_PROVIDER_MY92XX
#include <my92xx.h>
@ -73,99 +76,121 @@ const unsigned char _light_gamma_table[] = {
// UTILS
// -----------------------------------------------------------------------------
void _fromLong(unsigned long value, bool brightness) {
void _setRGBInputValue(unsigned char red, unsigned char green, unsigned char blue) {
_light_channel[0].inputValue = red;
_light_channel[1].inputValue = green;
_light_channel[2].inputValue = blue;
}
void _generateBrightness() {
double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
// Convert RGB to RGBW
if (_light_has_color && _light_use_white) {
unsigned char white, max_in, max_out;
double factor = 0;
white = std::min(_light_channel[0].inputValue, std::min(_light_channel[1].inputValue, _light_channel[2].inputValue));
max_in = std::max(_light_channel[0].inputValue, std::max(_light_channel[1].inputValue, _light_channel[2].inputValue));
for (unsigned int i=0; i < 3; i++) {
_light_channel[i].value = _light_channel[i].inputValue - white;
}
_light_channel[3].value = white;
max_out = std::max(std::max(_light_channel[0].value, _light_channel[1].value), std::max(_light_channel[2].value, _light_channel[3].value));
if (max_out > 0) {
factor = (double) (max_in / max_out);
}
// Scale up to equal input values. So [250,150,50] -> [200,100,0,50] -> [250, 125, 0, 63]
for (unsigned int i=0; i < 4; i++) {
_light_channel[i].value = round((double) _light_channel[i].value * factor * brightness);
}
// Don't apply brightness, it is already in the inputValue:
if (_light_channel.size() == 5) {
_light_channel[4].value = _light_channel[4].inputValue;
}
} else {
// Don't apply brightness, it is already in the inputValue:
for (unsigned char i=0; i < _light_channel.size(); i++) {
if (_light_has_color & (i<3)) {
_light_channel[i].value = _light_channel[i].inputValue * brightness;
} else {
_light_channel[i].value = _light_channel[i].inputValue;
}
}
}
}
// -----------------------------------------------------------------------------
// Input Values
// -----------------------------------------------------------------------------
void _fromLong(unsigned long value, bool brightness) {
if (brightness) {
_light_channel[0].value = (value >> 24) & 0xFF;
_light_channel[1].value = (value >> 16) & 0xFF;
_light_channel[2].value = (value >> 8) & 0xFF;
_setRGBInputValue((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF);
_light_brightness = (value & 0xFF) * LIGHT_MAX_BRIGHTNESS / 255;
} else {
_light_channel[0].value = (value >> 16) & 0xFF;
_light_channel[1].value = (value >> 8) & 0xFF;
_light_channel[2].value = (value) & 0xFF;
_setRGBInputValue((value >> 16) & 0xFF, (value >> 8) & 0xFF, (value) & 0xFF);
}
}
void _fromRGB(const char * rgb) {
char * p = (char *) rgb;
if (strlen(p) == 0) return;
// if color begins with a # then assume HEX RGB
if (p[0] == '#') {
switch (p[0]) {
case '#': // HEX Value
if (_light_has_color) {
++p;
unsigned long value = strtoul(p, NULL, 16);
// RGBA values are interpreted like RGB + brightness
_fromLong(value, strlen(p) > 7);
}
// it's a temperature in mireds
} else if (p[0] == 'M') {
break;
case 'M': // Mired Value
if (_light_has_color) {
unsigned long mireds = atol(p + 1);
_fromMireds(mireds);
}
// it's a temperature in kelvin
} else if (p[0] == 'K') {
break;
case 'K': // Kelvin Value
if (_light_has_color) {
unsigned long kelvin = atol(p + 1);
_fromKelvin(kelvin);
}
// otherwise assume decimal values separated by commas
} else {
break;
default: // assume decimal values separated by commas
char * tok;
unsigned char count = 0;
unsigned char channels = _light_channel.size();
tok = strtok(p, ",");
while (tok != NULL) {
_light_channel[count].value = atoi(tok);
_light_channel[count].inputValue = atoi(tok);
if (++count == channels) break;
tok = strtok(NULL, ",");
}
// RGB but less than 3 values received
// RGB but less than 3 values received, assume it is 0
if (_light_has_color && (count < 3)) {
_light_channel[1].value = _light_channel[0].value;
_light_channel[2].value = _light_channel[0].value;
// check channel 1 and 2:
for (int i = 1; i <= 2; i++) {
if (count < (i+1)) {
_light_channel[i].inputValue = 0;
}
}
}
break;
}
}
void _toRGB(char * rgb, size_t len, bool applyBrightness) {
if (!_light_has_color) return;
float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1;
unsigned long value = 0;
value += _light_channel[0].value * b;
value <<= 8;
value += _light_channel[1].value * b;
value <<= 8;
value += _light_channel[2].value * b;
snprintf_P(rgb, len, PSTR("#%06X"), value);
}
void _toRGB(char * rgb, size_t len) {
_toRGB(rgb, len, false);
}
// HSV string is expected to be "H,S,V", where:
@ -192,86 +217,122 @@ void _fromHSV(const char * hsv) {
// HSV to RGB transformation -----------------------------------------------
//INPUT: [0,100,57]
//IS: [145,0,0]
//SHOULD: [255,0,0]
double h = (value[0] == 360) ? 0 : (double) value[0] / 60.0;
double f = (h - floor(h));
double s = (double) value[1] / 100.0;
unsigned char v = round((double) value[2] * 255.0 / 100.0);
unsigned char p = round(v * (1.0 - s));
unsigned char q = round(v * (1.0 - s * f));
unsigned char t = round(v * (1.0 - s * (1.0 - f)));
_light_brightness = round((double) value[2] * 2.55); // (255/100)
unsigned char p = round(255 * (1.0 - s));
unsigned char q = round(255 * (1.0 - s * f));
unsigned char t = round(255 * (1.0 - s * (1.0 - f)));
switch (int(h)) {
case 0:
_light_channel[0].value = v;
_light_channel[1].value = t;
_light_channel[2].value = p;
_setRGBInputValue(255, t, p);
break;
case 1:
_light_channel[0].value = q;
_light_channel[1].value = v;
_light_channel[2].value = p;
_setRGBInputValue(q, 255, p);
break;
case 2:
_light_channel[0].value = p;
_light_channel[1].value = v;
_light_channel[2].value = t;
_setRGBInputValue(p, 255, t);
break;
case 3:
_light_channel[0].value = p;
_light_channel[1].value = q;
_light_channel[2].value = v;
_setRGBInputValue(p, q, 255);
break;
case 4:
_light_channel[0].value = t;
_light_channel[1].value = p;
_light_channel[2].value = v;
_setRGBInputValue(t, p, 255);
break;
case 5:
_light_channel[0].value = v;
_light_channel[1].value = p;
_light_channel[2].value = q;
_setRGBInputValue(255, p, q);
break;
default:
_light_channel[0].value = 0;
_light_channel[1].value = 0;
_light_channel[2].value = 0;
_setRGBInputValue(0, 0, 0);
break;
}
}
// Thanks to Sacha Telgenhof for sharing this code in his AiLight library
// https://github.com/stelgenhof/AiLight
void _fromKelvin(unsigned long kelvin, bool setMireds) {
// Check we have RGB channels
if (!_light_has_color) return;
_light_brightness = LIGHT_MAX_BRIGHTNESS;
if (setMireds) {
_light_mireds = round(1000000UL / kelvin);
}
// Calculate colors
unsigned int red = (kelvin <= 66)
? LIGHT_MAX_VALUE
: 329.698727446 * pow((kelvin - 60), -0.1332047592);
unsigned int green = (kelvin <= 66)
? 99.4708025861 * log(kelvin) - 161.1195681661
: 288.1221695283 * pow(kelvin, -0.0755148492);
unsigned int blue = (kelvin >= 66)
? LIGHT_MAX_VALUE
: ((kelvin <= 19)
? 0
: 138.5177312231 * log(kelvin - 10) - 305.0447927307);
_setRGBInputValue(
constrain(red, 0, LIGHT_MAX_VALUE),
constrain(green, 0, LIGHT_MAX_VALUE),
constrain(blue, 0, LIGHT_MAX_VALUE)
);
}
void _toHSV(char * hsv, size_t len) {
void _fromKelvin(unsigned long kelvin) {
_fromKelvin(kelvin, true);
}
if (!_light_has_color) return;
// Color temperature is measured in mireds (kelvin = 1e6/mired)
void _fromMireds(unsigned long mireds) {
if (mireds == 0) mireds = 1;
_light_mireds = mireds;
unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000) / 100;
_fromKelvin(kelvin, false);
}
double min, max;
double h, s, v;
// -----------------------------------------------------------------------------
// Output Values
// -----------------------------------------------------------------------------
double r = (double) _light_channel[0].value / 255.0;
double g = (double) _light_channel[1].value / 255.0;
double b = (double) _light_channel[2].value / 255.0;
void _toRGB(char * rgb, size_t len) {
unsigned long value = 0;
min = (r < g) ? r : g;
min = (min < b) ? min : b;
max = (r > g) ? r : g;
max = (max > b) ? max : b;
value += _light_channel[0].inputValue;
value <<= 8;
value += _light_channel[1].inputValue;
value <<= 8;
value += _light_channel[2].inputValue;
snprintf_P(rgb, len, PSTR("#%06X"), value);
}
void _toHSV(char * hsv, size_t len) {
double min, max, h, s, v;
double brightness = (double) _light_brightness / LIGHT_MAX_BRIGHTNESS;
double r = (double) (_light_channel[0].inputValue * brightness) / 255.0;
double g = (double) (_light_channel[1].inputValue * brightness) / 255.0;
double b = (double) (_light_channel[2].inputValue * brightness) / 255.0;
min = std::min(r, std::min(g, b));
max = std::max(r, std::max(g, b));
v = 100.0 * max;
if (v == 0) {
h = s = 0;
} else {
s = 100.0 * (max - min) / max;
if (s == 0) {
h = 0;
} else {
if (max == r) {
if (g >= b) {
h = 0.0 + 60.0 * (g - b) / (max - min);
@ -284,77 +345,33 @@ void _toHSV(char * hsv, size_t len) {
h = 240.0 + 60.0 * (r - g) / (max - min);
}
}
}
// String
snprintf_P(hsv, len, PSTR("%d,%d,%d"), round(h), round(s), round(v));
}
void _toLong(char * color, size_t len, bool applyBrightness) {
void _toLong(char * color, size_t len) {
if (!_light_has_color) return;
float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1;
snprintf_P(color, len, PSTR("%d,%d,%d"),
(int) (_light_channel[0].value * b),
(int) (_light_channel[1].value * b),
(int) (_light_channel[2].value * b)
(int) _light_channel[0].inputValue,
(int) _light_channel[1].inputValue,
(int) _light_channel[2].inputValue
);
}
void _toLong(char * color, size_t len) {
_toLong(color, len, false);
}
void _toCSV(char * buffer, size_t len, bool applyBrightness) {
char num[10];
float b = applyBrightness ? (float) _light_brightness / LIGHT_MAX_BRIGHTNESS : 1;
for (unsigned char i=0; i<_light_channel.size(); i++) {
itoa(_light_channel[i].value * b, num, 10);
itoa(_light_channel[i].inputValue * b, num, 10);
if (i>0) strncat(buffer, ",", len--);
strncat(buffer, num, len);
len = len - strlen(num);
}
}
// Thanks to Sacha Telgenhof for sharing this code in his AiLight library
// https://github.com/stelgenhof/AiLight
void _fromKelvin(unsigned long kelvin) {
// Check we have RGB channels
if (!_light_has_color) return;
// Calculate colors
unsigned int red = (kelvin <= 66)
? LIGHT_MAX_VALUE
: 329.698727446 * pow((kelvin - 60), -0.1332047592);
unsigned int green = (kelvin <= 66)
? 99.4708025861 * log(kelvin) - 161.1195681661
: 288.1221695283 * pow(kelvin, -0.0755148492);
unsigned int blue = (kelvin >= 66)
? LIGHT_MAX_VALUE
: ((kelvin <= 19)
? 0
: 138.5177312231 * log(kelvin - 10) - 305.0447927307);
// Save values
_light_channel[0].value = constrain(red, 0, LIGHT_MAX_VALUE);
_light_channel[1].value = constrain(green, 0, LIGHT_MAX_VALUE);
_light_channel[2].value = constrain(blue, 0, LIGHT_MAX_VALUE);
}
// Color temperature is measured in mireds (kelvin = 1e6/mired)
void _fromMireds(unsigned long mireds) {
if (mireds == 0) mireds = 1;
unsigned long kelvin = constrain(1000000UL / mireds, 1000, 40000) / 100;
_fromKelvin(kelvin);
}
// -----------------------------------------------------------------------------
// PROVIDER
// -----------------------------------------------------------------------------
@ -374,7 +391,6 @@ unsigned int _toPWM(unsigned char id) {
}
void _shadow() {
// Update transition ticker
_light_steps_left--;
if (_light_steps_left == 0) _light_transition_ticker.detach();
@ -382,14 +398,7 @@ void _shadow() {
// Transitions
unsigned char target;
for (unsigned int i=0; i < _light_channel.size(); i++) {
if (_light_state && _light_channel[i].state) {
target = _light_channel[i].value;
if ((_light_brightness < LIGHT_MAX_BRIGHTNESS) && _light_has_color && (i < 3)) {
target *= ((float) _light_brightness / LIGHT_MAX_BRIGHTNESS);
}
} else {
target = 0;
}
target = _light_state ? _light_channel[i].value : 0;
if (_light_steps_left == 0) {
_light_channel[i].current = target;
} else {
@ -398,17 +407,6 @@ void _shadow() {
}
_light_channel[i].shadow = _light_channel[i].current;
}
// Use white channel for same RGB
if (_light_use_white && _light_has_color) {
if (_light_channel[0].shadow == _light_channel[1].shadow && _light_channel[1].shadow == _light_channel[2].shadow ) {
_light_channel[3].shadow = _light_channel[0].shadow * ((float) _light_brightness / LIGHT_MAX_BRIGHTNESS);
_light_channel[2].shadow = 0;
_light_channel[1].shadow = 0;
_light_channel[0].shadow = 0;
}
}
}
void _lightProviderUpdate() {
@ -442,7 +440,7 @@ void _lightProviderUpdate() {
void _lightColorSave() {
for (unsigned int i=0; i < _light_channel.size(); i++) {
setSetting("ch", i, _light_channel[i].value);
setSetting("ch", i, _light_channel[i].inputValue);
}
setSetting("brightness", _light_brightness);
saveSettings();
@ -450,7 +448,7 @@ void _lightColorSave() {
void _lightColorRestore() {
for (unsigned int i=0; i < _light_channel.size(); i++) {
_light_channel[i].value = getSetting("ch", i, i==0 ? 255 : 0).toInt();
_light_channel[i].inputValue = getSetting("ch", i, i==0 ? 255 : 0).toInt();
}
_light_brightness = getSetting("brightness", LIGHT_MAX_BRIGHTNESS).toInt();
lightUpdate(false, false);
@ -471,7 +469,6 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
mqttSubscribe(MQTT_TOPIC_BRIGHTNESS);
mqttSubscribe(MQTT_TOPIC_MIRED);
mqttSubscribe(MQTT_TOPIC_KELVIN);
mqttSubscribe(MQTT_TOPIC_COLOR); // DEPRECATE
mqttSubscribe(MQTT_TOPIC_COLOR_RGB);
mqttSubscribe(MQTT_TOPIC_COLOR_HSV);
}
@ -513,7 +510,7 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
}
// Color
if (t.equals(MQTT_TOPIC_COLOR) || t.equals(MQTT_TOPIC_COLOR_RGB)) { // DEPRECATE MQTT_TOPIC_COLOR
if (t.equals(MQTT_TOPIC_COLOR_RGB)) {
lightColor(payload, true);
lightUpdate(true, mqttForward());
return;
@ -548,19 +545,18 @@ void _lightMQTTCallback(unsigned int type, const char * topic, const char * payl
}
void lightMQTT() {
char buffer[20];
if (_light_has_color) {
// Color
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, sizeof(buffer), false);
_toRGB(buffer, sizeof(buffer));
} else {
_toLong(buffer, sizeof(buffer), false);
_toLong(buffer, sizeof(buffer));
}
mqttSend(MQTT_TOPIC_COLOR, buffer); // DEPRECATE
mqttSend(MQTT_TOPIC_COLOR_RGB, buffer);
_toHSV(buffer, sizeof(buffer));
mqttSend(MQTT_TOPIC_COLOR_HSV, buffer);
@ -568,11 +564,14 @@ void lightMQTT() {
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_brightness);
mqttSend(MQTT_TOPIC_BRIGHTNESS, buffer);
// Mireds
snprintf_P(buffer, sizeof(buffer), PSTR("%d"), _light_mireds);
mqttSend(MQTT_TOPIC_MIRED, buffer);
}
// Channels
for (unsigned int i=0; i < _light_channel.size(); i++) {
itoa(_light_channel[i].value, buffer, 10);
itoa(_light_channel[i].inputValue, buffer, 10);
mqttSend(MQTT_TOPIC_CHANNEL, i, buffer);
}
@ -598,7 +597,7 @@ void lightMQTTGroup() {
void lightBroker() {
char buffer[10];
for (unsigned int i=0; i < _light_channel.size(); i++) {
itoa(_light_channel[i].value, buffer, 10);
itoa(_light_channel[i].inputValue, buffer, 10);
brokerPublish(MQTT_TOPIC_CHANNEL, i, buffer);
}
}
@ -617,14 +616,12 @@ bool lightHasColor() {
return _light_has_color;
}
unsigned char lightWhiteChannels() {
return _light_channel.size() % 3;
}
void lightUpdate(bool save, bool forward, bool group_forward) {
_generateBrightness();
// Configure color transition
_light_steps_left = _light_use_transitions ? LIGHT_TRANSITION_STEPS : 1;
_light_steps_left = _light_use_transitions ? _light_transition_time / LIGHT_TRANSITION_STEP : 1;
_light_transition_ticker.attach_ms(LIGHT_TRANSITION_STEP, _lightProviderUpdate);
// Report channels to local broker
@ -696,7 +693,7 @@ void lightColor(unsigned long color) {
String lightColor(bool rgb) {
char str[12];
if (rgb) {
_toRGB(str, sizeof(str), false);
_toRGB(str, sizeof(str));
} else {
_toHSV(str, sizeof(str));
}
@ -709,14 +706,14 @@ String lightColor() {
unsigned int lightChannel(unsigned char id) {
if (id <= _light_channel.size()) {
return _light_channel[id].value;
return _light_channel[id].inputValue;
}
return 0;
}
void lightChannel(unsigned char id, unsigned int value) {
if (id <= _light_channel.size()) {
_light_channel[id].value = constrain(value, 0, LIGHT_MAX_VALUE);
_light_channel[id].inputValue = constrain(value, 0, LIGHT_MAX_VALUE);
}
}
@ -738,6 +735,12 @@ void lightBrightnessStep(int steps) {
#if WEB_SUPPORT
bool _lightWebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "light", 5) == 0) return true;
if (strncmp(key, "use", 3) == 0) return true;
return false;
}
void _lightWebSocketOnSend(JsonObject& root) {
root["colorVisible"] = 1;
root["mqttGroupColor"] = getSetting("mqttGroupColor");
@ -745,6 +748,7 @@ void _lightWebSocketOnSend(JsonObject& root) {
root["useWhite"] = _light_use_white;
root["useGamma"] = _light_use_gamma;
root["useTransitions"] = _light_use_transitions;
root["lightTime"] = _light_transition_time;
root["useCSS"] = getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1;
bool useRGB = getSetting("useRGB", LIGHT_USE_RGB).toInt() == 1;
root["useRGB"] = useRGB;
@ -757,15 +761,13 @@ void _lightWebSocketOnSend(JsonObject& root) {
}
}
JsonArray& channels = root.createNestedArray("channels");
for (unsigned char id=0; id < lightChannels(); id++) {
for (unsigned char id=0; id < _light_channel.size(); id++) {
channels.add(lightChannel(id));
}
}
void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject& data) {
if (_light_has_color) {
if (strcmp(action, "color") == 0) {
if (data.containsKey("rgb")) {
lightColor(data["rgb"], true);
@ -780,7 +782,6 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
lightUpdate(true, true);
}
}
}
if (strcmp(action, "channel") == 0) {
@ -789,99 +790,76 @@ void _lightWebSocketOnAction(uint32_t client_id, const char * action, JsonObject
lightUpdate(true, true);
}
}
}
void _lightAPISetup() {
// API entry points (protected with apikey)
if (_light_has_color) {
// DEPRECATE
apiRegister(MQTT_TOPIC_COLOR,
[](char * buffer, size_t len) {
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, len, false);
} else {
_toLong(buffer, len, false);
}
},
[](const char * payload) {
lightColor(payload, true);
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_COLOR_RGB,
[](char * buffer, size_t len) {
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, len, false);
} else {
_toLong(buffer, len, false);
}
},
[](const char * payload) {
lightColor(payload, true);
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_COLOR_HSV,
[](char * buffer, size_t len) {
_toHSV(buffer, len);
},
[](const char * payload) {
lightColor(payload, false);
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_BRIGHTNESS,
[](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), _light_brightness);
},
[](const char * payload) {
lightBrightness(atoi(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_KELVIN,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromKelvin(atol(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_MIRED,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromMireds(atol(payload));
lightUpdate(true, true);
// API entry points (protected with apikey)
if (_light_has_color) {
apiRegister(MQTT_TOPIC_COLOR_RGB,
[](char * buffer, size_t len) {
if (getSetting("useCSS", LIGHT_USE_CSS).toInt() == 1) {
_toRGB(buffer, len);
} else {
_toLong(buffer, len);
}
);
}
for (unsigned int id=0; id<lightChannels(); id++) {
char key[15];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id);
},
[](const char * payload) {
lightColor(payload, true);
lightUpdate(true, true);
}
);
apiRegister(key,
[id](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), lightChannel(id));
},
[id](const char * payload) {
lightChannel(id, atoi(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_COLOR_HSV,
[](char * buffer, size_t len) {
_toHSV(buffer, len);
},
[](const char * payload) {
lightColor(payload, false);
lightUpdate(true, true);
}
);
}
apiRegister(MQTT_TOPIC_BRIGHTNESS,
[](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), _light_brightness);
},
[](const char * payload) {
lightBrightness(atoi(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_KELVIN,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromKelvin(atol(payload));
lightUpdate(true, true);
}
);
apiRegister(MQTT_TOPIC_MIRED,
[](char * buffer, size_t len) {},
[](const char * payload) {
_fromMireds(atol(payload));
lightUpdate(true, true);
}
);
}
for (unsigned int id=0; id<_light_channel.size(); id++) {
char key[15];
snprintf_P(key, sizeof(key), PSTR("%s/%d"), MQTT_TOPIC_CHANNEL, id);
apiRegister(key,
[id](char * buffer, size_t len) {
snprintf_P(buffer, len, PSTR("%d"), lightChannel(id));
},
[id](const char * payload) {
lightChannel(id, atoi(payload));
lightUpdate(true, true);
}
);
}
}
#endif // WEB_SUPPORT
@ -987,6 +965,7 @@ void _lightConfigure() {
_light_use_gamma = getSetting("useGamma", LIGHT_USE_GAMMA).toInt() == 1;
_light_use_transitions = getSetting("useTransitions", LIGHT_USE_TRANSITIONS).toInt() == 1;
_light_transition_time = getSetting("lightTime", LIGHT_TRANSITION_TIME).toInt();
}
@ -1046,13 +1025,14 @@ void lightSetup() {
DEBUG_MSG_P(PSTR("[LIGHT] LIGHT_PROVIDER = %d\n"), LIGHT_PROVIDER);
DEBUG_MSG_P(PSTR("[LIGHT] Number of channels: %d\n"), _light_channel.size());
_lightColorRestore();
_lightConfigure();
_lightColorRestore();
#if WEB_SUPPORT
_lightAPISetup();
wsOnSendRegister(_lightWebSocketOnSend);
wsOnActionRegister(_lightWebSocketOnAction);
wsOnReceiveRegister(_lightWebSocketOnReceive);
wsOnAfterParseRegister([]() {
#if LIGHT_SAVE_ENABLED == 0
lightSave();


+ 5
- 0
code/espurna/mqtt.ino View File

@ -298,6 +298,10 @@ unsigned long _mqttNextMessageId() {
#if WEB_SUPPORT
bool _mqttWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "mqtt", 3) == 0);
}
void _mqttWebSocketOnSend(JsonObject& root) {
root["mqttVisible"] = 1;
root["mqttStatus"] = mqttConnected();
@ -795,6 +799,7 @@ void mqttSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_mqttWebSocketOnSend);
wsOnAfterParseRegister(_mqttConfigure);
wsOnReceiveRegister(_mqttWebSocketOnReceive);
#endif
#if TERMINAL_SUPPORT


+ 9
- 0
code/espurna/nofuss.ino View File

@ -18,12 +18,20 @@ bool _nofussEnabled = false;
// NOFUSS
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
bool _nofussWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "nofuss", 6) == 0);
}
void _nofussWebSocketOnSend(JsonObject& root) {
root["nofussVisible"] = 1;
root["nofussEnabled"] = getSetting("nofussEnabled", NOFUSS_ENABLED).toInt() == 1;
root["nofussServer"] = getSetting("nofussServer", NOFUSS_SERVER);
}
#endif
void _nofussConfigure() {
String nofussServer = getSetting("nofussServer", NOFUSS_SERVER);
@ -147,6 +155,7 @@ void nofussSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_nofussWebSocketOnSend);
wsOnAfterParseRegister(_nofussConfigure);
wsOnReceiveRegister(_nofussWebSocketOnReceive);
#endif
#if TERMINAL_SUPPORT


+ 9
- 0
code/espurna/ntp.ino View File

@ -21,6 +21,12 @@ bool _ntp_configure = false;
// NTP
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
bool _ntpWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "ntp", 3) == 0);
}
void _ntpWebSocketOnSend(JsonObject& root) {
root["ntpVisible"] = 1;
root["ntpStatus"] = (timeStatus() == timeSet);
@ -31,6 +37,8 @@ void _ntpWebSocketOnSend(JsonObject& root) {
if (ntpSynced()) root["now"] = now();
}
#endif
void _ntpStart() {
_ntp_start = 0;
@ -159,6 +167,7 @@ void ntpSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_ntpWebSocketOnSend);
wsOnReceiveRegister(_ntpWebSocketOnReceive);
wsOnAfterParseRegister([]() { _ntp_configure = true; });
#endif


+ 5
- 0
code/espurna/relay.ino View File

@ -498,6 +498,10 @@ void _relayConfigure() {
#if WEB_SUPPORT
bool _relayWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "relay", 5) == 0);
}
void _relayWebSocketUpdate(JsonObject& root) {
JsonArray& relay = root.createNestedArray("relayStatus");
for (unsigned char i=0; i<relayCount(); i++) {
@ -577,6 +581,7 @@ void relaySetupWS() {
wsOnSendRegister(_relayWebSocketOnStart);
wsOnActionRegister(_relayWebSocketOnAction);
wsOnAfterParseRegister(_relayConfigure);
wsOnReceiveRegister(_relayWebSocketOnReceive);
}
#endif // WEB_SUPPORT


+ 5
- 0
code/espurna/scheduler.ino View File

@ -15,6 +15,10 @@ Adapted by Xose Pérez <xose dot perez at gmail dot com>
#if WEB_SUPPORT
bool _schWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "sch", 3) == 0);
}
void _schWebSocketOnSend(JsonObject &root){
if (relayCount() > 0) {
@ -201,6 +205,7 @@ void schSetup() {
// Update websocket clients
#if WEB_SUPPORT
wsOnSendRegister(_schWebSocketOnSend);
wsOnReceiveRegister(_schWebSocketOnReceive);
wsOnAfterParseRegister(_schConfigure);
#endif


+ 15
- 2
code/espurna/sensor.ino View File

@ -93,6 +93,15 @@ double _magnitudeProcess(unsigned char type, double value) {
#if WEB_SUPPORT
bool _sensorWebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "pwr", 3) == 0) return true;
if (strncmp(key, "sns", 3) == 0) return true;
if (strncmp(key, "tmp", 3) == 0) return true;
if (strncmp(key, "hum", 3) == 0) return true;
if (strncmp(key, "energy", 6) == 0) return true;
return false;
}
void _sensorWebSocketSendData(JsonObject& root) {
char buffer[10];
@ -168,7 +177,7 @@ void _sensorWebSocketStart(JsonObject& root) {
if (_magnitudes.size() > 0) {
root["sensorsVisible"] = 1;
//root["apiRealTime"] = _sensor_realtime;
root["powerUnits"] = _sensor_power_units;
root["pwrUnits"] = _sensor_power_units;
root["energyUnits"] = _sensor_energy_units;
root["tmpUnits"] = _sensor_temperature_units;
root["tmpCorrection"] = _sensor_temperature_correction;
@ -587,7 +596,7 @@ void _sensorConfigure() {
_sensor_read_interval = 1000 * constrain(getSetting("snsRead", SENSOR_READ_INTERVAL).toInt(), SENSOR_READ_MIN_INTERVAL, SENSOR_READ_MAX_INTERVAL);
_sensor_report_every = constrain(getSetting("snsReport", SENSOR_REPORT_EVERY).toInt(), SENSOR_REPORT_MIN_EVERY, SENSOR_REPORT_MAX_EVERY);
_sensor_realtime = getSetting("apiRealTime", API_REAL_TIME_VALUES).toInt() == 1;
_sensor_power_units = getSetting("powerUnits", SENSOR_POWER_UNITS).toInt();
_sensor_power_units = getSetting("pwrUnits", SENSOR_POWER_UNITS).toInt();
_sensor_energy_units = getSetting("energyUnits", SENSOR_ENERGY_UNITS).toInt();
_sensor_temperature_units = getSetting("tmpUnits", SENSOR_TEMPERATURE_UNITS).toInt();
_sensor_temperature_correction = getSetting("tmpCorrection", SENSOR_TEMPERATURE_CORRECTION).toFloat();
@ -774,6 +783,9 @@ String magnitudeUnits(unsigned char type) {
void sensorSetup() {
// Backwards compatibility
moveSetting("powerUnits", "pwrUnits");
// Load sensors
_sensorLoad();
_sensorInit();
@ -785,6 +797,7 @@ void sensorSetup() {
// Websockets
wsOnSendRegister(_sensorWebSocketStart);
wsOnReceiveRegister(_sensorWebSocketOnReceive);
wsOnSendRegister(_sensorWebSocketSendData);
wsOnAfterParseRegister(_sensorConfigure);


+ 3047
- 351
code/espurna/static/index.html.gz.h
File diff suppressed because it is too large
View File


+ 9
- 0
code/espurna/telnet.ino View File

@ -20,11 +20,19 @@ bool _telnetFirst = true;
// Private methods
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
bool _telnetWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "telnet", 6) == 0);
}
void _telnetWebSocketOnSend(JsonObject& root) {
root["telnetVisible"] = 1;
root["telnetSTA"] = getSetting("telnetSTA", TELNET_STA).toInt() == 1;
}
#endif
void _telnetDisconnect(unsigned char clientId) {
_telnetClients[clientId]->free();
_telnetClients[clientId] = NULL;
@ -169,6 +177,7 @@ void telnetSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_telnetWebSocketOnSend);
wsOnReceiveRegister(_telnetWebSocketOnReceive);
#endif
DEBUG_MSG_P(PSTR("[TELNET] Listening on port %d\n"), TELNET_PORT);


+ 9
- 0
code/espurna/thinkspeak.ino View File

@ -32,6 +32,12 @@ unsigned long _tspk_last_flush = 0;
// -----------------------------------------------------------------------------
#if WEB_SUPPORT
bool _tspkWebSocketOnReceive(const char * key, JsonVariant& value) {
return (strncmp(key, "tspk", 4) == 0);
}
void _tspkWebSocketOnSend(JsonObject& root) {
unsigned char visible = 0;
@ -61,6 +67,8 @@ void _tspkWebSocketOnSend(JsonObject& root) {
}
#endif
void _tspkConfigure() {
_tspk_enabled = getSetting("tspkEnabled", THINGSPEAK_ENABLED).toInt() == 1;
if (_tspk_enabled && (getSetting("tspkKey").length() == 0)) {
@ -248,6 +256,7 @@ void tspkSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_tspkWebSocketOnSend);
wsOnAfterParseRegister(_tspkConfigure);
wsOnReceiveRegister(_tspkWebSocketOnReceive);
#endif
DEBUG_MSG_P(PSTR("[THINGSPEAK] Async %s, SSL %s\n"),


+ 12
- 0
code/espurna/wifi.ino View File

@ -290,6 +290,17 @@ void _wifiInitCommands() {
#if WEB_SUPPORT
bool _wifiWebSocketOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "wifi", 4) == 0) return true;
if (strncmp(key, "ssid", 4) == 0) return true;
if (strncmp(key, "pass", 4) == 0) return true;
if (strncmp(key, "ip", 2) == 0) return true;
if (strncmp(key, "gw", 2) == 0) return true;
if (strncmp(key, "mask", 4) == 0) return true;
if (strncmp(key, "dns", 3) == 0) return true;
return false;
}
void _wifiWebSocketOnSend(JsonObject& root) {
root["maxNetworks"] = WIFI_MAX_NETWORKS;
root["wifiScan"] = getSetting("wifiScan", WIFI_SCAN_NETWORKS).toInt() == 1;
@ -417,6 +428,7 @@ void wifiSetup() {
#if WEB_SUPPORT
wsOnSendRegister(_wifiWebSocketOnSend);
wsOnReceiveRegister(_wifiWebSocketOnReceive);
wsOnAfterParseRegister(_wifiConfigure);
wsOnActionRegister(_wifiWebSocketOnAction);
#endif


+ 27
- 0
code/espurna/ws.ino View File

@ -21,6 +21,7 @@ Ticker _web_defer;
std::vector<ws_on_send_callback_f> _ws_on_send_callbacks;
std::vector<ws_on_action_callback_f> _ws_on_action_callbacks;
std::vector<ws_on_after_parse_callback_f> _ws_on_after_parse_callbacks;
std::vector<ws_on_receive_callback_f> _ws_on_receive_callbacks;
// -----------------------------------------------------------------------------
// Private methods
@ -170,6 +171,19 @@ void _wsParse(AsyncWebSocketClient *client, uint8_t * payload, size_t length) {
continue;
}
// Check if key has to be processed
bool found = false;
for (unsigned char i = 0; i < _ws_on_receive_callbacks.size(); i++) {
found |= (_ws_on_receive_callbacks[i])(key.c_str(), value);
// TODO: remove this to call all OnReceiveCallbacks with the
// current key/value
if (found) break;
}
if (!found) {
delSetting(key);
continue;
}
// Store values
if (value.is<JsonArray&>()) {
if (_wsStore(key, value.as<JsonArray&>())) changed = true;
@ -227,6 +241,14 @@ void _wsUpdate(JsonObject& root) {
#endif
}
bool _wsOnReceive(const char * key, JsonVariant& value) {
if (strncmp(key, "ws", 2) == 0) return true;
if (strncmp(key, "admin", 5) == 0) return true;
if (strncmp(key, "hostname", 8) == 0) return true;
if (strncmp(key, "webPort", 7) == 0) return true;
return false;
}
void _wsOnStart(JsonObject& root) {
#if USE_PASSWORD && WEB_FORCE_PASS_CHANGE
@ -340,6 +362,10 @@ void wsOnSendRegister(ws_on_send_callback_f callback) {
_ws_on_send_callbacks.push_back(callback);
}
void wsOnReceiveRegister(ws_on_receive_callback_f callback) {
_ws_on_receive_callbacks.push_back(callback);
}
void wsOnActionRegister(ws_on_action_callback_f callback) {
_ws_on_action_callbacks.push_back(callback);
}
@ -411,6 +437,7 @@ void wsSetup() {
mqttRegister(_wsMQTTCallback);
#endif
wsOnSendRegister(_wsOnStart);
wsOnReceiveRegister(_wsOnReceive);
wsOnAfterParseRegister(wsConfigure);
espurnaRegisterLoop(_wsLoop);
}


+ 11
- 3
code/html/index.html View File

@ -408,7 +408,7 @@
<div class="pure-u-1 pure-u-lg-1-4"><input type="checkbox" name="useWhite" action="reload" tabindex="9" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Use forth dimmable channel as white when first 3 have the same RGB value.<br />Will only work if the device has at least 4 dimmable channels.<br />Reload the page to update the web interface.</div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Use forth dimmable channel as white when first 3 have the same RGB value.<br />Will only work if the device has at least 4 dimmable channels.<br />Enabling this will render useless the "Channel 4" slider in the status page.<br />Reload the page to update the web interface.</div>
</div>
<div class="pure-g">
@ -435,9 +435,17 @@
<div class="pure-u-1 pure-u-lg-3-4 hint">If enabled color changes will be smoothed.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Transition time</label>
<div class="pure-u-1 pure-u-lg-1-4"><input class="pure-u-1" type="number" name="lightTime" min="10" max="5000" tabindex="13" /></div>
<div class="pure-u-0 pure-u-lg-1-2"></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Time in millisecons to transition from one color to another.</div>
</div>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1-4"><label>MQTT group</label></div>
<div class="pure-u-1 pure-u-lg-3-4"><input name="mqttGroupColor" class="pure-u-1" tabindex="13" action="reconnect" /></div>
<div class="pure-u-1 pure-u-lg-3-4"><input name="mqttGroupColor" class="pure-u-1" tabindex="14" action="reconnect" /></div>
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">Sync color between different lights.</div>
</div>
@ -1047,7 +1055,7 @@
<div class="pure-g module module-pwr">
<label class="pure-u-1 pure-u-lg-1-4">Power units</label>
<select name="powerUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
<select name="pwrUnits" tabindex="16" class="pure-u-1 pure-u-lg-1-4">
<option value="0">Watts (W)</option>
<option value="1">Kilowatts (kW)</option>
</select>


+ 25
- 1
code/platformio.ini View File

@ -26,7 +26,7 @@ lib_deps =
https://github.com/madpilot/mDNSResolver#4cfcda1
https://github.com/xoseperez/my92xx#3.0.1
https://bitbucket.org/xoseperez/nofuss.git#0.2.5
https://github.com/gmag11/NtpClient.git#28af3a5
https://github.com/xoseperez/NtpClient.git#0016a59
OneWire
PMS Library
PZEM004T
@ -1747,6 +1747,30 @@ upload_port = "${env.ESPURNA_IP}"
upload_flags = --auth=${env.ESPURNA_AUTH} --port 8266
extra_scripts = ${common.extra_scripts}
[env:arniex-swifitch]
platform = ${common.platform}
framework = arduino
board = esp12e
board_flash_mode = dout
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
build_flags = ${common.build_flags_1m} -DARNIEX_SWIFITCH
extra_scripts = ${common.extra_scripts}
monitor_baud = 115200
[env:arniex-swifitch-ota]
platform = ${common.platform}
framework = arduino
board = esp12e
board_flash_mode = dout
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
build_flags = ${common.build_flags_1m} -DARNIEX_SWIFITCH
upload_speed = 115200
upload_port = "${env.ESPURNA_IP}"
upload_flags = --auth=${env.ESPURNA_AUTH} --port 8266
extra_scripts = ${common.extra_scripts}
# ------------------------------------------------------------------------------
# GENERIC OTA ENVIRONMENTS
# ------------------------------------------------------------------------------


BIN
images/devices/swifitch.png View File

Before After
Width: 3356  |  Height: 2780  |  Size: 321 KiB

Loading…
Cancel
Save