Browse Source

Merge branch 'dev' into toggle-switch

fastled
Xose Pérez 8 years ago
parent
commit
c19d58685f
21 changed files with 202 additions and 31 deletions
  1. +21
    -2
      CHANGELOG.md
  2. +16
    -8
      README.md
  3. +2
    -0
      code/build-fs
  4. +1
    -0
      code/espurna/config/arduino.h
  5. +7
    -0
      code/espurna/config/general.h
  6. +15
    -0
      code/espurna/config/hardware.h
  7. +1
    -0
      code/espurna/config/prototypes.h
  8. +6
    -0
      code/espurna/config/sensors.h
  9. +1
    -1
      code/espurna/config/version.h
  10. +16
    -2
      code/espurna/dht.ino
  11. +7
    -3
      code/espurna/domoticz.ino
  12. +1
    -1
      code/espurna/ds18b20.ino
  13. +37
    -2
      code/espurna/emon.ino
  14. +1
    -4
      code/espurna/ntp.ino
  15. +35
    -1
      code/espurna/pow.ino
  16. +5
    -3
      code/espurna/relay.ino
  17. +5
    -0
      code/espurna/wifi.ino
  18. +3
    -3
      code/html/index.html
  19. +1
    -1
      code/pio_hooks.py
  20. +21
    -0
      code/platformio.ini
  21. BIN
      images/devices/mqtt-relay.jpg

+ 21
- 2
CHANGELOG.md View File

@ -3,6 +3,25 @@
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [1.5.3] 2017-02-02
### Fixed
- Issue #50 and #54. Fixed domoticz MQTT message format
### Added
- Energy calculation and aggregation. API entry points and MQTT messages.
## [1.5.2] 2017-01-29
### Fixed
- Fix bug in emon topic payload
## [1.5.1] 2017-01-28
### Added
- OpenEnergyMonitor WiFi MQTT Relay / Thermostat support (thanks to Denis French)
### Fixed
- NTP connection refresh upon wifi connection
- Filesystem image build using local gulp installation
## [1.5.0] 2017-01-21
### Added
- Pulse mode. Allows to define a pulse time after which the relay will switch back
@ -47,7 +66,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Support for inverse logic relays
### Fixed
- Fixed error in relay identification from MQTT messages (issue #31)
- Issue #31. Fixed error in relay identification from MQTT messages
## [1.4.1] 2017-01-05
### Added
@ -63,7 +82,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Support for static IP connections
### Fixed
- Enforce minimum password strength in web interface (#16)
- Issue #16. Enforce minimum password strength in web interface
### Changed
- Using default client_id provided by AsyncMqttClient


+ 16
- 8
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.5.0**, read the [changelog](CHANGELOG.md).
**Current Release Version is 1.5.3**, read the [changelog](CHANGELOG.md).
## Features
@ -46,17 +46,25 @@ For more information please refer to the [ESPurna Wiki](https://bitbucket.org/xo
## Supported hardware
|![IteadStudio S20](images/devices/s20.jpg) **IteadStudio S20**|![IteadStudio Slampher](images/devices/slampher.jpg) **IteadStudio Slampher**|![IteadStudio Sonoff 4CH](images/devices/sonoff-4ch.jpg) **IteadStudio Sonoff 4CH**|
|![IteadStudio Sonoff Basic](images/devices/sonoff-basic.jpg) **IteadStudio Sonoff Basic**|![IteadStudio Motor Switch](images/devices/motor-switch.jpg) **IteadStudio Motor Switch**|![IteadStudio 1CH Inching](images/devices/1ch-inching.jpg) **IteadStudio 1CH Inching**|
|![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)**|||
||||
|-|-|-|
|![IteadStudio S20](images/devices/s20.jpg)|![IteadStudio Slampher](images/devices/slampher.jpg)|![IteadStudio Sonoff 4CH](images/devices/sonoff-4ch.jpg)|
|**IteadStudio S20**|**IteadStudio Slampher**|**IteadStudio Sonoff 4CH**|
|![IteadStudio Sonoff Basic](images/devices/sonoff-basic.jpg)|![IteadStudio Motor Switch](images/devices/motor-switch.jpg)|![IteadStudio 1CH Inching](images/devices/1ch-inching.jpg)|
|**IteadStudio Sonoff Basic**|**IteadStudio Motor Switch**|**IteadStudio 1CH Inching**|
|![IteadStudio Sonoff Dual](images/devices/sonoff-dual.jpg)|![IteadStudio Sonoff POW](images/devices/sonoff-pow.jpg)|![IteadStudio Sonoff TH10/TH16](images/devices/sonoff-th10-th16.jpg)|
|**IteadStudio Sonoff Dual**|**IteadStudio Sonoff POW**|**IteadStudio Sonoff TH10/TH16**|
|![IteadStudio Sonoff RF](images/devices/sonoff-rf.jpg)|![IteadStudio Sonoff SV](images/devices/sonoff-sv.jpg)|![IteadStudio Sonoff Touch](images/devices/sonoff-touch.jpg)|
|**IteadStudio Sonoff RF**|**IteadStudio Sonoff SV**|**IteadStudio Sonoff Touch**|
|![Wemos D1 Mini Relay Shield](images/devices/d1mini.jpg)|![Electrodragon Relay Board](images/devices/electrodragon-relay-board.jpg)|![WorkChoice EcoPlug](images/devices/workchoice-ecoplug.jpg)|
|**Wemos D1 Mini Relay Shield**|**Electrodragon Relay Board**|**WorkChoice EcoPlug**|
|![JanGoe Wifi Relay (NO/NC)](images/devices/jangoe-wifi-relay.png)|![OpenEnergyMonitor WiFi MQTT Relay / Thermostat](images/devices/mqtt-relay.jpg)||
|**JanGoe Wifi Relay (NO/NC)**|**OpenEnergyMonitor WiFi MQTT Relay / Thermostat**||
## License
Copyright (C) 2016-2017 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


+ 2
- 0
code/build-fs View File

@ -0,0 +1,2 @@
#!/bin/bash
node node_modules/gulp/bin/gulp.js

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

@ -28,6 +28,7 @@
//#define WIFI_RELAY_NC
//#define WIFI_RELAY_NO
//#define ESPURNA
//#define MQTT_RELAY
//--------------------------------------------------------------------------------
// Features (values below are non-default values)


+ 7
- 0
code/espurna/config/general.h View File

@ -7,6 +7,13 @@
#define BUFFER_SIZE 1024
#define HEARTBEAT_INTERVAL 300000
//--------------------------------------------------------------------------------
// EEPROM
//--------------------------------------------------------------------------------
#define EEPROM_RELAY_STATUS 0
#define EEPROM_POWER_COUNT 1
//--------------------------------------------------------------------------------
// RELAY
//--------------------------------------------------------------------------------


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

@ -256,6 +256,21 @@
#define LED1_PIN 13
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// WiFi MQTT Relay / Thermostat
// -----------------------------------------------------------------------------
#elif defined(MQTT_RELAY)
#define MANUFACTURER "OPENENERGYMONITOR"
#define DEVICE "MQTT_RELAY"
#define BUTTON1_PIN 0
#define BUTTON1_RELAY 1
#define RELAY1_PIN 12
#define RELAY1_PIN_INVERSE 0
#define LED1_PIN 16
#define LED1_PIN_INVERSE 0
// -----------------------------------------------------------------------------
// Unknown hardware
// -----------------------------------------------------------------------------


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

@ -12,3 +12,4 @@ void mqttRegister(void (*callback)(unsigned int, const char *, const char *));
template<typename T> bool setSetting(const String& key, T value);
template<typename T> String getSetting(const String& key, T defaultValue);
template<typename T> void domoticzSend(const char * key, T value);
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue);

+ 6
- 0
code/espurna/config/sensors.h View File

@ -20,6 +20,11 @@
#define DHT_TEMPERATURE_TOPIC "/temperature"
#define DHT_HUMIDITY_TOPIC "/humidity"
#define HUMIDITY_NORMAL 0
#define HUMIDITY_COMFORTABLE 1
#define HUMIDITY_DRY 2
#define HUMIDITY_WET 3
//--------------------------------------------------------------------------------
// DS18B20 temperature sensor
// Enable support by passing ENABLE_DS18B20=1 build flag
@ -46,6 +51,7 @@
#define EMON_MAINS_VOLTAGE 230
#define EMON_CURRENT_RATIO 180
#define EMON_POWER_TOPIC "/power"
#define EMON_ENERGY_TOPIC "/energy"
//--------------------------------------------------------------------------------
// HLW8012 power sensor (Sonoff POW)


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

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

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

@ -73,8 +73,22 @@ void dhtLoop() {
// Send to Domoticz
#if ENABLE_DOMOTICZ
domoticzSend("dczTmpIdx", temperature);
domoticzSend("dczHumIdx", humidity);
{
domoticzSend("dczTmpIdx", 0, temperature);
int status;
if (h > 70) {
status = HUMIDITY_WET;
} else if (h > 45) {
status = HUMIDITY_COMFORTABLE;
} else if (h > 30) {
status = HUMIDITY_NORMAL;
} else {
status = HUMIDITY_DRY;
}
char buffer[2];
sprintf(buffer, "%d", status);
domoticzSend("dczHumIdx", humidity, buffer);
}
#endif
// Update websocket clients


+ 7
- 3
code/espurna/domoticz.ino View File

@ -8,13 +8,17 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#if ENABLE_DOMOTICZ
template<typename T> void domoticzSend(const char * key, T value) {
template<typename T> void domoticzSend(const char * key, T nvalue, const char * svalue) {
unsigned int idx = getSetting(key).toInt();
if (idx > 0) {
char payload[45];
sprintf(payload, "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"\"}", idx, String(value).c_str());
char payload[128];
snprintf(payload, 128, "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"%s\"}", idx, String(nvalue).c_str(), svalue);
mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload);
}
}
template<typename T> void domoticzSend(const char * key, T nvalue) {
domoticzSend(key, nvalue, "");
}
#endif

+ 1
- 1
code/espurna/ds18b20.ino View File

@ -62,7 +62,7 @@ void dsLoop() {
// Send to Domoticz
#if ENABLE_DOMOTICZ
domoticzSend("dczTmpIdx", temperature);
domoticzSend("dczTmpIdx", 0, temperature);
#endif
// Update websocket clients


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

@ -9,10 +9,12 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#if ENABLE_EMON
#include <EmonLiteESP.h>
#include <EEPROM.h>
EmonLiteESP emon;
double _current = 0;
unsigned int _power = 0;
double _energy = 0;
// -----------------------------------------------------------------------------
// EMON
@ -26,6 +28,10 @@ unsigned int getPower() {
return _power;
}
double getEnergy() {
return _energy;
}
double getCurrent() {
return _current;
}
@ -34,6 +40,20 @@ unsigned int currentCallback() {
return analogRead(EMON_CURRENT_PIN);
}
void retrieveEnergy() {
unsigned long energy = EEPROM.read(EEPROM_POWER_COUNT + 1);
energy = (energy << 8) + EEPROM.read(EEPROM_POWER_COUNT);
if (energy == 0xFFFF) energy = 0;
_energy = energy;
}
void saveEnergy() {
unsigned int energy = (int) _energy;
EEPROM.write(EEPROM_POWER_COUNT, energy & 0xFF);
EEPROM.write(EEPROM_POWER_COUNT + 1, (energy >> 8) & 0xFF);
EEPROM.commit();
}
void powerMonitorSetup() {
// backwards compatibility
@ -56,6 +76,11 @@ void powerMonitorSetup() {
apiRegister("/api/power", "power", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", _power);
});
apiRegister("/api/energy", "energy", [](char * buffer, size_t len) {
snprintf(buffer, len, "%ld", (unsigned long) _energy);
});
retrieveEnergy();
}
@ -108,14 +133,24 @@ void powerMonitorLoop() {
if (measurements == EMON_MEASUREMENTS) {
_power = (int) ((sum - max - min) * mainsVoltage / (measurements - 2));
double window = (double) EMON_INTERVAL * EMON_MEASUREMENTS / 1000.0 / 3600.0;
_energy += _power * window;
saveEnergy();
sum = 0;
measurements = 0;
char power[6];
snprintf(power, "%d", 6, _power);
snprintf(power, 6, "%d", _power);
char energy[8];
snprintf(energy, 6, "%ld", (unsigned long) _energy);
mqttSend(getSetting("emonPowerTopic", EMON_POWER_TOPIC).c_str(), power);
mqttSend(getSetting("emonEnergyTopic", EMON_ENERGY_TOPIC).c_str(), energy);
#if ENABLE_DOMOTICZ
domoticzSend("dczPowIdx", power);
{
char buffer[20];
snprintf(buffer, 20, "%s;%s", power, energy);
domoticzSend("dczPowIdx", 0, buffer);
}
#endif


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

@ -14,7 +14,7 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
// NTP
// -----------------------------------------------------------------------------
void ntpConnect(WiFiEventStationModeGotIP ipInfo) {
void ntpConnect() {
NTP.begin(NTP_SERVER, NTP_TIME_OFFSET, NTP_DAY_LIGHT);
NTP.setInterval(NTP_UPDATE_INTERVAL);
}
@ -33,9 +33,6 @@ void ntpSetup() {
}
});
static WiFiEventHandler e;
e = WiFi.onStationModeGotIP(ntpConnect);
}
void ntpLoop() {


+ 35
- 1
code/espurna/pow.ino View File

@ -12,9 +12,11 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#include <HLW8012.h>
#include <Hash.h>
#include <ArduinoJson.h>
#include <EEPROM.h>
HLW8012 hlw8012;
bool _powEnabled = false;
double _energy = 0;
// -----------------------------------------------------------------------------
// POW
@ -117,8 +119,26 @@ unsigned int getPowerFactor() {
return (int) (100 * hlw8012.getPowerFactor());
}
double getEnergy() {
return _energy;
}
// -----------------------------------------------------------------------------
void retrieveEnergy() {
unsigned long energy = EEPROM.read(EEPROM_POWER_COUNT + 1);
energy = (energy << 8) + EEPROM.read(EEPROM_POWER_COUNT);
if (energy == 0xFFFF) energy = 0;
_energy = energy;
}
void saveEnergy() {
unsigned int energy = (int) _energy;
EEPROM.write(EEPROM_POWER_COUNT, energy & 0xFF);
EEPROM.write(EEPROM_POWER_COUNT + 1, (energy >> 8) & 0xFF);
EEPROM.commit();
}
void powSetup() {
// Initialize HLW8012
@ -143,10 +163,16 @@ void powSetup() {
// Retrieve calibration values
powRetrieveCalibration();
// Recover energy reading
retrieveEnergy();
// API definitions
apiRegister("/api/power", "power", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", getActivePower());
});
apiRegister("/api/energy", "energy", [](char * buffer, size_t len) {
snprintf(buffer, len, "%ld", (unsigned long) _energy);
});
apiRegister("/api/current", "current", [](char * buffer, size_t len) {
dtostrf(getCurrent(), len-1, 2, buffer);
});
@ -216,8 +242,12 @@ void powLoop() {
reactive = (apparent > power) ? sqrt(apparent * apparent - power * power) : 0;
factor = (apparent > 0) ? 100 * power / apparent : 100;
if (factor > 100) factor = 100;
unsigned long window = (double) POW_REPORT_EVERY * POW_UPDATE_INTERVAL / 1000.0 / 3600.0;
_energy += power * window;
saveEnergy();
mqttSend(getSetting("powPowerTopic", POW_POWER_TOPIC).c_str(), String(power).c_str());
mqttSend(getSetting("powEnergyTopic", POW_ENERGY_TOPIC).c_str(), String(_energy).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());
@ -225,7 +255,11 @@ void powLoop() {
mqttSend(getSetting("powPFactorTopic", POW_PFACTOR_TOPIC).c_str(), String(factor).c_str());
#if ENABLE_DOMOTICZ
domoticzSend("dczPowIdx", power);
{
char buffer[20];
snprintf(buffer, 20, "%d;%ld", power, (unsigned long) _energy);
domoticzSend("dczPowIdx", 0, buffer);
}
#endif
power_sum = current_sum = voltage_sum = 0;


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

@ -208,20 +208,21 @@ void relaySave() {
if (relayStatus(i)) mask += bit;
bit += bit;
}
EEPROM.write(0, mask);
EEPROM.write(EEPROM_RELAY_STATUS, mask);
EEPROM.commit();
}
void relayRetrieve(bool invert) {
recursive = true;
unsigned char bit = 1;
unsigned char mask = invert ? ~EEPROM.read(0) : EEPROM.read(0);
unsigned char mask = invert ? ~EEPROM.read(EEPROM_RELAY_STATUS) : EEPROM.read(EEPROM_RELAY_STATUS);
for (unsigned int i=0; i < _relays.size(); i++) {
relayStatus(i, ((mask & bit) == bit));
bit += bit;
}
if (invert) {
relaySave();
EEPROM.write(EEPROM_RELAY_STATUS, mask);
EEPROM.commit();
}
recursive = false;
}
@ -385,6 +386,7 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
if (type == MQTT_MESSAGE_EVENT) {
// Match topic
if (strncmp(topic, "domoticz", 8) == 0) return;
String t = String(topic + mqttTopicRootLength());
if (!t.startsWith(MQTT_RELAY_TOPIC)) return;
if (!t.endsWith(mqttSetter)) return;


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

@ -168,6 +168,11 @@ void wifiSetup() {
dnsServer.stop();
}
// NTP connection reset
if (code == MESSAGE_CONNECTED) {
ntpConnect();
}
// Manage POW
#if ENABLE_POW
if (code == MESSAGE_CONNECTED) {


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

@ -391,19 +391,19 @@
<input class="pure-u-1 pure-u-md-3-4" name="dczTopicOut" type="text" tabindex="32" placeholder="domoticz/out" />
</div>
<div class="pure-g modude module-dht module-ds">
<div class="pure-g module module-dht module-ds">
<label class="pure-u-1 pure-u-sm-1-4" for="dczTmpIdx">Temperature IDX</label>
<div class="pure-u-1 pure-u-sm-1-8"><input class="pure-u-sm-23-24" name="dczTmpIdx" type="number" min="0" tabindex="33" data="0" /></div>
<div class="pure-u-1 pure-u-sm-5-8 hint center">Set to 0 to disable notifications.</div>
</div>
<div class="pure-g modude module-dht">
<div class="pure-g module module-dht">
<label class="pure-u-1 pure-u-sm-1-4" for="dczHumIdx">Humidity IDX</label>
<div class="pure-u-1 pure-u-sm-1-8"><input class="pure-u-sm-23-24" name="dczHumIdx" type="number" min="0" tabindex="34" data="0" /></div>
<div class="pure-u-1 pure-u-sm-5-8 hint center">Set to 0 to disable notifications.</div>
</div>
<div class="pure-g modude module-pow module-emon">
<div class="pure-g module module-pow module-emon">
<label class="pure-u-1 pure-u-sm-1-4" for="dczPowIdx">Power IDX</label>
<div class="pure-u-1 pure-u-sm-1-8"><input class="pure-u-sm-23-24" name="dczPowIdx" type="number" min="0" tabindex="35" data="0" /></div>
<div class="pure-u-1 pure-u-sm-5-8 hint center">Set to 0 to disable notifications.</div>


+ 1
- 1
code/pio_hooks.py View File

@ -6,6 +6,6 @@ from SCons.Script import DefaultEnvironment
env = DefaultEnvironment()
def before_build_spiffs(source, target, env):
env.Execute("gulp")
env.Execute("node node_modules/gulp/bin/gulp.js")
env.AddPreAction(".pioenvs/%s/spiffs.bin" % env['PIOENV'], before_build_spiffs)

+ 21
- 0
code/platformio.ini View File

@ -284,3 +284,24 @@ build_flags = -g -DDEBUG_PORT=Serial -DWIFI_RELAY_NC
upload_speed = 115200
upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266
[env:mqtt-relay-debug]
platform = espressif8266
framework = arduino
board = esp_wroom_02
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_script = pio_hooks.py
build_flags = -g -DDEBUG_PORT=Serial -DMQTT_RELAY -DENABLE_DS18B20=1
[env:mqtt-relay-debug-ota]
platform = espressif8266
framework = arduino
board = esp_wroom_02
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
extra_script = pio_hooks.py
build_flags = -g -DDEBUG_PORT=Serial -DMQTT_RELAY -DENABLE_DS18B20=1
upload_speed = 115200
upload_port = "192.168.4.1"
upload_flags = --auth=fibonacci --port 8266

BIN
images/devices/mqtt-relay.jpg View File

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

Loading…
Cancel
Save