Browse Source

Merge branch 'dev' into inching

Conflicts:
	code/espurna/button.ino
	code/espurna/data/index.html.gz
	code/html/index.html
fastled
Xose Pérez 7 years ago
parent
commit
44ffb3fe6c
34 changed files with 424 additions and 151 deletions
  1. +19
    -0
      CHANGELOG.md
  2. +3
    -2
      README.md
  3. +1
    -1
      code/debug
  4. +53
    -11
      code/espurna/button.ino
  5. +1
    -0
      code/espurna/config/all.h
  6. +43
    -0
      code/espurna/config/arduino.h
  7. +13
    -2
      code/espurna/config/general.h
  8. +46
    -31
      code/espurna/config/hardware.h
  9. +8
    -2
      code/espurna/config/sensors.h
  10. +1
    -1
      code/espurna/config/version.h
  11. BIN
      code/espurna/data/index.html.gz
  12. BIN
      code/espurna/data/script.js.gz
  13. +1
    -2
      code/espurna/dht.ino
  14. +1
    -2
      code/espurna/domoticz.ino
  15. +2
    -3
      code/espurna/ds18b20.ino
  16. +1
    -2
      code/espurna/emon.ino
  17. +2
    -3
      code/espurna/espurna.ino
  18. +2
    -3
      code/espurna/fauxmo.ino
  19. +3
    -4
      code/espurna/led.ino
  20. +5
    -3
      code/espurna/mqtt.ino
  21. +1
    -2
      code/espurna/nofuss.ino
  22. +1
    -2
      code/espurna/ntp.ino
  23. +4
    -2
      code/espurna/ota.ino
  24. +80
    -25
      code/espurna/pow.ino
  25. +2
    -3
      code/espurna/relay.ino
  26. +1
    -2
      code/espurna/rf.ino
  27. +1
    -2
      code/espurna/settings.ino
  28. +48
    -26
      code/espurna/web.ino
  29. +1
    -7
      code/espurna/wifi.ino
  30. +1
    -1
      code/gulpfile.js
  31. +5
    -0
      code/html/custom.js
  32. +49
    -3
      code/html/index.html
  33. +25
    -4
      code/platformio.ini
  34. BIN
      images/devices/jangoe-wifi-relay.png

+ 19
- 0
CHANGELOG.md View File

@ -3,7 +3,26 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.4.4] 2017-01-13
### Added
- Adding current, voltage, apparent and reactive power reports to Sonoff POW (Web & MQTT)
### Fixed
- #35 Fixed frequent MQTT connection drops after WIFI reconnect
- Defer wifi disconnection from web interface to allow request to return
### Changed
- Move all Arduino IDE configuration values to their own file
- Using latest HLW8012 library in interrupt mode
## [1.4.3] 2017-01-11
### Fixed
- #6 Using forked Time library to prevent conflict with Arduino Core for ESP8266 time.h file in windows machines
## [1.4.2] 2017-01-09
### Added
- Support for inverse logic relays
### Fixed
- Fixed error in relay identification from MQTT messages (issue #31)


+ 3
- 2
README.md View File

@ -4,7 +4,7 @@ ESPurna ("spark" in Catalan) is a custom firmware for ESP8266 based smart switch
It was originally developed with the **[IteadStudio Sonoff](https://www.itead.cc/sonoff-wifi-wireless-switch.html)** in mind but now it supports a growing number of ESP8266-based boards.
It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries.
**Current Release Version is 1.4.2**, read the [changelog](CHANGELOG.md).
**Current Release Version is 1.4.4**, read the [changelog](CHANGELOG.md).
## Features
@ -44,11 +44,12 @@ For more information please refer to the [ESPurna Wiki](https://bitbucket.org/xo
|![IteadStudio Sonoff Dual](images/devices/sonoff-dual.jpg) **IteadStudio Sonoff Dual**|![IteadStudio Sonoff POW](images/devices/sonoff-pow.jpg) **IteadStudio Sonoff POW**|![IteadStudio Sonoff TH10/TH16](images/devices/sonoff-th10-th16.jpg) **IteadStudio Sonoff TH10/TH16**|
|![IteadStudio Sonoff RF](images/devices/sonoff-rf.jpg) **IteadStudio Sonoff RF**|![IteadStudio Sonoff SV](images/devices/sonoff-sv.jpg) **IteadStudio Sonoff SV**|![IteadStudio Sonoff Touch](images/devices/sonoff-touch.jpg) **IteadStudio Sonoff Touch**|
|![Wemos D1 Mini Relay Shield](images/devices/d1mini.jpg) **Wemos D1 Mini Relay Shield**|![Electrodragon Relay Board](images/devices/electrodragon-relay-board.jpg) **Electrodragon Relay Board**|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg) **WorkChoice EcoPlug**|
|![JanGoe Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.png) **JanGoe Wifi Relay (NO/NC)**|||
## License
Copyright (C) 2016 by Xose Pérez (@xoseperez)
Copyright (C) 2016-2017 by Xose Pérez (@xoseperez)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by


+ 1
- 1
code/debug View File

@ -4,7 +4,7 @@
# CONFIGURATION
# ------------------------------------------------------------------------------
ENVIRONMENT="node-debug"
ENVIRONMENT="d1-debug"
ADDR2LINE=$HOME/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-addr2line
DECODER=utils/EspStackTraceDecoder.jar
DECODER_ORIGIN=https://github.com/littleyoda/EspStackTraceDecoder/releases/download/untagged-83b6db3208da17a0f1fd/EspStackTraceDecoder.jar


+ 53
- 11
code/espurna/button.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
BUTTON MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -13,6 +12,16 @@ Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
#ifdef SONOFF_DUAL
#ifdef MQTT_BUTTON_TOPIC
void buttonMQTT(unsigned char id) {
String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER);
char buffer[strlen(MQTT_BUTTON_TOPIC) + mqttGetter.length() + 3];
sprintf(buffer, "%s/%d%s", MQTT_BUTTON_TOPIC, id, mqttGetter.c_str());
mqttSend(buffer, 1);
mqttSend(buffer, 0);
}
#endif
void buttonSetup() {}
void buttonLoop() {
@ -30,7 +39,12 @@ void buttonLoop() {
// Since we are not passing back RELAY2 value
// (in the relayStatus method) it will only be present
// here if it has actually been pressed
if ((value & 4) == 4) value = value ^ 1;
if ((value & 4) == 4) {
value = value ^ 1;
#ifdef MQTT_BUTTON_TOPIC
buttonMQTT(0);
#endif
}
// Otherwise check if any of the other two BUTTONs
// (in the header) has been pressent, but we should
@ -60,21 +74,36 @@ void buttonLoop() {
#include <DebounceEvent.h>
#include <vector>
std::vector<DebounceEvent *> _buttons;
typedef struct {
DebounceEvent * button;
unsigned int relayID;
} button_t;
std::vector<button_t> _buttons;
#ifdef MQTT_BUTTON_TOPIC
void buttonMQTT(unsigned char id) {
if (id >= _buttons.size()) return;
String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER);
char buffer[strlen(MQTT_BUTTON_TOPIC) + mqttGetter.length() + 3];
sprintf(buffer, "%s/%d%s", MQTT_BUTTON_TOPIC, id, mqttGetter.c_str());
mqttSend(buffer, _buttons[id].button->pressed() ? "1" : "0");
}
#endif
void buttonSetup() {
#ifdef BUTTON1_PIN
_buttons.push_back(new DebounceEvent(BUTTON1_PIN));
_buttons.push_back({new DebounceEvent(BUTTON1_PIN), BUTTON1_RELAY});
#endif
#ifdef BUTTON2_PIN
_buttons.push_back(new DebounceEvent(BUTTON2_PIN));
_buttons.push_back({new DebounceEvent(BUTTON2_PIN), BUTTON2_RELAY});
#endif
#ifdef BUTTON3_PIN
_buttons.push_back(new DebounceEvent(BUTTON3_PIN));
_buttons.push_back({new DebounceEvent(BUTTON3_PIN), BUTTON3_RELAY});
#endif
#ifdef BUTTON4_PIN
_buttons.push_back(new DebounceEvent(BUTTON4_PIN));
_buttons.push_back({new DebounceEvent(BUTTON4_PIN), BUTTON4_RELAY});
#endif
#ifdef LED_PULSE
@ -90,20 +119,33 @@ void buttonSetup() {
void buttonLoop() {
for (unsigned int i=0; i < _buttons.size(); i++) {
if (_buttons[i]->loop()) {
uint8_t event = _buttons[i]->getEvent();
if (_buttons[i].button->loop()) {
uint8_t event = _buttons[i].button->getEvent();
DEBUG_MSG("[BUTTON] Pressed #%d, event: %d\n", i, event);
#ifdef MQTT_BUTTON_TOPIC
buttonMQTT(i);
#endif
if (i == 0) {
if (event == EVENT_DOUBLE_CLICK) createAP();
if (event == EVENT_LONG_CLICK) ESP.reset();
}
#ifdef ITEAD_1CH_INCHING
if (i == 1) {
relayPulseToggle();
continue;
}
#endif
if (event == EVENT_SINGLE_CLICK) relayToggle(i);
if (event == EVENT_SINGLE_CLICK) {
if (_buttons[i].relayID > 0) {
relayToggle(_buttons[i].relayID - 1);
}
}
}
}


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

@ -1,4 +1,5 @@
#include "version.h"
#include "arduino.h"
#include "debug.h"
#include "general.h"
#include "hardware.h"


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

@ -0,0 +1,43 @@
//--------------------------------------------------------------------------------
// These settings are normally provided by PlatformIO
// Uncomment the appropiate line(s) to build from the Arduino IDE
//--------------------------------------------------------------------------------
//--------------------------------------------------------------------------------
// General
//--------------------------------------------------------------------------------
//#define DEBUG_PORT Serial
//--------------------------------------------------------------------------------
// Hardware
//--------------------------------------------------------------------------------
//#define D1_RELAYSHIELD
//#define NODEMCUV2
//#define SONOFF
//#define SONOFF_TH
//#define SLAMPHER
//#define S20
//#define SONOFF_SV
//#define SONOFF_POW
//#define SONOFF_DUAL
//#define SONOFF_4CH
//#define ESP_RELAY_BOARD
//#define ECOPLUG
//#define WIFI_RELAY_NC
//#define WIFI_RELAY_NO
//#define ESPURNA
//--------------------------------------------------------------------------------
// Features (values below are default values)
//--------------------------------------------------------------------------------
//#define ENABLE_DHT 0
//#define ENABLE_DS18B20 0
//#define ENABLE_EMON 0
//#define ENABLE_HLW8018 0
//#define ENABLE_RF 0
//#define ENABLE_FAUXMO 1
//#define ENABLE_NOFUSS 0
//#define ENABLE_DOMOTICZ 1

+ 13
- 2
code/espurna/config/general.h View File

@ -80,6 +80,7 @@
#define MQTT_SKIP_TIME 1000
#define MQTT_RELAY_TOPIC "/relay"
#define MQTT_LED_TOPIC "/led"
#define MQTT_BUTTON_TOPIC "/button"
#define MQTT_IP_TOPIC "/ip"
#define MQTT_VERSION_TOPIC "/version"
#define MQTT_FSVERSION_TOPIC "/fsversion"
@ -99,7 +100,9 @@
// DOMOTICZ
// -----------------------------------------------------------------------------
#define ENABLE_DOMOTICZ 1
#ifndef ENABLE_DOMOTICZ
#define ENABLE_DOMOTICZ 1
#endif
#define DOMOTICZ_IN_TOPIC "domoticz/in"
#define DOMOTICZ_OUT_TOPIC "domoticz/out"
@ -113,7 +116,15 @@
#define NTP_UPDATE_INTERVAL 1800
// -----------------------------------------------------------------------------
// FAUXO
// FAUXMO
// -----------------------------------------------------------------------------
// This setting defines whether Alexa support should be built into the firmware
#ifndef ENABLE_FAUXMO
#define ENABLE_FAUXMO 1
#endif
// This is default value for the fauxmoEnabled setting that defines whether
// this device should be discoberable and respond to Alexa commands.
// Both ENABLE_FAUXMO and fauxmoEnabled should be 1 for Alexa support to work.
#define FAUXMO_ENABLED 1

+ 46
- 31
code/espurna/config/hardware.h View File

@ -1,34 +1,3 @@
//--------------------------------------------------------------------------------
// HARDWARE
// This setting is normally provided by PlatformIO
// Uncomment the appropiate line to build from the Arduino IDE
//--------------------------------------------------------------------------------
//#define NODEMCUV2
//#define SONOFF
//#define SONOFF_TH
//#define SLAMPHER
//#define S20
//#define SONOFF_SV
//#define SONOFF_POW
//#define SONOFF_DUAL
//#define SONOFF_4CH
//#define ESP_RELAY_BOARD
//#define ECOPLUG
//#define ESPURNA
//#define ENABLE_DHT 1
//#define ENABLE_DS18B20 1
//#define ENABLE_EMON 1
//#define ENABLE_HLW8018 1
//#define ENABLE_RF 1
//#define ENABLE_FAUXMO 0
//#define ENABLE_NOFUSS 1
#ifndef ENABLE_FAUXMO
#define ENABLE_FAUXMO 1
#endif
// -----------------------------------------------------------------------------
// Development boards
// -----------------------------------------------------------------------------
@ -38,6 +7,7 @@
#define MANUFACTURER "NODEMCU"
#define DEVICE "LOLIN"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 2
@ -61,6 +31,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -71,6 +42,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_TH"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -81,6 +53,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_SV"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -91,6 +64,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SLAMPHER"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -101,6 +75,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "S20"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -111,6 +86,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_TOUCH"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13
@ -121,6 +97,7 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_POW"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 15
@ -142,9 +119,13 @@
#define MANUFACTURER "ITEAD"
#define DEVICE "SONOFF_4CH"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 9
#define BUTTON2_RELAY 2
#define BUTTON3_PIN 10
#define BUTTON3_RELAY 3
#define BUTTON4_PIN 14
#define BUTTON4_RELAY 4
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define RELAY2_PIN 5
@ -180,7 +161,9 @@
#define MANUFACTURER "ELECTRODRAGON"
#define DEVICE "ESP_RELAY_BOARD"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 2
#define BUTTON2_RELAY 2
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define RELAY2_PIN 13
@ -197,11 +180,42 @@
#define MANUFACTURER "WORKCHOICE"
#define DEVICE "ECOPLUG"
#define BUTTON1_PIN 13
#define BUTTON1_RELAY 1
#define RELAY1_PIN 15
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 2
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// JanGoe Wifi Relay (https://github.com/JanGoe/esp8266-wifi-relay)
// -----------------------------------------------------------------------------
#elif defined(WIFI_RELAY_NC)
#define MANUFACTURER "JANGOE"
#define DEVICE "WIFI_RELAY_NC"
#define BUTTON1_PIN 12
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 13
#define BUTTON2_RELAY 2
#define RELAY1_PIN 2
#define RELAY1_PIN_INVERSE 1
#define RELAY2_PIN 14
#define RELAY2_PIN_INVERSE 1
#elif defined(WIFI_RELAY_NO)
#define MANUFACTURER "JANGOE"
#define DEVICE "WIFI_RELAY_NO"
#define BUTTON1_PIN 12
#define BUTTON1_RELAY 1
#define BUTTON2_PIN 13
#define BUTTON2_RELAY 2
#define RELAY1_PIN 2
#define RELAY1_PIN_INVERSE 0
#define RELAY2_PIN 14
#define RELAY2_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// ESPurna board (still beta)
// -----------------------------------------------------------------------------
@ -211,6 +225,7 @@
#define MANUFACTURER "TINKERMAN"
#define DEVICE "ESPURNA"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 13


+ 8
- 2
code/espurna/config/sensors.h View File

@ -61,5 +61,11 @@
#define POW_VOLTAGE_R_UP ( 5 * 470000 ) // Real: 2280k
#define POW_VOLTAGE_R_DOWN ( 1000 ) // Real 1.009k
#define POW_POWER_TOPIC "/power"
#define POW_UPDATE_INTERVAL 10000
#define POW_REPORT_EVERY 6
#define POW_CURRENT_TOPIC "/current"
#define POW_VOLTAGE_TOPIC "/voltage"
#define POW_APOWER_TOPIC "/apower"
#define POW_RPOWER_TOPIC "/rpower"
#define POW_PFACTOR_TOPIC "/pfactor"
#define POW_ENERGY_TOPIC "/energy"
#define POW_UPDATE_INTERVAL 5000
#define POW_REPORT_EVERY 12

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

@ -1,4 +1,4 @@
#define APP_NAME "ESPurna"
#define APP_VERSION "1.4.2"
#define APP_VERSION "1.4.4"
#define APP_AUTHOR "xose.perez@gmail.com"
#define APP_WEBSITE "http://tinkerman.cat"

BIN
code/espurna/data/index.html.gz View File


BIN
code/espurna/data/script.js.gz View File


+ 1
- 2
code/espurna/dht.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
DHT MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 1
- 2
code/espurna/domoticz.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
DOMOTICZ MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 2
- 3
code/espurna/ds18b20.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
SD18B20 MODULE
DS18B20 MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 1
- 2
code/espurna/emon.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
EMON MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 2
- 3
code/espurna/espurna.ino View File

@ -1,7 +1,8 @@
/*
ESPurna
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -175,6 +176,4 @@ void loop() {
powerMonitorLoop();
#endif
yield();
}

+ 2
- 3
code/espurna/fauxmo.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
DHT MODULE
FAUXMO MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 3
- 4
code/espurna/led.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
LED MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -50,9 +49,9 @@ void ledBlink(unsigned char id, unsigned long delayOff, unsigned long delayOn) {
void showStatus() {
if (wifiConnected()) {
if (WiFi.getMode() == WIFI_AP) {
ledBlink(0, 2000, 2000);
ledBlink(0, 2500, 2500);
} else {
ledBlink(0, 5000, 500);
ledBlink(0, 4900, 100);
}
} else {
ledBlink(0, 500, 500);


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

@ -1,9 +1,8 @@
/*
ESPurna
MQTT MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -43,7 +42,7 @@ unsigned int mqttTopicRootLength() {
void mqttSendRaw(const char * topic, const char * message) {
if (mqtt.connected()) {
DEBUG_MSG("[MQTT] Sending %s %s\n", topic, message);
DEBUG_MSG("[MQTT] Sending %s => %s\n", topic, message);
mqtt.publish(topic, MQTT_QOS, MQTT_RETAIN, message);
}
}
@ -131,6 +130,8 @@ void mqttConnect() {
if (!mqtt.connected()) {
mqtt.disconnect();
char * host = strdup(getSetting("mqttServer", MQTT_SERVER).c_str());
if (strlen(host) == 0) return;
unsigned int port = getSetting("mqttPort", MQTT_PORT).toInt();
@ -140,6 +141,7 @@ void mqttConnect() {
DEBUG_MSG("[MQTT] Connecting to broker at %s", host);
mqtt.setServer(host, port);
mqtt.setKeepAlive(MQTT_KEEPALIVE).setCleanSession(false);
mqtt.setWill(MQTT_HEARTBEAT_TOPIC, MQTT_QOS, MQTT_RETAIN, "0");
if ((strlen(user) > 0) && (strlen(pass) > 0)) {
DEBUG_MSG(" as user '%s'.", user);
mqtt.setCredentials(user, pass);


+ 1
- 2
code/espurna/nofuss.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
NOFUSS MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 1
- 2
code/espurna/ntp.ino View File

@ -1,9 +1,8 @@
/*
ESPURNA
NTP MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 4
- 2
code/espurna/ota.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
OTA MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -25,10 +24,13 @@ void otaSetup() {
ArduinoOTA.onStart([]() {
DEBUG_MSG("[OTA] Start\n");
wsSend("{\"message\": \"OTA update started\"}");
});
ArduinoOTA.onEnd([]() {
DEBUG_MSG("\n[OTA] End\n");
wsSend("{\"action\": \"reload\"}");
delay(100);
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {


+ 80
- 25
code/espurna/pow.ino View File

@ -1,10 +1,9 @@
/*
ESPurna
POW MODULE
Support for Sonoff POW HLW8012-based power monitor
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -12,6 +11,8 @@ Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
#include <HLW8012.h>
#define POW_USE_INTERRUPTS 1
HLW8012 hlw8012;
// -----------------------------------------------------------------------------
@ -29,17 +30,19 @@ void hlw8012_cf_interrupt() {
}
void powAttachInterrupts() {
//attachInterrupt(POW_CF1_PIN, hlw8012_cf1_interrupt, CHANGE);
attachInterrupt(POW_CF1_PIN, hlw8012_cf1_interrupt, CHANGE);
attachInterrupt(POW_CF_PIN, hlw8012_cf_interrupt, CHANGE);
DEBUG_MSG("[POW] Enabled\n");
}
void powDettachInterrupts() {
//detachInterrupt(POW_CF1_PIN);
detachInterrupt(POW_CF1_PIN);
detachInterrupt(POW_CF_PIN);
DEBUG_MSG("[POW] Disabled\n");
}
// -----------------------------------------------------------------------------
void powSaveCalibration() {
setSetting("powPowerMult", hlw8012.getPowerMultiplier());
setSetting("powCurrentMult", hlw8012.getCurrentMultiplier());
@ -77,6 +80,13 @@ void powSetExpectedVoltage(unsigned int voltage) {
}
}
void powReset() {
hlw8012.resetMultipliers();
powSaveCalibration();
}
// -----------------------------------------------------------------------------
unsigned int getActivePower() {
return hlw8012.getActivePower();
}
@ -85,6 +95,10 @@ unsigned int getApparentPower() {
return hlw8012.getApparentPower();
}
unsigned int getReactivePower() {
return hlw8012.getReactivePower();
}
double getCurrent() {
return hlw8012.getCurrent();
}
@ -97,6 +111,8 @@ unsigned int getPowerFactor() {
return (int) (100 * hlw8012.getPowerFactor());
}
// -----------------------------------------------------------------------------
void powSetup() {
// Initialize HLW8012
@ -105,7 +121,11 @@ void powSetup() {
// * currentWhen is the value in sel_pin to select current sampling
// * set use_interrupts to true to use interrupts to monitor pulse widths
// * leave pulse_timeout to the default value, recommended when using interrupts
hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, false, 1000000);
#if POW_USE_INTERRUPTS
hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, true);
#else
hlw8012.begin(POW_CF_PIN, POW_CF1_PIN, POW_SEL_PIN, POW_SEL_CURRENT, false, 1000000);
#endif
// These values are used to calculate current, voltage and power factors as per datasheet formula
// These are the nominal values for the Sonoff POW resistors:
@ -114,25 +134,13 @@ void powSetup() {
// * The VOLTAGE_RESISTOR_DOWNSTREAM is the 1kOhm resistor in the voltage divider that feeds the V2P pin in the HLW8012
hlw8012.setResistors(POW_CURRENT_R, POW_VOLTAGE_R_UP, POW_VOLTAGE_R_DOWN);
// Retrieve calibration values
powRetrieveCalibration();
/*
static WiFiEventHandler e1 = WiFi.onStationModeDisconnected([](const WiFiEventStationModeDisconnected& event) {
powDettachInterrupts();
});
static WiFiEventHandler e2 = WiFi.onSoftAPModeStationDisconnected([](const WiFiEventSoftAPModeStationDisconnected& event) {
powDettachInterrupts();
});
static WiFiEventHandler e3 = WiFi.onStationModeConnected([](const WiFiEventStationModeConnected& event) {
powAttachInterrupts();
});
static WiFiEventHandler e4 = WiFi.onSoftAPModeStationConnected([](const WiFiEventSoftAPModeStationConnected& event) {
// Attach interrupts
#if POW_USE_INTERRUPTS
powAttachInterrupts();
});
*/
#endif
}
@ -141,20 +149,67 @@ void powLoop() {
static unsigned long last_update = 0;
static unsigned char report_count = POW_REPORT_EVERY;
static unsigned long power_sum = 0;
static double current_sum = 0;
static unsigned long voltage_sum = 0;
if ((millis() - last_update > POW_UPDATE_INTERVAL) || (last_update == 0 )){
last_update = millis();
unsigned int power = getActivePower();
char buffer[100];
sprintf_P(buffer, PSTR("{\"powVisible\": 1, \"powActivePower\": %d}"), power);
wsSend(buffer);
unsigned int voltage = getVoltage();
double current = getCurrent();
unsigned int apparent = getApparentPower();
unsigned int factor = getPowerFactor();
unsigned int reactive = getReactivePower();
power_sum += power;
current_sum += current;
voltage_sum += voltage;
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
root["powVisible"] = 1;
root["powActivePower"] = power;
root["powCurrent"] = current;
root["powVoltage"] = voltage;
root["powApparentPower"] = apparent;
root["powReactivePower"] = reactive;
root["powPowerFactor"] = factor;
String output;
root.printTo(output);
wsSend(output.c_str());
if (--report_count == 0) {
power = power_sum / POW_REPORT_EVERY;
current = current_sum / POW_REPORT_EVERY;
voltage = voltage_sum / POW_REPORT_EVERY;
apparent = current * voltage;
reactive = (apparent > power) ? sqrt(apparent * apparent - power * power) : 0;
factor = (apparent > 0) ? 100 * power / apparent : 100;
if (factor > 100) factor = 100;
mqttSend(getSetting("powPowerTopic", POW_POWER_TOPIC).c_str(), String(power).c_str());
mqttSend(getSetting("powCurrentTopic", POW_CURRENT_TOPIC).c_str(), String(current).c_str());
mqttSend(getSetting("powVoltageTopic", POW_VOLTAGE_TOPIC).c_str(), String(voltage).c_str());
mqttSend(getSetting("powAPowerTopic", POW_APOWER_TOPIC).c_str(), String(apparent).c_str());
mqttSend(getSetting("powRPowerTopic", POW_RPOWER_TOPIC).c_str(), String(reactive).c_str());
mqttSend(getSetting("powPFactorTopic", POW_PFACTOR_TOPIC).c_str(), String(factor).c_str());
power_sum = current_sum = voltage_sum = 0;
report_count = POW_REPORT_EVERY;
}
// Toggle between current and voltage monitoring
#if POW_USE_INTERRUPTS == 0
hlw8012.toggleMode();
#endif
}
}


+ 2
- 3
code/espurna/relay.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
RELAY MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -55,7 +54,7 @@ String relayString() {
void relayWS() {
String output = relayString();
wsSend((char *) output.c_str());
wsSend(output.c_str());
}
bool relayStatus(unsigned char id) {


+ 1
- 2
code/espurna/rf.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
RF MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 1
- 2
code/espurna/settings.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
SETTINGS MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/


+ 48
- 26
code/espurna/web.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
WEBSERVER MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -14,6 +13,7 @@ Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
#include <Hash.h>
#include <AsyncJson.h>
#include <ArduinoJson.h>
#include <Ticker.h>
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");
@ -24,29 +24,30 @@ typedef struct {
} ws_ticket_t;
ws_ticket_t _ticket[WS_BUFFER_SIZE];
Ticker deferred;
// -----------------------------------------------------------------------------
// WEBSOCKETS
// -----------------------------------------------------------------------------
bool wsSend(char * payload) {
//DEBUG_MSG("[WEBSOCKET] Broadcasting '%s'\n", payload);
bool wsSend(const char * payload) {
DEBUG_MSG("[WEBSOCKET] Broadcasting '%s'\n", payload);
ws.textAll(payload);
}
bool wsSend(uint32_t client_id, char * payload) {
//DEBUG_MSG("[WEBSOCKET] Sending '%s' to #%ld\n", payload, client_id);
bool wsSend(uint32_t client_id, const char * payload) {
DEBUG_MSG("[WEBSOCKET] Sending '%s' to #%ld\n", payload, client_id);
ws.text(client_id, payload);
}
void wsMQTTCallback(unsigned int type, const char * topic, const char * payload) {
if (type == MQTT_CONNECT_EVENT) {
wsSend((char *) "{\"mqttStatus\": true}");
wsSend("{\"mqttStatus\": true}");
}
if (type == MQTT_DISCONNECT_EVENT) {
wsSend((char *) "{\"mqttStatus\": false}");
wsSend("{\"mqttStatus\": false}");
}
}
@ -75,7 +76,12 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
DEBUG_MSG("[WEBSOCKET] Requested action: %s\n", action.c_str());
if (action.equals("reset")) ESP.reset();
if (action.equals("reconnect")) wifiDisconnect();
if (action.equals("reconnect")) {
// Let the HTTP request return and disconnect after 100ms
deferred.once_ms(100, wifiDisconnect);
}
if (action.equals("on")) relayStatus(relayID, true);
if (action.equals("off")) relayStatus(relayID, false);
@ -87,8 +93,9 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
JsonArray& config = root["config"];
DEBUG_MSG("[WEBSOCKET] Parsing configuration data\n");
bool dirty = false;
bool dirtyMQTT = false;
bool save = false;
bool changed = false;
bool changedMQTT = false;
bool apiEnabled = false;
#if ENABLE_FAUXMO
bool fauxmoEnabled = false;
@ -106,15 +113,28 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
if (key == "powExpectedPower") {
powSetExpectedActivePower(value.toInt());
continue;
changed = true;
}
#else
if (key == "powExpectedVoltage") {
powSetExpectedVoltage(value.toInt());
changed = true;
}
if (key == "powExpectedCurrent") {
powSetExpectedCurrent(value.toInt());
changed = true;
}
if (key.startsWith("pow")) continue;
if (key == "powExpectedReset") {
powReset();
changed = true;
}
#endif
if (key.startsWith("pow")) continue;
#if ENABLE_DOMOTICZ
if (key == "dczIdx") {
@ -179,8 +199,8 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
if (value != getSetting(key)) {
//DEBUG_MSG("[WEBSOCKET] Storing %s = %s\n", key.c_str(), value.c_str());
setSetting(key, value);
dirty = true;
if (key.startsWith("mqtt")) dirtyMQTT = true;
save = changed = true;
if (key.startsWith("mqtt")) changedMQTT = true;
}
}
@ -188,12 +208,12 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
// Checkboxes
if (apiEnabled != (getSetting("apiEnabled").toInt() == 1)) {
setSetting("apiEnabled", apiEnabled);
dirty = true;
save = changed = true;
}
#if ENABLE_FAUXMO
if (fauxmoEnabled != (getSetting("fauxmoEnabled").toInt() == 1)) {
setSetting("fauxmoEnabled", fauxmoEnabled);
dirty = true;
save = changed = true;
}
#endif
@ -207,7 +227,7 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
}
for (int i = network; i<WIFI_MAX_NETWORKS; i++) {
if (getSetting("ssid" + String(i)).length() > 0) {
dirty = true;
save = changed = true;
}
delSetting("ssid" + String(i));
delSetting("pass" + String(i));
@ -218,7 +238,7 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
}
// Save settings
if (dirty) {
if (save) {
saveSettings();
wifiConfigure();
@ -242,16 +262,15 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
#endif
// Check if we should reconfigure MQTT connection
if (dirtyMQTT) {
if (changedMQTT) {
mqttDisconnect();
}
}
if (changed) {
ws.text(client_id, "{\"message\": \"Changes saved\"}");
} else {
ws.text(client_id, "{\"message\": \"No changes detected\"}");
}
}
@ -346,6 +365,11 @@ void _wsStart(uint32_t client_id) {
#if ENABLE_POW
root["powVisible"] = 1;
root["powActivePower"] = getActivePower();
root["powApparentPower"] = getApparentPower();
root["powReactivePower"] = getReactivePower();
root["powVoltage"] = getVoltage();
root["powCurrent"] = getCurrent();
root["powPowerFactor"] = getPowerFactor();
#endif
root["maxNetworks"] = WIFI_MAX_NETWORKS;
@ -547,8 +571,6 @@ ArRequestHandlerFunction _onRelayStatusWrapper(unsigned int relayID) {
if (request->method() == HTTP_PUT) {
if (request->hasParam("status", true)) {
AsyncWebParameter* p = request->getParam("status", true);
wsSend((char *) String(relayID).c_str());
wsSend((char *) p->value().c_str());
unsigned int value = p->value().toInt();
if (value == 2) {
relayToggle(relayID);


+ 1
- 7
code/espurna/wifi.ino View File

@ -1,9 +1,8 @@
/*
ESPurna
WIFI MODULE
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
*/
@ -148,11 +147,6 @@ void wifiSetup() {
#endif
// Disconnect from MQTT server if no WIFI
if (code != MESSAGE_CONNECTED) {
if (mqttConnected()) mqttDisconnect();
}
// Configure mDNS
if (code == MESSAGE_CONNECTED) {


+ 1
- 1
code/gulpfile.js View File

@ -2,7 +2,7 @@
ESP8266 file system builder
Copyright (C) 2016 by Xose PĆ©rez <xose dot perez at gmail dot com>
Copyright (C) 2016 by Xose Pérez <xose dot perez at gmail dot com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by


+ 5
- 0
code/html/custom.js View File

@ -36,6 +36,9 @@ function doUpdate() {
var data = $("#formSave").serializeArray();
websock.send(JSON.stringify({'config': data}));
$(".powExpected").val(0);
$("input[name='powExpectedReset']")
.prop("checked", false)
.iphoneStyle("refresh");
}
return false;
}
@ -204,6 +207,8 @@ function processData(data) {
window.location = "/";
});
} else {
window.location = "/";
}
}


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

@ -136,10 +136,35 @@
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powActivePower">Power (W)</label>
<label class="pure-u-1 pure-u-sm-1-4" for="powActivePower">Active Power (W)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powActivePower" readonly />
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powApparentPower">Apparent Power (W)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powApparentPower" readonly />
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powReactivePower">Reactive Power (W)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powReactivePower" readonly />
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powCurrent">Current (A)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powCurrent" readonly />
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powVoltage">Voltage (V)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powVoltage" readonly />
</div>
<div class="pure-g module module-pow">
<label class="pure-u-1 pure-u-sm-1-4" for="powPowerFactor">Power Factor (%)</label>
<input class="pure-u-1 pure-u-sm-3-4" type="text" name="powPowerFactor" readonly />
</div>
<div id="relays">
</div>
@ -217,7 +242,7 @@
</div>
<div class="pure-g module module-fauxmo">
<div class="pure-u-1 pure-u-sm-1-4"><label for="fauxmoEnabled">Enable WeMo emulation</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><label for="fauxmoEnabled">Alexa integration</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="fauxmoEnabled" tabindex="6" /></div>
</div>
@ -383,7 +408,28 @@
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedPower">AC RMS Active Power</label>
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedPower" type="text" size="8" tabindex="41" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">If you are using a pure resistive load like a bulb this will be writen on it, otherwise use a socket multimeter to get this value.</div>
<div class="pure-u-1 pure-u-md-3-4 hint">In Watts (W). If you are using a pure resistive load like a bulb this will be writen on it, otherwise use a socket multimeter to get this value.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedVoltage">AC RMS Voltage</label>
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedVoltage" type="text" size="8" tabindex="41" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">In Volts (V). Enter your the nominal AC voltage for your household or facility, or use multimeter to get this value.</div>
</div>
<div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedCurrent">AC RMS Current</label>
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedCurrent" type="text" size="8" tabindex="41" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">In Ampers (A). If you are using a pure resistive load like a bulb this will the ratio between the two previous values, i.e. power / voltage. You can also use a current clamp around one fo the power wires to get this value.</div>
</div>
<div class="pure-g">
<div class="pure-u-1 pure-u-sm-1-4"><label for="powExpectedReset">Reset calibration</label></div>
<div class="pure-u-1 pure-u-sm-1-4"><input type="checkbox" name="powExpectedReset" /></div>
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">Move this switch to ON and press "Update" to revert to factory values.</div>
</div>
</fieldset>


+ 25
- 4
code/platformio.ini View File

@ -7,7 +7,7 @@ data_dir = espurna/data
lib_deps =
DHT sensor library
Adafruit Unified Sensor
Time
https://github.com/xoseperez/Time
ArduinoJson
ESPAsyncTCP
ESPAsyncWebServer
@ -16,8 +16,8 @@ lib_deps =
NtpClientLib
OneWire
DallasTemperature
JustWifi
HLW8012
https://bitbucket.org/xoseperez/justwifi.git
https://bitbucket.org/xoseperez/hlw8012.git
https://bitbucket.org/xoseperez/fauxmoesp.git
https://bitbucket.org/xoseperez/nofuss.git
https://bitbucket.org/xoseperez/emonliteesp.git
@ -127,7 +127,7 @@ extra_script = pio_hooks.py
build_flags = -g -Wl,-Tesp8266.flash.1m128.ld -DDEBUG_PORT=Serial -DSONOFF_POW
upload_speed = 115200
upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266
upload_flags = --auth=Algernon1 --port 8266
[env:sonoff-dual-debug]
platform = espressif8266
@ -263,3 +263,24 @@ build_flags = -g -Wl,-Tesp8266.flash.1m128.ld -DDEBUG_PORT=Serial -DECOPLUG
upload_speed = 115200
upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266
[env:jangoe-debug]
platform = espressif8266
framework = arduino
board = esp01_1m
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_script = pio_hooks.py
build_flags = -g -DDEBUG_PORT=Serial -DWIFI_RELAY_NC
[env:jangoe-debug-ota]
platform = espressif8266
framework = arduino
board = esp01_1m
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_script = pio_hooks.py
build_flags = -g -DDEBUG_PORT=Serial -DWIFI_RELAY_NC
upload_speed = 115200
upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266

BIN
images/devices/jangoe-wifi-relay.png View File

Before After
Width: 400  |  Height: 400  |  Size: 32 KiB

Loading…
Cancel
Save