Browse Source

Merge branch 'apis' into dev

Conflicts:
	code/espurna/data/index.html.gz
	code/espurna/pow.ino
	code/html/index.html
fastled
Xose Pérez 7 years ago
parent
commit
b32fa7f723
17 changed files with 465 additions and 253 deletions
  1. +1
    -0
      code/espurna/config/all.h
  2. +9
    -9
      code/espurna/config/arduino.h
  3. +1
    -1
      code/espurna/config/general.h
  4. +14
    -0
      code/espurna/config/prototypes.h
  5. +1
    -1
      code/espurna/config/sensors.h
  6. BIN
      code/espurna/data/index.html.gz
  7. BIN
      code/espurna/data/script.js.gz
  8. +31
    -16
      code/espurna/dht.ino
  9. +3
    -60
      code/espurna/domoticz.ino
  10. +17
    -7
      code/espurna/ds18b20.ino
  11. +31
    -18
      code/espurna/emon.ino
  12. +2
    -12
      code/espurna/espurna.ino
  13. +17
    -0
      code/espurna/pow.ino
  14. +148
    -25
      code/espurna/relay.ino
  15. +154
    -93
      code/espurna/web.ino
  16. +4
    -4
      code/html/custom.js
  17. +32
    -7
      code/html/index.html

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

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


+ 9
- 9
code/espurna/config/arduino.h View File

@ -30,14 +30,14 @@
//#define ESPURNA //#define ESPURNA
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// Features (values below are default values)
// Features (values below are non-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
//#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
//#define ENABLE_DOMOTICZ 0

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

@ -80,7 +80,7 @@
#define MQTT_MESSAGE_EVENT 2 #define MQTT_MESSAGE_EVENT 2
// Custom get and set postfixes // Custom get and set postfixes
// Use something like "/status" or "/set", with trailing slash
// Use something like "/status" or "/set", with leading slash
#define MQTT_USE_GETTER "" #define MQTT_USE_GETTER ""
#define MQTT_USE_SETTER "" #define MQTT_USE_SETTER ""


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

@ -0,0 +1,14 @@
#include <Arduino.h>
#include <functional>
#include <NtpClientLib.h>
#include <ESPAsyncWebServer.h>
#include <AsyncMqttClient.h>
#include <functional>
typedef std::function<void(char *, size_t)> apiGetCallbackFunction;
typedef std::function<void(const char *)> apiPutCallbackFunction;
void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn = NULL);
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);

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

@ -14,7 +14,7 @@
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
#define DHT_PIN 14 #define DHT_PIN 14
#define DHT_UPDATE_INTERVAL 300000
#define DHT_UPDATE_INTERVAL 60000
#define DHT_TYPE DHT22 #define DHT_TYPE DHT22
#define DHT_TIMING 11 #define DHT_TIMING 11
#define DHT_TEMPERATURE_TOPIC "/temperature" #define DHT_TEMPERATURE_TOPIC "/temperature"


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


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


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

@ -13,29 +13,33 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
DHT dht(DHT_PIN, DHT_TYPE, DHT_TIMING); DHT dht(DHT_PIN, DHT_TYPE, DHT_TIMING);
char dhtTemperature[6];
char dhtHumidity[6];
double _dhtTemperature = 0;
unsigned int _dhtHumidity = 0;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// DHT
// Values
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
char * getDHTTemperature() {
return dhtTemperature;
double getDHTTemperature() {
return _dhtTemperature;
} }
char * getDHTHumidity() {
return dhtHumidity;
unsigned int getDHTHumidity() {
return _dhtHumidity;
} }
void dhtSetup() { void dhtSetup() {
dht.begin(); dht.begin();
apiRegister("/api/temperature", "temperature", [](char * buffer, size_t len) {
dtostrf(_dhtTemperature, len-1, 1, buffer);
});
apiRegister("/api/humidity", "humidity", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", _dhtHumidity);
});
} }
void dhtLoop() { void dhtLoop() {
if (!mqttConnected()) return;
// Check if we should read new data // Check if we should read new data
static unsigned long last_update = 0; static unsigned long last_update = 0;
if ((millis() - last_update > DHT_UPDATE_INTERVAL) || (last_update == 0)) { if ((millis() - last_update > DHT_UPDATE_INTERVAL) || (last_update == 0)) {
@ -52,19 +56,30 @@ void dhtLoop() {
} else { } else {
dtostrf(t, 4, 1, dhtTemperature);
itoa((int) h, dhtHumidity, 10);
_dhtTemperature = t;
_dhtHumidity = h;
char temperature[6];
char humidity[6];
dtostrf(t, 4, 1, temperature);
itoa((unsigned int) h, humidity, 10);
DEBUG_MSG("[DHT] Temperature: %s\n", dhtTemperature);
DEBUG_MSG("[DHT] Humidity: %s\n", dhtHumidity);
DEBUG_MSG("[DHT] Temperature: %s\n", temperature);
DEBUG_MSG("[DHT] Humidity: %s\n", humidity);
// Send MQTT messages // Send MQTT messages
mqttSend(getSetting("dhtTmpTopic", DHT_TEMPERATURE_TOPIC).c_str(), dhtTemperature);
mqttSend(getSetting("dhtHumTopic", DHT_HUMIDITY_TOPIC).c_str(), dhtHumidity);
mqttSend(getSetting("dhtTmpTopic", DHT_TEMPERATURE_TOPIC).c_str(), temperature);
mqttSend(getSetting("dhtHumTopic", DHT_HUMIDITY_TOPIC).c_str(), humidity);
// Send to Domoticz
#if ENABLE_DOMOTICZ
domoticzSend("dczTmpIdx", temperature);
domoticzSend("dczHumIdx", humidity);
#endif
// Update websocket clients // Update websocket clients
char buffer[100]; char buffer[100];
sprintf_P(buffer, PSTR("{\"dhtVisible\": 1, \"dhtTmp\": %s, \"dhtHum\": %s}"), dhtTemperature, dhtHumidity);
sprintf_P(buffer, PSTR("{\"dhtVisible\": 1, \"dhtTmp\": %s, \"dhtHum\": %s}"), temperature, humidity);
wsSend(buffer); wsSend(buffer);
} }


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

@ -8,70 +8,13 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#if ENABLE_DOMOTICZ #if ENABLE_DOMOTICZ
#include <Hash.h>
#include <ArduinoJson.h>
void domoticzMQTTCallback(unsigned int type, const char * topic, const char * payload) {
String dczTopicOut = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribeRaw(dczTopicOut.c_str());
}
if (type == MQTT_MESSAGE_EVENT) {
// Check topic
if (dczTopicOut.equals(topic)) {
// Parse response
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char *) payload);
if (!root.success()) {
DEBUG_MSG("[DOMOTICZ] Error parsing data\n");
return;
}
// IDX
unsigned long idx = root["idx"];
int relayID = domoticzRelay(idx);
if (relayID >= 0) {
unsigned long value = root["nvalue"];
DEBUG_MSG("[DOMOTICZ] Received value %d for IDX %d\n", value, idx);
relayStatus(relayID, value == 1);
}
}
}
}
int domoticzIdx(unsigned int relayID) {
return getSetting("dczIdx" + String(relayID)).toInt();
}
int domoticzRelay(unsigned int idx) {
for (int relayID=0; relayID<relayCount(); relayID++) {
if (domoticzIdx(relayID) == idx) {
return relayID;
}
}
return -1;
}
void domoticzSend(unsigned int relayID) {
unsigned int idx = domoticzIdx(relayID);
template<typename T> void domoticzSend(const char * key, T value) {
unsigned int idx = getSetting(key).toInt();
if (idx > 0) { if (idx > 0) {
unsigned int value = relayStatus(relayID) ? 1 : 0;
char payload[45]; char payload[45];
sprintf(payload, "{\"idx\": %d, \"nvalue\": %d, \"svalue\": \"\"}", idx, value);
sprintf(payload, "{\"idx\": %d, \"nvalue\": %s, \"svalue\": \"\"}", idx, String(value).c_str());
mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload); mqttSendRaw(getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC).c_str(), payload);
} }
} }
void domoticzSetup() {
mqttRegister(domoticzMQTTCallback);
}
#endif #endif

+ 17
- 7
code/espurna/ds18b20.ino View File

@ -14,18 +14,21 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
OneWire oneWire(DS_PIN); OneWire oneWire(DS_PIN);
DallasTemperature ds18b20(&oneWire); DallasTemperature ds18b20(&oneWire);
char dsTemperature[6];
double _dsTemperature = 0;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// DS18B20 // DS18B20
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
char * getDSTemperature() {
return dsTemperature;
double getDSTemperature() {
return _dsTemperature;
} }
void dsSetup() { void dsSetup() {
ds18b20.begin(); ds18b20.begin();
apiRegister("/api/temperature", "temperature", [](char * buffer, size_t len) {
dtostrf(_dsTemperature, len-1, 1, buffer);
});
} }
void dsLoop() { void dsLoop() {
@ -48,16 +51,23 @@ void dsLoop() {
} else { } else {
dtostrf(t, 4, 1, dsTemperature);
_dsTemperature = t;
DEBUG_MSG("[DS18B20] Temperature: %s\n", dsTemperature);
char temperature[6];
dtostrf(t, 5, 1, temperature);
DEBUG_MSG("[DS18B20] Temperature: %s\n", temperature);
// Send MQTT messages // Send MQTT messages
mqttSend(getSetting("dsTmpTopic", DS_TEMPERATURE_TOPIC).c_str(), dsTemperature);
mqttSend(getSetting("dsTmpTopic", DS_TEMPERATURE_TOPIC).c_str(), temperature);
// Send to Domoticz
#if ENABLE_DOMOTICZ
domoticzSend("dczTmpIdx", temperature);
#endif
// Update websocket clients // Update websocket clients
char buffer[100]; char buffer[100];
sprintf_P(buffer, PSTR("{\"dsVisible\": 1, \"dsTmp\": %s}"), dsTemperature);
sprintf_P(buffer, PSTR("{\"dsVisible\": 1, \"dsTmp\": %s}"), temperature);
wsSend(buffer); wsSend(buffer);
} }


+ 31
- 18
code/espurna/emon.ino View File

@ -11,8 +11,8 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#include <EmonLiteESP.h> #include <EmonLiteESP.h>
EmonLiteESP emon; EmonLiteESP emon;
double current;
char power[8];
double _current = 0;
unsigned int _power = 0;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// EMON // EMON
@ -22,12 +22,12 @@ void setCurrentRatio(float value) {
emon.setCurrentRatio(value); emon.setCurrentRatio(value);
} }
char * getPower() {
return power;
unsigned int getPower() {
return _power;
} }
double getCurrent() { double getCurrent() {
return current;
return _current;
} }
unsigned int currentCallback() { unsigned int currentCallback() {
@ -52,6 +52,11 @@ void powerMonitorSetup() {
getSetting("emonRatio", EMON_CURRENT_RATIO).toFloat() getSetting("emonRatio", EMON_CURRENT_RATIO).toFloat()
); );
emon.setPrecision(EMON_CURRENT_PRECISION); emon.setPrecision(EMON_CURRENT_PRECISION);
apiRegister("/api/power", "power", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", _power);
});
} }
void powerMonitorLoop() { void powerMonitorLoop() {
@ -74,38 +79,46 @@ void powerMonitorLoop() {
// Safety check: do not read current if relay is OFF // Safety check: do not read current if relay is OFF
if (!relayStatus(0)) { if (!relayStatus(0)) {
current = 0;
_current = 0;
} else { } else {
current = emon.getCurrent(EMON_SAMPLES);
current -= EMON_CURRENT_OFFSET;
if (current < 0) current = 0;
_current = emon.getCurrent(EMON_SAMPLES);
_current -= EMON_CURRENT_OFFSET;
if (_current < 0) _current = 0;
} }
if (measurements == 0) { if (measurements == 0) {
max = min = current;
max = min = _current;
} else { } else {
if (current > max) max = current;
if (current < min) min = current;
if (_current > max) max = _current;
if (_current < min) min = _current;
} }
sum += current;
sum += _current;
++measurements; ++measurements;
float mainsVoltage = getSetting("emonMains", EMON_MAINS_VOLTAGE).toFloat(); float mainsVoltage = getSetting("emonMains", EMON_MAINS_VOLTAGE).toFloat();
//DEBUG_MSG("[ENERGY] Power now: %dW\n", int(current * mainsVoltage));
//DEBUG_MSG("[ENERGY] Power now: %dW\n", int(_current * mainsVoltage));
// Update websocket clients // Update websocket clients
char text[20]; char text[20];
sprintf_P(text, PSTR("{\"emonPower\": %d}"), int(current * mainsVoltage));
sprintf_P(text, PSTR("{\"emonPower\": %d}"), int(_current * mainsVoltage));
wsSend(text); wsSend(text);
// Send MQTT messages averaged every EMON_MEASUREMENTS // Send MQTT messages averaged every EMON_MEASUREMENTS
if (measurements == EMON_MEASUREMENTS) { if (measurements == EMON_MEASUREMENTS) {
double p = (sum - max - min) * mainsVoltage / (measurements - 2);
sprintf(power, "%d", int(p));
mqttSend(getSetting("emonPowerTopic", EMON_POWER_TOPIC).c_str(), power);
_power = (int) ((sum - max - min) * mainsVoltage / (measurements - 2));
sum = 0; sum = 0;
measurements = 0; measurements = 0;
char power[6];
snprintf(power, "%d", 6, _power);
mqttSend(getSetting("emonPowerTopic", EMON_POWER_TOPIC).c_str(), power);
#if ENABLE_DOMOTICZ
domoticzSend("dczPowIdx", power);
#endif
} }
next_measurement += EMON_INTERVAL; next_measurement += EMON_INTERVAL;


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

@ -19,20 +19,13 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include <Arduino.h>
#include "config/all.h" #include "config/all.h"
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// PROTOTYPES
// GLOBALS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
#include <NtpClientLib.h>
#include <ESPAsyncWebServer.h>
#include <AsyncMqttClient.h>
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);
char apibuffer[64];
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// METHODS // METHODS
@ -114,9 +107,6 @@ void setup() {
webSetup(); webSetup();
ntpSetup(); ntpSetup();
#if ENABLE_DOMOTICZ
domoticzSetup();
#endif
#if ENABLE_FAUXMO #if ENABLE_FAUXMO
fauxmoSetup(); fauxmoSetup();
#endif #endif


+ 17
- 0
code/espurna/pow.ino View File

@ -10,6 +10,8 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#if ENABLE_POW #if ENABLE_POW
#include <HLW8012.h> #include <HLW8012.h>
#include <Hash.h>
#include <ArduinoJson.h>
HLW8012 hlw8012; HLW8012 hlw8012;
bool _powEnabled = false; bool _powEnabled = false;
@ -141,6 +143,17 @@ void powSetup() {
// Retrieve calibration values // Retrieve calibration values
powRetrieveCalibration(); powRetrieveCalibration();
// API definitions
apiRegister("/api/power", "power", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", getActivePower());
});
apiRegister("/api/current", "current", [](char * buffer, size_t len) {
dtostrf(getCurrent(), len-1, 2, buffer);
});
apiRegister("/api/voltage", "voltage", [](char * buffer, size_t len) {
snprintf(buffer, len, "%d", getVoltage());
});
} }
void powLoop() { void powLoop() {
@ -211,6 +224,10 @@ void powLoop() {
mqttSend(getSetting("powRPowerTopic", POW_RPOWER_TOPIC).c_str(), String(reactive).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()); mqttSend(getSetting("powPFactorTopic", POW_PFACTOR_TOPIC).c_str(), String(factor).c_str());
#if ENABLE_DOMOTICZ
domoticzSend("dczPowIdx", power);
#endif
power_sum = current_sum = voltage_sum = 0; power_sum = current_sum = voltage_sum = 0;
report_count = POW_REPORT_EVERY; report_count = POW_REPORT_EVERY;


+ 148
- 25
code/espurna/relay.ino View File

@ -9,35 +9,24 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#include <EEPROM.h> #include <EEPROM.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <vector> #include <vector>
#include <functional>
typedef struct { typedef struct {
unsigned char pin; unsigned char pin;
bool reverse; bool reverse;
} relay_t; } relay_t;
std::vector<relay_t> _relays; std::vector<relay_t> _relays;
bool recursive = false;
#ifdef SONOFF_DUAL #ifdef SONOFF_DUAL
unsigned char dualRelayStatus = 0; unsigned char dualRelayStatus = 0;
#endif #endif
bool recursive = false;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// RELAY // RELAY
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void relayMQTT(unsigned char id) {
if (id >= _relays.size()) return;
String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER);
char buffer[strlen(MQTT_RELAY_TOPIC) + mqttGetter.length() + 3];
sprintf(buffer, "%s/%d%s", MQTT_RELAY_TOPIC, id, mqttGetter.c_str());
mqttSend(buffer, relayStatus(id) ? "1" : "0");
}
void relayMQTT() {
for (unsigned int i=0; i < _relays.size(); i++) {
relayMQTT(i);
}
}
String relayString() { String relayString() {
DynamicJsonBuffer jsonBuffer; DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject(); JsonObject& root = jsonBuffer.createObject();
@ -50,11 +39,6 @@ String relayString() {
return output; return output;
} }
void relayWS() {
String output = relayString();
wsSend(output.c_str());
}
bool relayStatus(unsigned char id) { bool relayStatus(unsigned char id) {
#ifdef SONOFF_DUAL #ifdef SONOFF_DUAL
if (id >= 2) return false; if (id >= 2) return false;
@ -91,19 +75,19 @@ bool relayStatus(unsigned char id, bool status, bool report) {
digitalWrite(_relays[id].pin, _relays[id].reverse ? !status : status); digitalWrite(_relays[id].pin, _relays[id].reverse ? !status : status);
#endif #endif
if (report) relayMQTT(id);
if (!recursive) { if (!recursive) {
relaySync(id); relaySync(id);
relaySave(); relaySave();
relayWS();
} }
#ifdef ENABLE_DOMOTICZ #ifdef ENABLE_DOMOTICZ
domoticzSend(id);
relayDomoticzSend(id);
#endif #endif
} }
if (report) relayMQTT(id);
if (!recursive) relayWS();
return changed; return changed;
} }
@ -181,6 +165,134 @@ unsigned char relayCount() {
return _relays.size(); return _relays.size();
} }
//------------------------------------------------------------------------------
// REST API
//------------------------------------------------------------------------------
void relaySetupAPI() {
// API entry points (protected with apikey)
for (unsigned int relayID=0; relayID<relayCount(); relayID++) {
char url[15];
sprintf(url, "/api/relay/%d", relayID);
char key[10];
sprintf(key, "relay%d", relayID);
apiRegister(url, key,
[relayID](char * buffer, size_t len) {
snprintf(buffer, "%d", len, relayStatus(relayID) ? 1 : 0);
},
[relayID](const char * payload) {
unsigned int value = payload[0] - '0';
if (value == 2) {
relayToggle(relayID);
} else {
relayStatus(relayID, value == 1);
}
});
}
}
//------------------------------------------------------------------------------
// WebSockets
//------------------------------------------------------------------------------
void relayWS() {
String output = relayString();
wsSend(output.c_str());
}
//------------------------------------------------------------------------------
// Domoticz
//------------------------------------------------------------------------------
#if ENABLE_DOMOTICZ
void relayDomoticzSend(unsigned int relayID) {
char buffer[15];
sprintf(buffer, "dczRelayIdx%d", relayID);
domoticzSend(buffer, relayStatus(relayID) ? "1" : "0");
}
int relayFromIdx(unsigned int idx) {
for (int relayID=0; relayID<relayCount(); relayID++) {
if (relayToIdx(relayID) == idx) {
return relayID;
}
}
return -1;
}
int relayToIdx(unsigned int relayID) {
char buffer[15];
sprintf(buffer, "dczRelayIdx%d", relayID);
return getSetting(buffer).toInt();
}
void relayDomoticzSetup() {
mqttRegister([](unsigned int type, const char * topic, const char * payload) {
String dczTopicOut = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
if (type == MQTT_CONNECT_EVENT) {
mqttSubscribeRaw(dczTopicOut.c_str());
}
if (type == MQTT_MESSAGE_EVENT) {
// Check topic
if (dczTopicOut.equals(topic)) {
// Parse response
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.parseObject((char *) payload);
if (!root.success()) {
DEBUG_MSG("[DOMOTICZ] Error parsing data\n");
return;
}
// IDX
unsigned long idx = root["idx"];
int relayID = relayFromIdx(idx);
if (relayID >= 0) {
unsigned long value = root["nvalue"];
DEBUG_MSG("[DOMOTICZ] Received value %d for IDX %d\n", value, idx);
relayStatus(relayID, value == 1);
}
}
}
});
}
#endif
//------------------------------------------------------------------------------
// MQTT
//------------------------------------------------------------------------------
void relayMQTT(unsigned char id) {
if (id >= _relays.size()) return;
String mqttGetter = getSetting("mqttGetter", MQTT_USE_GETTER);
char buffer[strlen(MQTT_RELAY_TOPIC) + mqttGetter.length() + 3];
sprintf(buffer, "%s/%d%s", MQTT_RELAY_TOPIC, id, mqttGetter.c_str());
mqttSend(buffer, relayStatus(id) ? "1" : "0");
}
void relayMQTT() {
for (unsigned int i=0; i < _relays.size(); i++) {
relayMQTT(i);
}
}
void relayMQTTCallback(unsigned int type, const char * topic, const char * payload) { void relayMQTTCallback(unsigned int type, const char * topic, const char * payload) {
String mqttSetter = getSetting("mqttSetter", MQTT_USE_SETTER); String mqttSetter = getSetting("mqttSetter", MQTT_USE_SETTER);
@ -220,6 +332,14 @@ void relayMQTTCallback(unsigned int type, const char * topic, const char * paylo
} }
void relaySetupMQTT() {
mqttRegister(relayMQTTCallback);
}
//------------------------------------------------------------------------------
// Setup
//------------------------------------------------------------------------------
void relaySetup() { void relaySetup() {
#ifdef SONOFF_DUAL #ifdef SONOFF_DUAL
@ -253,10 +373,13 @@ void relaySetup() {
if (relayMode == RELAY_MODE_OFF) relayStatus(i, false); if (relayMode == RELAY_MODE_OFF) relayStatus(i, false);
if (relayMode == RELAY_MODE_ON) relayStatus(i, true); if (relayMode == RELAY_MODE_ON) relayStatus(i, true);
} }
if (relayMode == RELAY_MODE_SAME) relayRetrieve(); if (relayMode == RELAY_MODE_SAME) relayRetrieve();
mqttRegister(relayMQTTCallback);
relaySetupAPI();
relaySetupMQTT();
#if ENABLE_DOMOTICZ
relayDomoticzSetup();
#endif
DEBUG_MSG("[RELAY] Number of relays: %d\n", _relays.size()); DEBUG_MSG("[RELAY] Number of relays: %d\n", _relays.size());


+ 154
- 93
code/espurna/web.ino View File

@ -14,17 +14,25 @@ Copyright (C) 2016-2017 by Xose Pérez <xose dot perez at gmail dot com>
#include <AsyncJson.h> #include <AsyncJson.h>
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <Ticker.h> #include <Ticker.h>
#include <vector>
AsyncWebServer server(80); AsyncWebServer server(80);
AsyncWebSocket ws("/ws"); AsyncWebSocket ws("/ws");
Ticker deferred;
typedef struct { typedef struct {
IPAddress ip; IPAddress ip;
unsigned long timestamp = 0; unsigned long timestamp = 0;
} ws_ticket_t; } ws_ticket_t;
ws_ticket_t _ticket[WS_BUFFER_SIZE]; ws_ticket_t _ticket[WS_BUFFER_SIZE];
Ticker deferred;
typedef struct {
char * url;
char * key;
apiGetCallbackFunction getFn = NULL;
apiPutCallbackFunction putFn = NULL;
} web_api_t;
std::vector<web_api_t> _apis;
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// WEBSOCKETS // WEBSOCKETS
@ -101,7 +109,7 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
bool fauxmoEnabled = false; bool fauxmoEnabled = false;
#endif #endif
unsigned int network = 0; unsigned int network = 0;
unsigned int dczIdx = 0;
unsigned int dczRelayIdx = 0;
String adminPass; String adminPass;
for (unsigned int i=0; i<config.size(); i++) { for (unsigned int i=0; i<config.size(); i++) {
@ -137,10 +145,10 @@ void _wsParse(uint32_t client_id, uint8_t * payload, size_t length) {
#if ENABLE_DOMOTICZ #if ENABLE_DOMOTICZ
if (key == "dczIdx") {
if (dczIdx >= relayCount()) continue;
key = key + String(dczIdx);
++dczIdx;
if (key == "dczRelayIdx") {
if (dczRelayIdx >= relayCount()) continue;
key = key + String(dczRelayIdx);
++dczRelayIdx;
} }
#else #else
@ -319,11 +327,28 @@ void _wsStart(uint32_t client_id) {
root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC); root["dczTopicIn"] = getSetting("dczTopicIn", DOMOTICZ_IN_TOPIC);
root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC); root["dczTopicOut"] = getSetting("dczTopicOut", DOMOTICZ_OUT_TOPIC);
JsonArray& dczIdx = root.createNestedArray("dczIdx");
JsonArray& dczRelayIdx = root.createNestedArray("dczRelayIdx");
for (byte i=0; i<relayCount(); i++) { for (byte i=0; i<relayCount(); i++) {
dczIdx.add(domoticzIdx(i));
dczRelayIdx.add(relayToIdx(i));
} }
#if ENABLE_DHT
root["dczTmpIdx"] = getSetting("dczTmpIdx").toInt();
root["dczHumIdx"] = getSetting("dczHumIdx").toInt();
#endif
#if ENABLE_DS18B20
root["dczTmpIdx"] = getSetting("dczTmpIdx").toInt();
#endif
#if ENABLE_EMON
root["dczPowIdx"] = getSetting("dczPowIdx").toInt();
#endif
#if ENABLE_POW
root["dczPowIdx"] = getSetting("dczPowIdx").toInt();
#endif
#endif #endif
#if ENABLE_FAUXMO #if ENABLE_FAUXMO
@ -453,7 +478,7 @@ void _wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTy
// WEBSERVER // WEBSERVER
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
void _logRequest(AsyncWebServerRequest *request) {
void webLogRequest(AsyncWebServerRequest *request) {
DEBUG_MSG("[WEBSERVER] Request: %s %s\n", request->methodToString(), request->url().c_str()); DEBUG_MSG("[WEBSERVER] Request: %s %s\n", request->methodToString(), request->url().c_str());
} }
@ -464,46 +489,7 @@ bool _authenticate(AsyncWebServerRequest *request) {
return request->authenticate(HTTP_USERNAME, httpPassword); return request->authenticate(HTTP_USERNAME, httpPassword);
} }
void _onAuth(AsyncWebServerRequest *request) {
_logRequest(request);
if (!_authenticate(request)) return request->requestAuthentication();
IPAddress ip = request->client()->remoteIP();
unsigned long now = millis();
unsigned short index;
for (index = 0; index < WS_BUFFER_SIZE; index++) {
if (_ticket[index].ip == ip) break;
if (_ticket[index].timestamp == 0) break;
if (now - _ticket[index].timestamp > WS_TIMEOUT) break;
}
if (index == WS_BUFFER_SIZE) {
request->send(423);
} else {
_ticket[index].ip = ip;
_ticket[index].timestamp = now;
request->send(204);
}
}
void _onHome(AsyncWebServerRequest *request) {
_logRequest(request);
if (!_authenticate(request)) return request->requestAuthentication();
String password = getSetting("adminPass", ADMIN_PASS);
if (password.equals(ADMIN_PASS)) {
request->send(SPIFFS, "/password.html");
} else {
request->send(SPIFFS, "/index.html");
}
}
bool _apiAuth(AsyncWebServerRequest *request) {
bool _authAPI(AsyncWebServerRequest *request) {
if (getSetting("apiEnabled").toInt() == 0) { if (getSetting("apiEnabled").toInt() == 0) {
DEBUG_MSG("[WEBSERVER] HTTP API is not enabled\n"); DEBUG_MSG("[WEBSERVER] HTTP API is not enabled\n");
@ -528,11 +514,68 @@ bool _apiAuth(AsyncWebServerRequest *request) {
} }
void _onRelay(AsyncWebServerRequest *request) {
ArRequestHandlerFunction _bindAPI(unsigned int apiID) {
return [apiID](AsyncWebServerRequest *request) {
webLogRequest(request);
if (!_authAPI(request)) return;
bool asJson = false;
if (request->hasHeader("Accept")) {
AsyncWebHeader* h = request->getHeader("Accept");
asJson = h->value().equals("application/json");
}
web_api_t api = _apis[apiID];
if (request->method() == HTTP_PUT) {
if (request->hasParam("value", true)) {
AsyncWebParameter* p = request->getParam("value", true);
(api.putFn)((p->value()).c_str());
}
}
_logRequest(request);
char value[10];
(api.getFn)(value, 10);
if (!_apiAuth(request)) return;
// jump over leading spaces
char *p = value;
while ((unsigned char) *p == ' ') ++p;
if (asJson) {
char buffer[64];
sprintf_P(buffer, PSTR("{ \"%s\": %s }"), api.key, p);
request->send(200, "application/json", buffer);
} else {
request->send(200, "text/plain", p);
}
};
}
void apiRegister(const char * url, const char * key, apiGetCallbackFunction getFn, apiPutCallbackFunction putFn) {
// Store it
web_api_t api;
api.url = strdup(url);
api.key = strdup(key);
api.getFn = getFn;
api.putFn = putFn;
_apis.push_back(api);
// Bind call
unsigned int methods = HTTP_GET;
if (putFn != NULL) methods += HTTP_PUT;
server.on(url, methods, _bindAPI(_apis.size() - 1));
}
void _onAPIs(AsyncWebServerRequest *request) {
webLogRequest(request);
if (!_authAPI(request)) return;
bool asJson = false; bool asJson = false;
if (request->hasHeader("Accept")) { if (request->hasHeader("Accept")) {
@ -542,55 +585,80 @@ void _onRelay(AsyncWebServerRequest *request) {
String output; String output;
if (asJson) { if (asJson) {
output = relayString();
DynamicJsonBuffer jsonBuffer;
JsonObject& root = jsonBuffer.createObject();
for (unsigned int i=0; i < _apis.size(); i++) {
root[_apis[i].key] = _apis[i].url;
}
root.printTo(output);
request->send(200, "application/json", output); request->send(200, "application/json", output);
} else { } else {
for (unsigned int i=0; i<relayCount(); i++) {
output += "Relay #" + String(i) + String(": ") + String(relayStatus(i) ? "1" : "0") + "\n";
for (unsigned int i=0; i < _apis.size(); i++) {
output += _apis[i].key + String(" -> ") + _apis[i].url + String("\n<br />");
} }
request->send(200, "text/plain", output); request->send(200, "text/plain", output);
} }
};
}
ArRequestHandlerFunction _onRelayStatusWrapper(unsigned int relayID) {
void _onHome(AsyncWebServerRequest *request) {
return [relayID](AsyncWebServerRequest *request) {
DEBUG_MSG("[DEBUG] Free heap: %d bytes\n", ESP.getFreeHeap());
_logRequest(request);
FSInfo fs_info;
if (SPIFFS.info(fs_info)) {
DEBUG_MSG("[DEBUG] File system total size: %d bytes\n", fs_info.totalBytes);
DEBUG_MSG(" used size : %d bytes\n", fs_info.usedBytes);
DEBUG_MSG(" block size: %d bytes\n", fs_info.blockSize);
DEBUG_MSG(" page size : %d bytes\n", fs_info.pageSize);
DEBUG_MSG(" max files : %d\n", fs_info.maxOpenFiles);
DEBUG_MSG(" max length: %d\n", fs_info.maxPathLength);
} else {
DEBUG_MSG("[DEBUG] Error, FS not accesible!\n");
}
if (!_apiAuth(request)) return;
webLogRequest(request);
if (request->method() == HTTP_PUT) {
if (request->hasParam("status", true)) {
AsyncWebParameter* p = request->getParam("status", true);
unsigned int value = p->value().toInt();
if (value == 2) {
relayToggle(relayID);
} else {
relayStatus(relayID, value == 1);
}
}
}
if (!_authenticate(request)) return request->requestAuthentication();
bool asJson = false;
if (request->hasHeader("Accept")) {
AsyncWebHeader* h = request->getHeader("Accept");
asJson = h->value().equals("application/json");
}
String password = getSetting("adminPass", ADMIN_PASS);
if (password.equals(ADMIN_PASS)) {
request->send(SPIFFS, "/password.html");
} else {
request->send(SPIFFS, "/index.html");
}
String output;
if (asJson) {
output = String("{\"relayStatus\": ") + String(relayStatus(relayID) ? "1" : "0") + "}";
request->send(200, "application/json", output);
} else {
request->send(200, "text/plain", relayStatus(relayID) ? "1" : "0");
}
}
};
void _onAuth(AsyncWebServerRequest *request) {
webLogRequest(request);
if (!_authenticate(request)) return request->requestAuthentication();
IPAddress ip = request->client()->remoteIP();
unsigned long now = millis();
unsigned short index;
for (index = 0; index < WS_BUFFER_SIZE; index++) {
if (_ticket[index].ip == ip) break;
if (_ticket[index].timestamp == 0) break;
if (now - _ticket[index].timestamp > WS_TIMEOUT) break;
}
if (index == WS_BUFFER_SIZE) {
request->send(423);
} else {
_ticket[index].ip = ip;
_ticket[index].timestamp = now;
request->send(204);
}
} }
AsyncWebServer * getServer() {
return &server;
}
void webSetup() { void webSetup() {
// Setup websocket // Setup websocket
@ -604,14 +672,7 @@ void webSetup() {
server.on("/", HTTP_GET, _onHome); server.on("/", HTTP_GET, _onHome);
server.on("/index.html", HTTP_GET, _onHome); server.on("/index.html", HTTP_GET, _onHome);
server.on("/auth", HTTP_GET, _onAuth); server.on("/auth", HTTP_GET, _onAuth);
// API entry points (protected with apikey)
for (unsigned int relayID=0; relayID<relayCount(); relayID++) {
char buffer[15];
sprintf(buffer, "/api/relay/%d", relayID);
server.on(buffer, HTTP_GET + HTTP_PUT, _onRelayStatusWrapper(relayID));
}
server.on("/api/relay", HTTP_GET, _onRelay);
server.on("/apis", HTTP_GET, _onAPIs);
// Serve static files // Serve static files
char lastModified[50]; char lastModified[50];


+ 4
- 4
code/html/custom.js View File

@ -128,7 +128,7 @@ function createIdxs(count) {
for (var id=0; id<count; id++) { for (var id=0; id<count; id++) {
var line = $(template).clone(); var line = $(template).clone();
$(line).find("input").each(function() { $(line).find("input").each(function() {
$(this).attr("data", id).attr("tabindex", 43+id);
$(this).attr("data", id).attr("tabindex", 40+id);
}); });
if (count > 1) $(".id", line).html(" " + id); if (count > 1) $(".id", line).html(" " + id);
line.appendTo("#idxs"); line.appendTo("#idxs");
@ -264,12 +264,12 @@ function processData(data) {
} }
// Domoticz // Domoticz
if (key == "dczIdx") {
var idxs = data.dczIdx;
if (key == "dczRelayIdx") {
var idxs = data.dczRelayIdx;
createIdxs(idxs.length); createIdxs(idxs.length);
for (var i in idxs) { for (var i in idxs) {
var element = $(".dczIdx[data=" + i + "]");
var element = $(".dczRelayIdx[data=" + i + "]");
if (element.length > 0) element.val(idxs[i]); if (element.length > 0) element.val(idxs[i]);
} }


+ 32
- 7
code/html/index.html View File

@ -265,7 +265,11 @@
<input name="apiKey" class="pure-u-3-4 pure-u-md-1-2" type="text" tabindex="13" /> <input name="apiKey" class="pure-u-3-4 pure-u-md-1-2" type="text" tabindex="13" />
<div class=" pure-u-1-4 pure-u-md-1-4"><button class="pure-button button-apikey pure-u-23-24">Generate</button></div> <div class=" pure-u-1-4 pure-u-md-1-4"><button class="pure-button button-apikey pure-u-23-24">Generate</button></div>
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div> <div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">This is the key you will have to pass with every HTTP request to the API, either to get or write values.</div>
<div class="pure-u-1 pure-u-md-3-4 hint">
This is the key you will have to pass with every HTTP request to the API, either to get or write values.<br />
All API calls must contain the <strong>apikey</strong> parameter with the value above.<br />
To know what APIs are enabled do a call to <strong>/apis</strong>.
</div>
</div> </div>
</fieldset> </fieldset>
@ -327,12 +331,15 @@
<label class="pure-u-1 pure-u-md-1-4" for="mqttTopic">MQTT Root Topic</label> <label class="pure-u-1 pure-u-md-1-4" for="mqttTopic">MQTT Root Topic</label>
<input class="pure-u-1 pure-u-md-3-4" name="mqttTopic" type="text" size="20" tabindex="25" /> <input class="pure-u-1 pure-u-md-3-4" name="mqttTopic" type="text" size="20" tabindex="25" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div> <div class="pure-u-0 pure-u-md-1-4">&nbsp;</div>
<div class="pure-u-1 pure-u-md-3-4 hint">This is the root topic for this device. The {identifier} placeholder will be replaces by the device hostname.<br />
<div class="pure-u-1 pure-u-md-3-4 hint">
This is the root topic for this device. The {identifier} placeholder will be replaces by the device hostname.<br />
- <strong>&lt;root&gt;/relay/#</strong> Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the relay ID (starting from 0). If the board has only one relay it will be 0.<br /> - <strong>&lt;root&gt;/relay/#</strong> Send a 0 or a 1 as a payload to this topic to switch it on or off. You can also send a 2 to toggle its current state. Replace # with the relay ID (starting from 0). If the board has only one relay it will be 0.<br />
- <strong>&lt;root&gt;/led/#</strong> Send a 0 or a 1 as a payload to this topic to set the onboard LED to the given state, send a 3 to turn it back to WIFI indicator. Replace # with the LED ID (starting from 0). If the board has only one LED it will be 0.<br /> - <strong>&lt;root&gt;/led/#</strong> Send a 0 or a 1 as a payload to this topic to set the onboard LED to the given state, send a 3 to turn it back to WIFI indicator. Replace # with the LED ID (starting from 0). If the board has only one LED it will be 0.<br />
- <strong>&lt;root&gt;/button/#</strong> For each button in the board subscribe to this topic to know when it is pressed (payload 1) or released (payload 0).<br />
- <strong>&lt;root&gt;/ip</strong> The device will report to this topic its IP.<br /> - <strong>&lt;root&gt;/ip</strong> The device will report to this topic its IP.<br />
- <strong>&lt;root&gt;/version</strong> The device will report to this topic its firmware version on boot.<br /> - <strong>&lt;root&gt;/version</strong> The device will report to this topic its firmware version on boot.<br />
- <strong>&lt;root&gt;/status</strong> The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0. - <strong>&lt;root&gt;/status</strong> The device will report a 1 to this topic every few minutes. Upon MQTT disconnecting this will be set to 0.
</div>
</div> </div>
</fieldset> </fieldset>
@ -363,6 +370,24 @@
<input class="pure-u-1 pure-u-md-3-4" name="dczTopicOut" type="text" tabindex="32" placeholder="domoticz/out" /> <input class="pure-u-1 pure-u-md-3-4" name="dczTopicOut" type="text" tabindex="32" placeholder="domoticz/out" />
</div> </div>
<div class="pure-g modude 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">
<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">
<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>
</div>
<div id="idxs"> <div id="idxs">
</div> </div>
@ -386,21 +411,21 @@
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedPower">AC RMS Active Power</label> <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" />
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedPower" type="text" size="8" tabindex="51" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div> <div class="pure-u-0 pure-u-md-1-4">&nbsp;</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 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>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedVoltage">AC RMS Voltage</label> <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" />
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedVoltage" type="text" size="8" tabindex="52" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div> <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 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>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-md-1-4" for="powExpectedCurrent">AC RMS Current</label> <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" />
<input class="pure-u-1 pure-u-md-3-4 powExpected" name="powExpectedCurrent" type="text" size="8" tabindex="55" placeholder="0" />
<div class="pure-u-0 pure-u-md-1-4">&nbsp;</div> <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 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>
@ -479,8 +504,8 @@
<div id="idxTemplate" class="template"> <div id="idxTemplate" class="template">
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-sm-1-4">Relay<span class="id"></span> IDX</label>
<div class="pure-u-1 pure-u-sm-1-8"><input class="pure-u-sm-23-24 dczIdx" name="dczIdx" type="number" min="0" tabindex="0" data="0" /></div>
<label class="pure-u-1 pure-u-sm-1-4" for="dczRelayIdx">Relay<span class="id"></span> IDX</label>
<div class="pure-u-1 pure-u-sm-1-8"><input class="pure-u-sm-23-24 dczRelayIdx" name="dczRelayIdx" type="number" min="0" tabindex="0" data="0" /></div>
<div class="pure-u-1 pure-u-sm-5-8 hint center">Set to 0 to disable notifications.</div> <div class="pure-u-1 pure-u-sm-5-8 hint center">Set to 0 to disable notifications.</div>
</div> </div>
</div> </div>


Loading…
Cancel
Save