Browse Source

Merge branch 'dev' of github.com:xoseperez/espurna into dev

ota
Xose Pérez 6 years ago
parent
commit
f2a620649c
73 changed files with 327 additions and 94 deletions
  1. +2
    -2
      README.md
  2. +0
    -4
      code/espurna/config/all.h
  3. +1
    -0
      code/espurna/config/arduino.h
  4. +24
    -0
      code/espurna/config/hardware.h
  5. +20
    -0
      code/espurna/config/prototypes.h
  6. +15
    -0
      code/espurna/migrate.ino
  7. +0
    -9
      code/espurna/utils.ino
  8. +60
    -14
      code/html/custom.css
  9. +79
    -19
      code/html/custom.js
  10. +79
    -40
      code/html/index.html
  11. +18
    -4
      code/ota.py
  12. +29
    -2
      code/platformio.ini
  13. BIN
      images/devices/aithinker-ai-light.jpg
  14. BIN
      images/devices/arilux-al-lc01.jpg
  15. BIN
      images/devices/arilux-al-lc06.jpg
  16. BIN
      images/devices/arilux-e27.jpg
  17. BIN
      images/devices/authometion-lyt8266.jpg
  18. BIN
      images/devices/electrodragon-wifi-iot.jpg
  19. BIN
      images/devices/exs-wifi-relay-v31.jpg
  20. BIN
      images/devices/geiger_espurna_configuration.png
  21. BIN
      images/devices/geiger_espurna_status.png
  22. BIN
      images/devices/geiger_grafana_dashboard.png
  23. BIN
      images/devices/geiger_scope_following_pulses.png
  24. BIN
      images/devices/geiger_scope_single_pulse.png
  25. BIN
      images/devices/geiger_wiring_diagram.png
  26. BIN
      images/devices/generic-ag-l4-1.jpg
  27. BIN
      images/devices/generic-ag-l4-2.jpg
  28. BIN
      images/devices/generic-ag-l4-3.jpg
  29. BIN
      images/devices/generic-ag-l4-4.jpg
  30. BIN
      images/devices/generic-ag-l4-5.jpg
  31. BIN
      images/devices/generic-geiger-diy.png
  32. BIN
      images/devices/generic-relay-40.jpg
  33. BIN
      images/devices/generic-rgbled-10.jpg
  34. BIN
      images/devices/generic-v9261f.jpg
  35. BIN
      images/devices/heygo-hy02.jpg
  36. BIN
      images/devices/huacanxing-h801.jpg
  37. BIN
      images/devices/intermittech-quinled-2.6.jpg
  38. BIN
      images/devices/itead-1ch-inching.jpg
  39. BIN
      images/devices/itead-bn-sz01.jpg
  40. BIN
      images/devices/itead-motor.jpg
  41. BIN
      images/devices/itead-s20.jpg
  42. BIN
      images/devices/itead-s26.jpg
  43. BIN
      images/devices/itead-slampher.jpg
  44. BIN
      images/devices/itead-sonoff-4ch-pro.jpg
  45. BIN
      images/devices/itead-sonoff-4ch.jpg
  46. BIN
      images/devices/itead-sonoff-b1.jpg
  47. BIN
      images/devices/itead-sonoff-basic.jpg
  48. BIN
      images/devices/itead-sonoff-dual.jpg
  49. BIN
      images/devices/itead-sonoff-ifan02.jpg
  50. BIN
      images/devices/itead-sonoff-led.jpg
  51. BIN
      images/devices/itead-sonoff-pow.jpg
  52. BIN
      images/devices/itead-sonoff-rf.jpg
  53. BIN
      images/devices/itead-sonoff-sv.jpg
  54. BIN
      images/devices/itead-sonoff-t1.jpg
  55. BIN
      images/devices/itead-sonoff-th.jpg
  56. BIN
      images/devices/jangoe-wifi-relay.jpg
  57. BIN
      images/devices/jorgegarcia-wifi-relays.jpg
  58. BIN
      images/devices/kmc-70011.jpg
  59. BIN
      images/devices/lingan-swa1.jpg
  60. BIN
      images/devices/lohas-9w.jpg
  61. BIN
      images/devices/magichome-led-controller.jpg
  62. BIN
      images/devices/mancavemade-esp-live.jpg
  63. BIN
      images/devices/neo-coolcam-wifi.jpg
  64. BIN
      images/devices/nodemcu-lolin-v3.jpg
  65. BIN
      images/devices/openenergymonitor-mqtt-relay.jpg
  66. BIN
      images/devices/schuko-wifi-plug.jpg
  67. BIN
      images/devices/tinkerman-espurna-h.jpg
  68. BIN
      images/devices/tonbux-powerstrip02.jpg
  69. BIN
      images/devices/wemos-d1-mini-relayshield.jpg
  70. BIN
      images/devices/wion-50055.jpg
  71. BIN
      images/devices/witty-cloud.jpg
  72. BIN
      images/devices/workchoice-ecoplug.jpg
  73. BIN
      images/devices/xenon-sm-pw702u.jpg

+ 2
- 2
README.md View File

@ -4,8 +4,8 @@ ESPurna ("spark" in Catalan) is a custom firmware for ESP8285/ESP8266 based smar
It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries. It uses the Arduino Core for ESP8266 framework and a number of 3rd party libraries.
[![version](https://img.shields.io/badge/version-1.13.3a-brightgreen.svg)](CHANGELOG.md) [![version](https://img.shields.io/badge/version-1.13.3a-brightgreen.svg)](CHANGELOG.md)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.com/xoseperez/espurna/tree/dev/)
[![travis](https://travis-ci.org/xoseperez/espurna.svg?branch=dev)](https://travis-ci.org/xoseperez/espurna)
[![branch](https://img.shields.io/badge/branch-dev-orange.svg)](https://github.com/ColinShorts/espurna.git/tree/dev/)
[![travis](https://travis-ci.org/ColinShorts/espurna.git.svg?branch=dev)](https://travis-ci.org/ColinShorts/espurna.git)
[![codacy](https://api.codacy.com/project/badge/Grade/c9496e25cf07434cba786b462cb15f49)](https://www.codacy.com/app/xoseperez/espurna/dashboard) [![codacy](https://api.codacy.com/project/badge/Grade/c9496e25cf07434cba786b462cb15f49)](https://www.codacy.com/app/xoseperez/espurna/dashboard)
[![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE) [![license](https://img.shields.io/github/license/xoseperez/espurna.svg)](LICENSE)
<br /> <br />


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

@ -35,7 +35,3 @@
#include "sensors.h" #include "sensors.h"
#include "webui.h" #include "webui.h"
#include "progmem.h" #include "progmem.h"
#ifdef USE_CORE_VERSION_H
#include "core_version.h"
#endif

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

@ -98,6 +98,7 @@
//#define XIAOMI_SMART_DESK_LAMP //#define XIAOMI_SMART_DESK_LAMP
//#define ALLTERCO_SHELLY2 //#define ALLTERCO_SHELLY2
//#define PHYX_ESP12_RGB //#define PHYX_ESP12_RGB
//#define IWOOLE_LED_TABLE_LAMP
//-------------------------------------------------------------------------------- //--------------------------------------------------------------------------------
// Features (values below are non-default values) // Features (values below are non-default values)


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

@ -2765,6 +2765,30 @@
#define LIGHT_CH2_INVERSE 0 #define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0 #define LIGHT_CH3_INVERSE 0
// -----------------------------------------------------------------------------
// iWoole LED Table Lamp
// http://iwoole.com/newst-led-smart-night-light-7w-smart-table-light-rgbw-wifi-app-remote-control-110v-220v-us-eu-plug-smart-lamp-google-home-decore-p00022p1.html
// -----------------------------------------------------------------------------
#elif defined(IWOOLE_LED_TABLE_LAMP)
// Info
#define MANUFACTURER "IWOOLE"
#define DEVICE "LED_TABLE_LAMP"
#define RELAY_PROVIDER RELAY_PROVIDER_LIGHT
#define LIGHT_PROVIDER LIGHT_PROVIDER_DIMMER
#define DUMMY_RELAY_COUNT 1
// Light
#define LIGHT_CHANNELS 4
#define LIGHT_CH1_PIN 12 // RED
#define LIGHT_CH2_PIN 5 // GREEN
#define LIGHT_CH3_PIN 14 // BLUE
#define LIGHT_CH4_PIN 4 // WHITE
#define LIGHT_CH1_INVERSE 0
#define LIGHT_CH2_INVERSE 0
#define LIGHT_CH3_INVERSE 0
#define LIGHT_CH4_INVERSE 0
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEST boards (do not use!!) // TEST boards (do not use!!)


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

@ -2,6 +2,7 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <functional> #include <functional>
#include <pgmspace.h> #include <pgmspace.h>
#include <core_version.h>
extern "C" { extern "C" {
#include "user_interface.h" #include "user_interface.h"
@ -35,6 +36,25 @@ extern "C" {
void custom_crash_callback(struct rst_info*, uint32_t, uint32_t); void custom_crash_callback(struct rst_info*, uint32_t, uint32_t);
} }
// Core version 2.4.2 and higher changed the cont_t structure to a pointer:
// https://github.com/esp8266/Arduino/commit/5d5ea92a4d004ab009d5f642629946a0cb8893dd#diff-3fa12668b289ccb95b7ab334833a4ba8L35
// Core version 2.5.0 introduced EspClass helper method:
// https://github.com/esp8266/Arduino/commit/0e0e34c614fe8a47544c9998201b1d9b3c24eb18
extern "C" {
#include <cont.h>
#if defined(ARDUINO_ESP8266_RELEASE_2_3_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_0) \
|| defined(ARDUINO_ESP8266_RELEASE_2_4_1)
extern cont_t g_cont;
#define getFreeStack() cont_get_free_stack(&g_cont)
#elif defined(ARDUINO_ESP8266_RELEASE_2_4_2)
extern cont_t* g_pcont;
#define getFreeStack() cont_get_free_stack(g_pcont)
#else
#define getFreeStack() ESP.getFreeContStack()
#endif
}
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Domoticz // Domoticz
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------


+ 15
- 0
code/espurna/migrate.ino View File

@ -1189,6 +1189,21 @@ void migrate() {
setSetting("chLogic", 1, 0); setSetting("chLogic", 1, 0);
setSetting("chLogic", 3, 0); setSetting("chLogic", 3, 0);
#elif defined(IWOOLE_LED_TABLE_LAMP)
setSetting("board", 90);
setSetting("relayProvider", RELAY_PROVIDER_LIGHT);
setSetting("lightProvider", LIGHT_PROVIDER_DIMMER);
setSetting("chGPIO", 0, 12);
setSetting("chGPIO", 1, 5);
setSetting("chGPIO", 2, 14);
setSetting("chGPIO", 3, 4);
setSetting("chLogic", 0, 0);
setSetting("chLogic", 1, 0);
setSetting("chLogic", 2, 0);
setSetting("chLogic", 3, 0);
setSetting("relays", 1);
#else #else
// Allow users to define new settings without migration config // Allow users to define new settings without migration config


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

@ -6,11 +6,6 @@ Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
*/ */
extern "C" {
#include <cont.h>
extern cont_t g_cont;
}
#include <Ticker.h> #include <Ticker.h>
Ticker _defer_reset; Ticker _defer_reset;
@ -81,10 +76,6 @@ unsigned int getUsedHeap() {
return getInitialFreeHeap() - getFreeHeap(); return getInitialFreeHeap() - getFreeHeap();
} }
unsigned int getFreeStack() {
return cont_get_free_stack(&g_cont);
}
String getEspurnaModules() { String getEspurnaModules() {
return FPSTR(espurna_modules); return FPSTR(espurna_modules);
} }


+ 60
- 14
code/html/custom.css View File

@ -50,10 +50,6 @@ h2 {
display: block; display: block;
} }
.content {
margin: 0;
}
.page { .page {
margin-top: 10px; margin-top: 10px;
} }
@ -98,16 +94,12 @@ div.center {
display: none; display: none;
} }
#credentials {
font-size: 200%;
height: 100px;
left: 50%;
margin-left: -200px;
margin-top: -50px;
position: fixed;
text-align: center;
top: 50%;
width: 400px;
.content #password {
margin: 0 auto;
}
.content #layout {
margin: 0;
} }
div.state { div.state {
@ -206,6 +198,10 @@ div.state {
background: rgb(255, 128, 0); /* orange */ background: rgb(255, 128, 0); /* orange */
} }
.button-generate-password {
background: rgb(66, 184, 221); /* blue */
}
.button-upgrade-browse, .button-upgrade-browse,
.button-clear-filters, .button-clear-filters,
.button-clear-messages, .button-clear-messages,
@ -448,3 +444,53 @@ table.dataTable.display tbody td {
height: 400px; height: 400px;
margin-bottom: 10px; margin-bottom: 10px;
} }
/* -----------------------------------------------------------------------------
Password input controls
-------------------------------------------------------------------------- */
.password-reveal {
font-family: EmojiSymbols,Segoe UI Symbol;
background: rgba(0,0,0,0);
display: inline-block;
float: right;
z-index: 50;
margin-top: 6px;
margin-left: -30px;
vertical-align: middle;
font-size: 1.2em;
height: 100%;
}
.password-reveal:after {
content: "👁";
}
input[type="password"] + .password-reveal {
color: rgba(205, 205, 205, 0.3);
}
input[type="text"] + .password-reveal {
color: rgba(66, 184, 221, 0.8);
}
.no-select {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
input::-ms-clear,
input::-ms-reveal {
display: none;
}
/* css minifier must not combine these.
* style will not apply otherwise */
input::-ms-input-placeholder {
color: #ccd;
}
input::placeholder {
color: #ccc;
}

+ 79
- 19
code/html/custom.js View File

@ -146,8 +146,7 @@ function loadTimeZones() {
} }
function validateForm(form) {
function validatePassword(password) {
// http://www.the-art-of-web.com/javascript/validate-password/ // http://www.the-art-of-web.com/javascript/validate-password/
// at least one lowercase and one uppercase letter or number // at least one lowercase and one uppercase letter or number
// at least eight characters (letters, numbers or special characters) // at least eight characters (letters, numbers or special characters)
@ -155,16 +154,26 @@ function validateForm(form) {
// MUST be 8..63 printable ASCII characters. See: // MUST be 8..63 printable ASCII characters. See:
// https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution) // https://en.wikipedia.org/wiki/Wi-Fi_Protected_Access#Target_users_(authentication_key_distribution)
// https://github.com/xoseperez/espurna/issues/1151 // https://github.com/xoseperez/espurna/issues/1151
var re_password = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{8,63}$/; var re_password = /^(?=.*[A-Z\d])(?=.*[a-z])[\w~!@#$%^&*\(\)<>,.\?;:{}\[\]\\|]{8,63}$/;
return (
(password !== undefined)
&& (typeof password === "string")
&& (password.length > 0)
&& re_password.test(password)
);
}
function validateForm(form) {
// password // password
var adminPass1 = $("input[name='adminPass']", form).first().val(); var adminPass1 = $("input[name='adminPass']", form).first().val();
if (adminPass1.length > 0 && !re_password.test(adminPass1)) {
if (!validatePassword(adminPass1)) {
alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!"); alert("The password you have entered is not valid, it must be 8..63 characters and have at least 1 lowercase and 1 uppercase / number!");
return false; return false;
} }
var adminPass2 = $("input[name='adminPass']", form).last().val();
var adminPass2 = $("input[name='adminPass_confirm']", form).last().val();
if (adminPass1 !== adminPass2) { if (adminPass1 !== adminPass2) {
alert("Passwords are different!"); alert("Passwords are different!");
return false; return false;
@ -225,6 +234,12 @@ function addValue(data, name, value) {
"node", "key", "topic" "node", "key", "topic"
]; ];
// join both adminPass and ..._confirm
if (name.startsWith("adminPass")) {
name = "adminPass";
}
if (name in data) { if (name in data) {
if (!Array.isArray(data[name])) { if (!Array.isArray(data[name])) {
data[name] = [data[name]]; data[name] = [data[name]];
@ -260,26 +275,67 @@ function getData(form) {
} }
function randomString(length, chars) {
var mask = "";
if (chars.indexOf("a") > -1) { mask += "abcdefghijklmnopqrstuvwxyz"; }
if (chars.indexOf("A") > -1) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
if (chars.indexOf("#") > -1) { mask += "0123456789"; }
if (chars.indexOf("@") > -1) { mask += "ABCDEF"; }
if (chars.indexOf("!") > -1) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
var result = "";
for (var i = length; i > 0; --i) {
result += mask[Math.round(Math.random() * (mask.length - 1))];
function randomString(length, args) {
if (typeof args === "undefined") {
args = {
lowercase: true,
uppercase: true,
numbers: true,
special: true
}
} }
return result;
var mask = "";
if (args.lowercase) { mask += "abcdefghijklmnopqrstuvwxyz"; }
if (args.uppercase) { mask += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; }
if (args.numbers || args.hex) { mask += "0123456789"; }
if (args.hex) { mask += "ABCDEF"; }
if (args.special) { mask += "~`!@#$%^&*()_+-={}[]:\";'<>?,./|\\"; }
var source = new Uint32Array(length);
var result = new Array(length);
window.crypto.getRandomValues(source).forEach(function(value, i) {
result[i] = mask[value % mask.length];
});
return result.join("");
} }
function generateAPIKey() { function generateAPIKey() {
var apikey = randomString(16, "@#");
var apikey = randomString(16, {hex: true});
$("input[name='apiKey']").val(apikey); $("input[name='apiKey']").val(apikey);
return false; return false;
} }
function generatePassword() {
var password = "";
do {
password = randomString(10);
} while (!validatePassword(password));
return password;
}
function toggleVisiblePassword() {
var elem = this.previousElementSibling;
if (elem.type === "password") {
elem.type = "text";
} else {
elem.type = "password";
}
return false;
}
function doGeneratePassword() {
$("input", $("#formPassword"))
.val(generatePassword())
.each(function() {
this.type = "text";
});
return false;
}
function getJson(str) { function getJson(str) {
try { try {
return JSON.parse(str); return JSON.parse(str);
@ -476,11 +532,11 @@ function doReconnect(ask) {
function doUpdate() { function doUpdate() {
var form = $("#formSave");
if (validateForm(form)) {
var forms = $(".form-settings");
if (validateForm(forms)) {
// Get data // Get data
sendConfig(getData(form));
sendConfig(getData(forms));
// Empty special fields // Empty special fields
$(".pwrExpected").val(0); $(".pwrExpected").val(0);
@ -753,6 +809,7 @@ function addNetwork() {
$(this).attr("tabindex", tabindex); $(this).attr("tabindex", tabindex);
tabindex++; tabindex++;
}); });
$(".password-reveal", line).on("click", toggleVisiblePassword);
$(line).find(".button-del-network").on("click", delNetwork); $(line).find(".button-del-network").on("click", delNetwork);
$(line).find(".button-more-network").on("click", moreNetwork); $(line).find(".button-more-network").on("click", moreNetwork);
line.appendTo("#networks"); line.appendTo("#networks");
@ -1575,12 +1632,15 @@ $(function() {
createCheckboxes(); createCheckboxes();
setInterval(function() { keepTime(); }, 1000); setInterval(function() { keepTime(); }, 1000);
$(".password-reveal").on("click", toggleVisiblePassword);
$("#menuLink").on("click", toggleMenu); $("#menuLink").on("click", toggleMenu);
$(".pure-menu-link").on("click", showPanel); $(".pure-menu-link").on("click", showPanel);
$("progress").attr({ value: 0, max: 100 }); $("progress").attr({ value: 0, max: 100 });
$(".button-update").on("click", doUpdate); $(".button-update").on("click", doUpdate);
$(".button-update-password").on("click", doUpdatePassword); $(".button-update-password").on("click", doUpdatePassword);
$(".button-generate-password").on("click", doGeneratePassword);
$(".button-reboot").on("click", doReboot); $(".button-reboot").on("click", doReboot);
$(".button-reconnect").on("click", doReconnect); $(".button-reconnect").on("click", doReconnect);
$(".button-wifi-scan").on("click", doScan); $(".button-wifi-scan").on("click", doScan);


+ 79
- 40
code/html/index.html View File

@ -28,40 +28,45 @@
<div class="content"> <div class="content">
<form id="formPassword" class="pure-form" action="/" method="post">
<form id="formPassword" class="pure-form">
<div class="panel block">
<div class="panel block" id="panel-password">
<div class="header"> <div class="header">
<h1>SECURITY</h1> <h1>SECURITY</h1>
<h2>Before using this device you have to change the default password for the user 'admin'. This password will be used for the <strong>AP mode hotspot</strong>, the <strong>web interface</strong> (where you are now) and the <strong>over-the-air updates</strong>.</h2>
<h2>Before using this device you have to change the default password for the user <strong>admin</strong>. This password will be used for the <strong>AP mode hotspot</strong>, the <strong>web interface</strong> (where you are now) and the <strong>over-the-air updates</strong>.</h2>
</div> </div>
<div class="page"> <div class="page">
<fieldset> <fieldset>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Admin password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" maxlength="63" type="password" tabindex="1" autocomplete="false" />
<div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint">
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br />
It must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
<label class="pure-u-1 pure-u-lg-1-4" for="adminPass">New Password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="adminPass" maxlength="63" type="password" tabindex="1" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div> </div>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Repeat password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" type="password" tabindex="2" autocomplete="false" />
<label class="pure-u-1 pure-u-lg-1-4" for="adminPass_confirm">Repeat password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="adminPass_confirm" type="password" tabindex="2" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div> </div>
</fieldset>
<div class="pure-u-0 pure-u-lg-1-4 more"></div>
<button class="pure-button button-update-password" type="button">Update</button>
<div class="pure-g">
<div class="pure-u-1 pure-u-lg-1 hint">
Password must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
</div>
<div class="pure-g">
<button class="pure-u-11-24 pure-u-lg-1-4 pure-button button-generate-password" type="button" title="Generate password based on password policy">Generate</button>
<div class="pure-u-2-24 pure-u-lg-1-2"></div>
<button class="pure-u-11-24 pure-u-lg-1-4 pure-button button-update-password" title="Save new password">Save</button>
</div>
</fieldset>
</div> </div>
</div> </div>
</form> </form>
</div> <!-- content --> </div> <!-- content -->
@ -309,8 +314,7 @@
</div> </div>
</div> </div>
<form id="formSave" class="pure-form" action="/" method="post" enctype="multipart/form-data">
<form id="form-general" class="pure-form form-settings">
<div class="panel" id="panel-general"> <div class="panel" id="panel-general">
<div class="header"> <div class="header">
@ -382,7 +386,9 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</form>
<form id="form-relay" class="pure-form form-settings">
<div class="panel" id="panel-relay"> <div class="panel" id="panel-relay">
<div class="header"> <div class="header">
@ -414,8 +420,10 @@
</div> </div>
</div> </div>
</form>
<!-- removeIf(!light) -->
<!-- removeIf(!light) -->
<form id="form-color" class="pure-form form-settings">
<div class="panel" id="panel-color"> <div class="panel" id="panel-color">
<div class="header"> <div class="header">
@ -501,8 +509,10 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
<!-- endRemoveIf(!light) -->
</form>
<!-- endRemoveIf(!light) -->
<form id="form-admin" class="pure-form form-settings">
<div class="panel" id="panel-admin"> <div class="panel" id="panel-admin">
<div class="header"> <div class="header">
@ -523,18 +533,18 @@
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Admin password</label> <label class="pure-u-1 pure-u-lg-1-4">Admin password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" maxlength="63" type="password" action="reboot" tabindex="11" autocomplete="false" />
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" placeholder="New password" maxlength="63" type="password" action="reboot" tabindex="11" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
<div class="pure-u-1 pure-u-lg-1-4"></div>
<input name="adminPass_confirm" class="pure-u-1 pure-u-lg-3-4" placeholder="Repeat password" maxlength="63" type="password" action="reboot" tabindex="12" autocomplete="false" spellcheck="false" />
<span class="no-select password-reveal"></span>
<div class="pure-u-0 pure-u-lg-1-4"></div> <div class="pure-u-0 pure-u-lg-1-4"></div>
<div class="pure-u-1 pure-u-lg-3-4 hint"> <div class="pure-u-1 pure-u-lg-3-4 hint">
The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br /> The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode or to flash a new firmware over-the-air (OTA).<br />
It must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div> It must be <strong>8..63 characters</strong> (numbers and letters and any of these special characters: _,.;:~!?@#$%^&amp;*&lt;&gt;\|(){}[]) and have at least <strong>one lowercase</strong> and <strong>one uppercase</strong> or <strong>one number</strong>.</div>
</div> </div>
<div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Repeat password</label>
<input name="adminPass" class="pure-u-1 pure-u-lg-3-4" maxlength="63" type="password" action="reboot" tabindex="12" autocomplete="false" />
</div>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">HTTP port</label> <label class="pure-u-1 pure-u-lg-1-4">HTTP port</label>
<input name="webPort" class="pure-u-1 pure-u-lg-1-4" type="text" action="reboot" tabindex="13" /> <input name="webPort" class="pure-u-1 pure-u-lg-1-4" type="text" action="reboot" tabindex="13" />
@ -615,7 +625,9 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</form>
<form id="form-wifi" class="pure-form form-settings">
<div class="panel" id="panel-wifi"> <div class="panel" id="panel-wifi">
<div class="header"> <div class="header">
@ -657,7 +669,9 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</form>
<form id="form-schedule" class="pure-form form-settings">
<div class="panel" id="panel-schedule"> <div class="panel" id="panel-schedule">
<div class="header"> <div class="header">
@ -681,8 +695,10 @@
</div> </div>
</div> </div>
</form>
<!-- removeIf(!rfm69) -->
<!-- removeIf(!rfm69) -->
<form id="form-mapping" class="pure-form form-settings">
<div class="panel" id="panel-mapping"> <div class="panel" id="panel-mapping">
<div class="header"> <div class="header">
@ -711,10 +727,12 @@
</fieldset> </fieldset>
</div> </div>
</div> </div>
</form>
<form id="form-messages" class="pure-form">
<div class="panel" id="panel-messages"> <div class="panel" id="panel-messages">
<div class="header"> <div class="header">
<h1>MESSAGES</h1> <h1>MESSAGES</h1>
<h2> <h2>
@ -753,8 +771,10 @@
</div> </div>
</div> </div>
<!-- endRemoveIf(!rfm69) -->
</form>
<!-- endRemoveIf(!rfm69) -->
<form id="form-mqtt" class="pure-form form-settings">
<div class="panel" id="panel-mqtt"> <div class="panel" id="panel-mqtt">
<div class="header"> <div class="header">
@ -783,12 +803,13 @@
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">MQTT User</label> <label class="pure-u-1 pure-u-lg-1-4">MQTT User</label>
<input class="pure-u-1 pure-u-lg-1-4" name="mqttUser" type="text" tabindex="23" placeholder="Leave blank if no user" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-1-4" name="mqttUser" type="text" tabindex="23" placeholder="Leave blank if no user" autocomplete="off" />
</div> </div>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">MQTT Password</label> <label class="pure-u-1 pure-u-lg-1-4">MQTT Password</label>
<input class="pure-u-1 pure-u-lg-1-4" name="mqttPassword" type="password" tabindex="24" placeholder="Leave blank if no pass" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-1-4" name="mqttPassword" type="password" tabindex="24" placeholder="Leave blank if no pass" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div> </div>
<div class="pure-g"> <div class="pure-g">
@ -873,7 +894,9 @@
</div> </div>
</div> </div>
</form>
<form id="form-ntp" class="pure-form form-settings">
<div class="panel" id="panel-ntp"> <div class="panel" id="panel-ntp">
<div class="header"> <div class="header">
@ -917,7 +940,9 @@
</div> </div>
</div> </div>
</form>
<form id="form-domoticz" class="pure-form form-settings">
<div class="panel" id="panel-domoticz"> <div class="panel" id="panel-domoticz">
<div class="header"> <div class="header">
@ -964,7 +989,9 @@
</div> </div>
</div> </div>
</form>
<form id="form-ha" class="pure-form form-settings">
<div class="panel" id="panel-ha"> <div class="panel" id="panel-ha">
<div class="header"> <div class="header">
@ -1018,7 +1045,9 @@
</div> </div>
</div> </div>
</form>
<form id="form-thingspeak" class="pure-form form-settings">
<div class="panel" id="panel-thingspeak"> <div class="panel" id="panel-thingspeak">
<div class="header"> <div class="header">
@ -1060,7 +1089,9 @@
</div> </div>
</div> </div>
</form>
<form id="form-idb" class="pure-form form-settings">
<div class="panel" id="panel-idb"> <div class="panel" id="panel-idb">
<div class="header"> <div class="header">
@ -1096,19 +1127,22 @@
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Username</label> <label class="pure-u-1 pure-u-lg-1-4">Username</label>
<input class="pure-u-1 pure-u-lg-3-4" name="idbUsername" type="text" tabindex="44" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4" name="idbUsername" type="text" tabindex="44" autocomplete="off" />
</div> </div>
<div class="pure-g"> <div class="pure-g">
<label class="pure-u-1 pure-u-lg-1-4">Password</label> <label class="pure-u-1 pure-u-lg-1-4">Password</label>
<input class="pure-u-1 pure-u-lg-3-4" name="idbPassword" type="password" tabindex="45" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4" name="idbPassword" type="password" tabindex="45" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal"></span>
</div> </div>
</fieldset> </fieldset>
</div> </div>
</div> </div>
</form>
<form id="form-dbg" class="pure-form">
<div class="panel" id="panel-dbg"> <div class="panel" id="panel-dbg">
<div class="header"> <div class="header">
@ -1140,8 +1174,10 @@
</div> </div>
</div> </div>
</form>
<!-- removeIf(!sensor) -->
<!-- removeIf(!sensor) -->
<form id="form-sns" class="pure-form form-settings">
<div class="panel" id="panel-sns"> <div class="panel" id="panel-sns">
<div class="header"> <div class="header">
@ -1297,9 +1333,11 @@
</div> </div>
</div> </div>
<!-- endRemoveIf(!sensor) -->
</form>
<!-- endRemoveIf(!sensor) -->
<!-- removeIf(!rfbridge) -->
<!-- removeIf(!rfbridge) -->
<form id="form-rfb" class="pure-form form-settings">
<div class="panel" id="panel-rfb"> <div class="panel" id="panel-rfb">
<div class="header"> <div class="header">
@ -1319,10 +1357,10 @@
<div id="rfbNodes"></div> <div id="rfbNodes"></div>
</fieldset> </fieldset>
</div> </div>
</div>
<!-- endRemoveIf(!rfbridge) -->
</div>
</form> </form>
<!-- endRemoveIf(!rfbridge) -->
</div> <!-- content --> </div> <!-- content -->
@ -1363,7 +1401,8 @@
<div class="pure-u-1-6 pure-u-lg-1-12"><button type="button" class="pure-button button-more-network pure-u-1">...</button></div> <div class="pure-u-1-6 pure-u-lg-1-12"><button type="button" class="pure-button button-more-network pure-u-1">...</button></div>
<label class="pure-u-1 pure-u-lg-1-4 more">Password</label> <label class="pure-u-1 pure-u-lg-1-4 more">Password</label>
<input class="pure-u-1 pure-u-lg-3-4 more" name="pass" type="password" action="reconnect" value="" tabindex="0" autocomplete="false" />
<input class="pure-u-1 pure-u-lg-3-4 more" name="pass" type="password" action="reconnect" value="" tabindex="0" autocomplete="new-password" spellcheck="false" />
<span class="no-select password-reveal more"></span>
<label class="pure-u-1 pure-u-lg-1-4 more">Static IP</label> <label class="pure-u-1 pure-u-lg-1-4 more">Static IP</label>
<input class="pure-u-1 pure-u-lg-3-4 more" name="ip" type="text" action="reconnect" value="" maxlength="15" tabindex="0" autocomplete="false" /> <input class="pure-u-1 pure-u-lg-3-4 more" name="ip" type="text" action="reconnect" value="" maxlength="15" tabindex="0" autocomplete="false" />
@ -1586,7 +1625,7 @@
<!-- removeIf(!rfm69) --> <!-- removeIf(!rfm69) -->
<div id="nodeTemplate" class="template"> <div id="nodeTemplate" class="template">
<div class="pure-g"> <div class="pure-g">
<div class="pure-u-md-1-6 pure-u-1-2"><input name="node" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Node ID"></div>
<div class="pure-u-md-1-6 pure-u-1-2"><input name="node" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Node ID" autocomplete="false"></div>
<div class="pure-u-md-1-6 pure-u-1-2"><input name="key" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Key"></div> <div class="pure-u-md-1-6 pure-u-1-2"><input name="key" type="text" class="pure-u-11-12" value="" size="8" tabindex="0" placeholder="Key"></div>
<div class="pure-u-md-1-2 pure-u-3-4"><input name="topic" type="text" class="pure-md-11-12 pure-u-23-24" value="" size="8" tabindex="0" placeholder="MQTT Topic"></div> <div class="pure-u-md-1-2 pure-u-3-4"><input name="topic" type="text" class="pure-md-11-12 pure-u-23-24" value="" size="8" tabindex="0" placeholder="MQTT Topic"></div>
<div class="pure-u-md-1-6 pure-u-1-4"><button type="button" class="pure-button button-del-mapping pure-u-5-6 pure-u-md-5-6">Del</button></div> <div class="pure-u-md-1-6 pure-u-1-4"><button type="button" class="pure-button button-del-mapping pure-u-5-6 pure-u-md-5-6">Del</button></div>


+ 18
- 4
code/ota.py View File

@ -15,6 +15,7 @@ import socket
import subprocess import subprocess
import sys import sys
import time import time
import os
from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf
@ -232,13 +233,24 @@ def boardname(board):
def store(device, env): def store(device, env):
source = ".pioenvs/%s/firmware.elf" % env source = ".pioenvs/%s/firmware.elf" % env
destination = ".pioenvs/elfs/%s.elf" % boardname(device).lower() destination = ".pioenvs/elfs/%s.elf" % boardname(device).lower()
dst_dir = os.path.dirname(destination)
if not os.path.exists(dst_dir):
os.mkdir(dst_dir)
shutil.move(source, destination) shutil.move(source, destination)
def run(device, env): def run(device, env):
print("Building and flashing image over-the-air...") print("Building and flashing image over-the-air...")
command = "ESPURNA_IP=\"%s\" ESPURNA_BOARD=\"%s\" ESPURNA_AUTH=\"%s\" ESPURNA_FLAGS=\"%s\" platformio run --silent --environment %s -t upload"
command = command % (device['ip'], device['board'], device['auth'], device['flags'], env)
subprocess.check_call(command, shell=True)
environ = os.environ.copy()
environ["ESPURNA_IP"] = device["ip"]
environ["ESPURNA_BOARD"] = device["board"]
environ["ESPURNA_AUTH"] = device["auth"]
environ["ESPURNA_FLAGS"] = device["flags"]
command = ("platformio", "run", "--silent", "--environment", env, "-t", "upload")
subprocess.check_call(command, env=environ)
store(device, env) store(device, env)
# ------------------------------------------------------------------------------- # -------------------------------------------------------------------------------
@ -308,6 +320,8 @@ if __name__ == '__main__':
if len(queue) == 0: if len(queue) == 0:
sys.exit(0) sys.exit(0)
queue = sorted(queue, key=lambda device: device.get('board', ''))
# Flash eash board # Flash eash board
for board in queue: for board in queue:
@ -315,7 +329,7 @@ if __name__ == '__main__':
if args.core > 0: if args.core > 0:
board['flags'] = "-DESPURNA_CORE " + board['flags'] board['flags'] = "-DESPURNA_CORE " + board['flags']
env = "esp8266-%sm-ota" % board['size']
env = "esp8266-%dm-ota" % board['size']
# Summary # Summary
print() print()


+ 29
- 2
code/platformio.ini View File

@ -18,6 +18,7 @@ platform_150 = espressif8266@1.5.0
platform_160 = espressif8266@1.6.0 platform_160 = espressif8266@1.6.0
platform_173 = espressif8266@1.7.3 platform_173 = espressif8266@1.7.3
platform_180 = espressif8266@1.8.0 platform_180 = espressif8266@1.8.0
platform_latest = ${common.platform_180}
platform = ${common.platform_150} platform = ${common.platform_150}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
@ -193,7 +194,7 @@ monitor_speed = ${common.monitor_speed}
extra_scripts = ${common.extra_scripts} extra_scripts = ${common.extra_scripts}
[env:travis02] [env:travis02]
platform = ${common.platform_173}
platform = ${common.platform_latest}
framework = ${common.framework} framework = ${common.framework}
board = ${common.board_4m} board = ${common.board_4m}
board_build.flash_mode = ${common.flash_mode} board_build.flash_mode = ${common.flash_mode}
@ -204,7 +205,7 @@ monitor_speed = ${common.monitor_speed}
extra_scripts = ${common.extra_scripts} extra_scripts = ${common.extra_scripts}
[env:travis03] [env:travis03]
platform = ${common.platform_173}
platform = ${common.platform_latest}
framework = ${common.framework} framework = ${common.framework}
board = ${common.board_4m} board = ${common.board_4m}
board_build.flash_mode = ${common.flash_mode} board_build.flash_mode = ${common.flash_mode}
@ -2202,6 +2203,32 @@ upload_port = ${common.upload_port}
upload_flags = ${common.upload_flags} upload_flags = ${common.upload_flags}
extra_scripts = ${common.extra_scripts} extra_scripts = ${common.extra_scripts}
[env:iwoole-led-table-lamp]
platform = ${common.platform}
framework = ${common.framework}
board = ${common.board_1m}
board_build.flash_mode = ${common.flash_mode}
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
build_flags = ${common.build_flags_1m0m} -DIWOOLE_LED_TABLE_LAMP
monitor_speed = ${common.monitor_speed}
extra_scripts = ${common.extra_scripts}
[env:iwoole-led-table-lamp-ota]
platform = ${common.platform}
framework = ${common.framework}
board = ${common.board_1m}
board_build.flash_mode = ${common.flash_mode}
lib_deps = ${common.lib_deps}
lib_ignore = ${common.lib_ignore}
build_flags = ${common.build_flags_1m0m} -DIWOOLE_LED_TABLE_LAMP
upload_speed = ${common.upload_speed}
upload_port = ${common.upload_port}
upload_flags = ${common.upload_flags}
monitor_speed = ${common.monitor_speed}
extra_scripts = ${common.extra_scripts}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# GENERIC OTA ENVIRONMENTS # GENERIC OTA ENVIRONMENTS
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------


BIN
images/devices/aithinker-ai-light.jpg View File

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

BIN
images/devices/arilux-al-lc01.jpg View File

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

BIN
images/devices/arilux-al-lc06.jpg View File

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

BIN
images/devices/arilux-e27.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 26 KiB Width: 400  |  Height: 400  |  Size: 25 KiB

BIN
images/devices/authometion-lyt8266.jpg View File

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

BIN
images/devices/electrodragon-wifi-iot.jpg View File

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

BIN
images/devices/exs-wifi-relay-v31.jpg View File

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

BIN
images/devices/geiger_espurna_configuration.png View File

Before After
Width: 2076  |  Height: 1564  |  Size: 247 KiB Width: 2076  |  Height: 1564  |  Size: 123 KiB

BIN
images/devices/geiger_espurna_status.png View File

Before After
Width: 1800  |  Height: 1496  |  Size: 325 KiB Width: 1800  |  Height: 1496  |  Size: 166 KiB

BIN
images/devices/geiger_grafana_dashboard.png View File

Before After
Width: 2414  |  Height: 658  |  Size: 457 KiB Width: 2414  |  Height: 658  |  Size: 263 KiB

BIN
images/devices/geiger_scope_following_pulses.png View File

Before After
Width: 800  |  Height: 480  |  Size: 39 KiB Width: 800  |  Height: 480  |  Size: 14 KiB

BIN
images/devices/geiger_scope_single_pulse.png View File

Before After
Width: 800  |  Height: 480  |  Size: 38 KiB Width: 800  |  Height: 480  |  Size: 13 KiB

BIN
images/devices/geiger_wiring_diagram.png View File

Before After
Width: 2152  |  Height: 864  |  Size: 1.9 MiB Width: 2152  |  Height: 864  |  Size: 1.4 MiB

BIN
images/devices/generic-ag-l4-1.jpg View File

Before After
Width: 960  |  Height: 1280  |  Size: 200 KiB Width: 960  |  Height: 1280  |  Size: 190 KiB

BIN
images/devices/generic-ag-l4-2.jpg View File

Before After
Width: 960  |  Height: 1280  |  Size: 234 KiB Width: 960  |  Height: 1280  |  Size: 221 KiB

BIN
images/devices/generic-ag-l4-3.jpg View File

Before After
Width: 960  |  Height: 1280  |  Size: 232 KiB Width: 960  |  Height: 1280  |  Size: 217 KiB

BIN
images/devices/generic-ag-l4-4.jpg View File

Before After
Width: 960  |  Height: 1280  |  Size: 166 KiB Width: 960  |  Height: 1280  |  Size: 158 KiB

BIN
images/devices/generic-ag-l4-5.jpg View File

Before After
Width: 1280  |  Height: 960  |  Size: 141 KiB Width: 1280  |  Height: 960  |  Size: 134 KiB

BIN
images/devices/generic-geiger-diy.png View File

Before After
Width: 400  |  Height: 400  |  Size: 148 KiB Width: 400  |  Height: 400  |  Size: 129 KiB

BIN
images/devices/generic-relay-40.jpg View File

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

BIN
images/devices/generic-rgbled-10.jpg View File

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

BIN
images/devices/generic-v9261f.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 12 KiB Width: 400  |  Height: 400  |  Size: 11 KiB

BIN
images/devices/heygo-hy02.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 8.4 KiB Width: 400  |  Height: 400  |  Size: 8.2 KiB

BIN
images/devices/huacanxing-h801.jpg View File

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

BIN
images/devices/intermittech-quinled-2.6.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 29 KiB Width: 400  |  Height: 400  |  Size: 28 KiB

BIN
images/devices/itead-1ch-inching.jpg View File

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

BIN
images/devices/itead-bn-sz01.jpg View File

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

BIN
images/devices/itead-motor.jpg View File

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

BIN
images/devices/itead-s20.jpg View File

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

BIN
images/devices/itead-s26.jpg View File

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

BIN
images/devices/itead-slampher.jpg View File

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

BIN
images/devices/itead-sonoff-4ch-pro.jpg View File

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

BIN
images/devices/itead-sonoff-4ch.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 9.5 KiB Width: 400  |  Height: 400  |  Size: 9.4 KiB

BIN
images/devices/itead-sonoff-b1.jpg View File

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

BIN
images/devices/itead-sonoff-basic.jpg View File

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

BIN
images/devices/itead-sonoff-dual.jpg View File

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

BIN
images/devices/itead-sonoff-ifan02.jpg View File

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

BIN
images/devices/itead-sonoff-led.jpg View File

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

BIN
images/devices/itead-sonoff-pow.jpg View File

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

BIN
images/devices/itead-sonoff-rf.jpg View File

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

BIN
images/devices/itead-sonoff-sv.jpg View File

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

BIN
images/devices/itead-sonoff-t1.jpg View File

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

BIN
images/devices/itead-sonoff-th.jpg View File

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

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

Before After
Width: 400  |  Height: 400  |  Size: 38 KiB Width: 400  |  Height: 400  |  Size: 37 KiB

BIN
images/devices/jorgegarcia-wifi-relays.jpg View File

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

BIN
images/devices/kmc-70011.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 9.2 KiB Width: 400  |  Height: 400  |  Size: 8.8 KiB

BIN
images/devices/lingan-swa1.jpg View File

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

BIN
images/devices/lohas-9w.jpg View File

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

BIN
images/devices/magichome-led-controller.jpg View File

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

BIN
images/devices/mancavemade-esp-live.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 9.4 KiB Width: 400  |  Height: 400  |  Size: 8.8 KiB

BIN
images/devices/neo-coolcam-wifi.jpg View File

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

BIN
images/devices/nodemcu-lolin-v3.jpg View File

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

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

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

BIN
images/devices/schuko-wifi-plug.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 8.5 KiB Width: 400  |  Height: 400  |  Size: 8.2 KiB

BIN
images/devices/tinkerman-espurna-h.jpg View File

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

BIN
images/devices/tonbux-powerstrip02.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 6.2 KiB Width: 400  |  Height: 400  |  Size: 6.1 KiB

BIN
images/devices/wemos-d1-mini-relayshield.jpg View File

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

BIN
images/devices/wion-50055.jpg View File

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

BIN
images/devices/witty-cloud.jpg View File

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

BIN
images/devices/workchoice-ecoplug.jpg View File

Before After
Width: 400  |  Height: 400  |  Size: 8.2 KiB Width: 400  |  Height: 400  |  Size: 7.8 KiB

BIN
images/devices/xenon-sm-pw702u.jpg View File

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

Loading…
Cancel
Save