diff --git a/CHANGELOG_LATEST.md b/CHANGELOG_LATEST.md
index bed071666..926f611c0 100644
--- a/CHANGELOG_LATEST.md
+++ b/CHANGELOG_LATEST.md
@@ -46,6 +46,6 @@
- new ESP32 partition side to allow for smoother OTA and fallback
- network Gateway IP is optional (#682)emsesp/EMS-ESP
- moved to a new GitHub repo https://github.com/emsesp/EMS-ESP32
-
+- Invert LED changed to Hide LED
### Removed
- Shower Alert (disabled for now)
diff --git a/interface/src/project/EMSESPBoardProfiles.tsx b/interface/src/project/EMSESPBoardProfiles.tsx
new file mode 100644
index 000000000..ce1cc6bb8
--- /dev/null
+++ b/interface/src/project/EMSESPBoardProfiles.tsx
@@ -0,0 +1,23 @@
+import React from 'react';
+import MenuItem from '@material-ui/core/MenuItem';
+
+type BoardProfiles = {
+ [name: string]: string
+};
+
+export const BOARD_PROFILES: BoardProfiles = {
+ "S32": "Gateway S32",
+ "NODEMCU": "NodeMCU 32S",
+ "MT-ET": "MT-ET Live D1 Mini",
+ "LOLIN": "Lolin D32",
+ "WEMOS": "Wemos Mini D1-32",
+ "E32": "Gateway E32",
+ "OLIMEX": "Olimex ESP32-EVB-EA",
+ "TLK110": "Ethernet (TLK110)"
+}
+
+export function boardProfileSelectItems() {
+ return Object.keys(BOARD_PROFILES).map(code => (
+
+ ));
+}
diff --git a/interface/src/project/EMSESPSettingsForm.tsx b/interface/src/project/EMSESPSettingsForm.tsx
index 5bce4f5ae..7add38aa5 100644
--- a/interface/src/project/EMSESPSettingsForm.tsx
+++ b/interface/src/project/EMSESPSettingsForm.tsx
@@ -7,27 +7,88 @@ import MenuItem from '@material-ui/core/MenuItem';
import Grid from '@material-ui/core/Grid';
+import { redirectingAuthorizedFetch, withAuthenticatedContext, AuthenticatedContextProps } from "../authentication";
+
import { RestFormProps, FormActions, FormButton, BlockFormControlLabel } from '../components';
import { isIP, optional } from '../validators';
import { EMSESPSettings } from './EMSESPtypes';
-type EMSESPSettingsFormProps = RestFormProps & WithWidthProps;
+import { boardProfileSelectItems } from './EMSESPBoardProfiles';
+
+import { ENDPOINT_ROOT } from "../api";
+export const BOARD_PROFILE_ENDPOINT = ENDPOINT_ROOT + "boardProfile";
+
+type EMSESPSettingsFormProps = RestFormProps & AuthenticatedContextProps & WithWidthProps;
+
+interface EMSESPSettingsFormState {
+ processing: boolean;
+}
class EMSESPSettingsForm extends React.Component {
+ state: EMSESPSettingsFormState = {
+ processing: false
+ }
+
componentDidMount() {
ValidatorForm.addValidationRule('isOptionalIP', optional(isIP));
}
+ changeBoardProfile = (event: React.ChangeEvent) => {
+ const { data, setData } = this.props;
+ setData({
+ ...data,
+ board_profile: event.target.value
+ });
+
+ if (event.target.value === "CUSTOM")
+ return;
+
+ this.setState({ processing: true });
+ redirectingAuthorizedFetch(BOARD_PROFILE_ENDPOINT, {
+ method: "POST",
+ body: JSON.stringify({ code: event.target.value }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ if (response.status === 200) {
+ return response.json();
+ }
+ throw Error("Unexpected response code: " + response.status);
+ })
+ .then((json) => {
+ this.props.enqueueSnackbar("Profile loaded", { variant: 'success' });
+ setData({
+ ...data,
+ led_gpio: json.led_gpio,
+ dallas_gpio: json.dallas_gpio,
+ rx_gpio: json.rx_gpio,
+ tx_gpio: json.tx_gpio,
+ pbutton_gpio: json.pbutton_gpio,
+ board_profile: event.target.value
+ });
+ this.setState({ processing: false });
+ })
+ .catch((error) => {
+ this.props.enqueueSnackbar(
+ error.message || "Problem fetching board profile",
+ { variant: "warning" }
+ );
+ this.setState({ processing: false });
+ });
+ };
+
render() {
const { data, saveData, handleValueChange } = this.props;
return (
- Modify any of the EMS-ESP settings here. For help visit the {'wiki'}.
+ Modify any of the EMS-ESP settings here. For help refer to the {'online documentation'}.
@@ -37,7 +98,6 @@ class EMSESPSettingsForm extends React.Component {
-
{
- Choose from a pre-configured board layout to automatically set the GPIO pins
+ Select a pre-configured board layout to automatically set the GPIO pins, or set your own custom configuration
@@ -100,25 +160,18 @@ class EMSESPSettingsForm extends React.Component {
value={data.board_profile}
fullWidth
variant="outlined"
- onChange={handleValueChange('board_profile')}
+ onChange={this.changeBoardProfile}
margin="normal">
-
-
-
-
-
-
-
-
+ {boardProfileSelectItems()}
+
- { (data.board_profile === 0) &&
-
+ { (data.board_profile === "CUSTOM") &&
{
{
{
{
{
value="hide_led"
/>
}
- label="Invert LED"
+ label = "Hide LED"
/>
}
@@ -361,4 +414,4 @@ class EMSESPSettingsForm extends React.Component {
}
-export default withWidth()(EMSESPSettingsForm);
+export default withAuthenticatedContext(withWidth()(EMSESPSettingsForm));
diff --git a/interface/src/project/EMSESPtypes.ts b/interface/src/project/EMSESPtypes.ts
index 1d8d69785..a2a8f0ce7 100644
--- a/interface/src/project/EMSESPtypes.ts
+++ b/interface/src/project/EMSESPtypes.ts
@@ -20,7 +20,7 @@ export interface EMSESPSettings {
analog_enabled: boolean;
pbutton_gpio: number;
trace_raw: boolean;
- board_profile: number;
+ board_profile: string;
}
export enum busConnectionStatus {
diff --git a/lib_standalone/ESP8266React.h b/lib_standalone/ESP8266React.h
index 62aaddb82..e598051f3 100644
--- a/lib_standalone/ESP8266React.h
+++ b/lib_standalone/ESP8266React.h
@@ -49,7 +49,7 @@ class DummySettings {
String staticIPConfig = "";
String dnsIP1 = "";
String dnsIP2 = "";
- uint8_t board_profile = 0;
+ String board_profile = "CUSTOM";
uint16_t publish_time_boiler = 10;
uint16_t publish_time_thermostat = 10;
uint16_t publish_time_solar = 10;
diff --git a/src/WebSettingsService.cpp b/src/WebSettingsService.cpp
index b299546b5..b5bbf1db6 100644
--- a/src/WebSettingsService.cpp
+++ b/src/WebSettingsService.cpp
@@ -22,9 +22,16 @@ namespace emsesp {
uint8_t WebSettings::flags_;
+using namespace std::placeholders; // for `_1` etc
+
WebSettingsService::WebSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(WebSettings::read, WebSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager)
- , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE) {
+ , _fsPersistence(WebSettings::read, WebSettings::update, this, fs, EMSESP_SETTINGS_FILE)
+ , _boardProfileHandler(EMSESP_BOARD_PROFILE_SERVICE_PATH, securityManager->wrapCallback(std::bind(&WebSettingsService::board_profile, this, _1, _2), AuthenticationPredicates::IS_ADMIN)) {
+ _boardProfileHandler.setMethod(HTTP_POST);
+ _boardProfileHandler.setMaxContentLength(256);
+ server->addHandler(&_boardProfileHandler);
+
addUpdateHandler([&](const String & originId) { onUpdate(); }, false);
}
@@ -189,4 +196,35 @@ void WebSettingsService::save() {
_fsPersistence.writeToFS();
}
+// build the json profile to send back
+void WebSettingsService::board_profile(AsyncWebServerRequest * request, JsonVariant & json) {
+ if (json.is()) {
+ AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_MEDIUM);
+ JsonObject root = response->getRoot();
+ if (json.containsKey("code")) {
+ String board_profile = json["code"];
+ std::vector data; // led, dallas, rx, tx, button
+ // check for valid board
+ if (System::load_board_profile(data, board_profile.c_str())) {
+ root["led_gpio"] = data[0];
+ root["dallas_gpio"] = data[1];
+ root["rx_gpio"] = data[2];
+ root["tx_gpio"] = data[3];
+ root["pbutton_gpio"] = data[4];
+ } else {
+ AsyncWebServerResponse * response = request->beginResponse(200);
+ request->send(response);
+ return;
+ }
+
+ response->setLength();
+ request->send(response);
+ return;
+ }
+ }
+
+ AsyncWebServerResponse * response = request->beginResponse(200);
+ request->send(response);
+}
+
} // namespace emsesp
\ No newline at end of file
diff --git a/src/WebSettingsService.h b/src/WebSettingsService.h
index c03f488bc..fc812cfc3 100644
--- a/src/WebSettingsService.h
+++ b/src/WebSettingsService.h
@@ -24,6 +24,7 @@
#define EMSESP_SETTINGS_FILE "/config/emsespSettings.json"
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings"
+#define EMSESP_BOARD_PROFILE_SERVICE_PATH "/rest/boardProfile"
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#define EMSESP_DEFAULT_TX_DELAY 0 // no delay
@@ -42,7 +43,7 @@
#define EMSESP_DEFAULT_API_ENABLED false // turn off, because its insecure
#define EMSESP_DEFAULT_BOOL_FORMAT 1 // on/off
#define EMSESP_DEFAULT_ANALOG_ENABLED false
-#define EMSESP_DEFAULT_BOARD_PROFILE 0 // default ESP32
+#define EMSESP_DEFAULT_BOARD_PROFILE "S32"
// Default GPIO PIN definitions
#if defined(ESP32)
@@ -85,7 +86,7 @@ class WebSettings {
bool api_enabled;
bool analog_enabled;
uint8_t pbutton_gpio;
- uint8_t board_profile;
+ String board_profile;
static void read(WebSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, WebSettings & settings);
@@ -137,8 +138,11 @@ class WebSettingsService : public StatefulService {
void save();
private:
- HttpEndpoint _httpEndpoint;
- FSPersistence _fsPersistence;
+ HttpEndpoint _httpEndpoint;
+ FSPersistence _fsPersistence;
+ AsyncCallbackJsonWebHandler _boardProfileHandler;
+
+ void board_profile(AsyncWebServerRequest * request, JsonVariant & json);
void onUpdate();
};
diff --git a/src/locale_EN.h b/src/locale_EN.h
index 3c192fabd..121678fd7 100644
--- a/src/locale_EN.h
+++ b/src/locale_EN.h
@@ -68,7 +68,7 @@ MAKE_PSTR_WORD(master)
MAKE_PSTR_WORD(pin)
MAKE_PSTR_WORD(publish)
MAKE_PSTR_WORD(timeout)
-MAKE_PSTR_WORD(ethernet)
+MAKE_PSTR_WORD(board_profile)
// for commands
MAKE_PSTR_WORD(call)
@@ -101,6 +101,7 @@ MAKE_PSTR(master_thermostat_fmt, "Master Thermostat Device ID = %s")
MAKE_PSTR(host_fmt, "Host = %s")
MAKE_PSTR(port_fmt, "Port = %d")
MAKE_PSTR(hostname_fmt, "Hostname = %s")
+MAKE_PSTR(board_profile_fmt, "Board Profile = %s")
MAKE_PSTR(mark_interval_fmt, "Mark interval = %lus")
MAKE_PSTR(wifi_ssid_fmt, "WiFi SSID = %s")
MAKE_PSTR(wifi_password_fmt, "WiFi Password = %S")
@@ -187,13 +188,6 @@ MAKE_PSTR_WORD(French)
MAKE_PSTR_WORD(Italian)
MAKE_PSTR_WORD(high)
MAKE_PSTR_WORD(low)
-MAKE_PSTR(internal_temperature, "internal temperature")
-MAKE_PSTR(internal_setpoint, "internal setpoint")
-MAKE_PSTR(external_temperature, "external temperature")
-MAKE_PSTR(burner_temperature, "burner temperature")
-MAKE_PSTR(WW_temperature, "WW temperature")
-MAKE_PSTR(functioning_mode, "functioning mode")
-MAKE_PSTR(smoke_temperature, "smoke temperature")
MAKE_PSTR_WORD(radiator)
MAKE_PSTR_WORD(convector)
MAKE_PSTR_WORD(floor)
@@ -212,6 +206,13 @@ MAKE_PSTR_WORD(night)
MAKE_PSTR_WORD(day)
MAKE_PSTR_WORD(holiday)
MAKE_PSTR_WORD(reduce)
+MAKE_PSTR(internal_temperature, "internal temperature")
+MAKE_PSTR(internal_setpoint, "internal setpoint")
+MAKE_PSTR(external_temperature, "external temperature")
+MAKE_PSTR(burner_temperature, "burner temperature")
+MAKE_PSTR(WW_temperature, "WW temperature")
+MAKE_PSTR(functioning_mode, "functioning mode")
+MAKE_PSTR(smoke_temperature, "smoke temperature")
// thermostat lists
MAKE_PSTR_LIST(enum_ibaMainDisplay, F_(internal_temperature), F_(internal_setpoint), F_(external_temperature), F_(burner_temperature), F_(WW_temperature), F_(functioning_mode), F_(time), F_(date), F_(smoke_temperature))
diff --git a/src/system.cpp b/src/system.cpp
index ac29ba8ee..e282cb1d2 100644
--- a/src/system.cpp
+++ b/src/system.cpp
@@ -203,12 +203,12 @@ void System::wifi_tweak() {
}
// check for valid ESP32 pins. This is very dependent on which ESP32 board is being used.
-// Typically you can't use 1, 6-11 (SPI flash), 12, 14 & 15, 20, 24 and 28-31
+// Typically you can't use 1, 6-11, 12, 14, 15, 20, 24, 28-31 and 40+
// we allow 0 as it has a special function on the NodeMCU apparently
// See https://diyprojects.io/esp32-how-to-use-gpio-digital-io-arduino-code/#.YFpVEq9KhjG
// and https://nodemcu.readthedocs.io/en/dev-esp32/modules/gpio/
bool System::is_valid_gpio(uint8_t pin) {
- if ((pin == 1) || (pin >= 6 && pin <= 12) || (pin >= 14 && pin <= 15) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31)) {
+ if ((pin == 1) || (pin >= 6 && pin <= 12) || (pin >= 14 && pin <= 15) || (pin == 20) || (pin == 24) || (pin >= 28 && pin <= 31) || (pin > 40)) {
return false; // bad pin
}
return true;
@@ -233,6 +233,8 @@ void System::start(uint32_t heap_start) {
LOG_INFO(F("System %s booted (EMS-ESP version %s)"), networkSettings.hostname.c_str(), EMSESP_APP_VERSION); // print boot message
});
+ LOG_INFO("Loaded Board profile: %s", board_profile_.c_str());
+
commands_init(); // console & api commands
led_init(false); // init LED
adc_init(false); // analog ADC
@@ -321,7 +323,7 @@ void System::led_init(bool refresh) {
if ((led_gpio_ != 0) && is_valid_gpio(led_gpio_)) {
pinMode(led_gpio_, OUTPUT); // 0 means disabled
- digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, forever
+ digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
}
}
@@ -470,6 +472,8 @@ void System::network_init(bool refresh) {
last_system_check_ = 0; // force the LED to go from fast flash to pulse
send_heartbeat();
+ /*
+
// check board profile for those which use ethernet (id > 10)
// ethernet uses lots of additional memory so we only start it when it's explicitly set in the config
if (board_profile_ < 10) {
@@ -531,6 +535,7 @@ void System::network_init(bool refresh) {
"local");
#endif
}
+ */
}
// check health of system, done every few seconds
@@ -559,7 +564,7 @@ void System::system_check() {
system_healthy_ = true;
send_heartbeat();
if (led_gpio_) {
- digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON); // LED on, for ever
+ digitalWrite(led_gpio_, hide_led_ ? !LED_ON : LED_ON);
}
}
}
@@ -568,7 +573,7 @@ void System::system_check() {
// commands - takes static function pointers
// these commands respond to the topic "system" and take a payload like {cmd:"", data:"", id:""}
-// no individual subsribe for pin command because id is needed
+// no individual subscribe for pin command because id is needed
void System::commands_init() {
Command::add(EMSdevice::DeviceType::SYSTEM, F_(pin), System::command_pin, MqttSubFlag::FLAG_NOSUB);
Command::add(EMSdevice::DeviceType::SYSTEM, F_(send), System::command_send);
@@ -779,7 +784,7 @@ void System::console_commands(Shell & shell, unsigned int context) {
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(hostname)},
flash_string_vector{F_(name_mandatory)},
- [](Shell & shell __attribute__((unused)), const std::vector & arguments) {
+ [](Shell & shell, const std::vector & arguments) {
shell.println("The network connection will be reset...");
Shell::loop_all();
delay(1000); // wait a second
@@ -823,39 +828,41 @@ void System::console_commands(Shell & shell, unsigned int context) {
});
});
- EMSESPShell::commands->add_command(
- ShellContext::SYSTEM,
- CommandFlags::ADMIN,
- flash_string_vector{F_(set), F_(ethernet)},
- flash_string_vector{F_(n_mandatory)},
- [](Shell & shell, const std::vector & arguments) {
- uint8_t n = Helpers::hextoint(arguments.front().c_str());
- if (n <= 2) {
- EMSESP::webSettingsService.update(
- [&](WebSettings & settings) {
- settings.board_profile = n;
- shell.printfln(F_(ethernet_option_fmt), n);
- return StateUpdateResult::CHANGED;
- },
- "local");
- EMSESP::system_.network_init(true);
- } else {
- shell.println(F("Must be 0, 1 or 2"));
- }
- },
- [](Shell & shell __attribute__((unused)), const std::vector & arguments __attribute__((unused))) -> const std::vector {
- return std::vector{read_flash_string(F("0")), read_flash_string(F("1")), read_flash_string(F("2"))};
- });
+ EMSESPShell::commands->add_command(ShellContext::SYSTEM,
+ CommandFlags::ADMIN,
+ flash_string_vector{F_(set), F_(board_profile)},
+ flash_string_vector{F_(name_mandatory)},
+ [](Shell & shell, const std::vector & arguments) {
+ // check for valid profile
+ std::vector data; // led, dallas, rx, tx, button
+ std::string board_profile = Helpers::toUpper(arguments.front());
+ if (!load_board_profile(data, board_profile)) {
+ shell.println(F("Invalid board profile"));
+ return;
+ }
+ shell.printfln("Loaded board profile %s with %d,%d,%d,%d,%d", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4]);
+ EMSESP::webSettingsService.update(
+ [&](WebSettings & settings) {
+ settings.board_profile = board_profile.c_str();
+ settings.led_gpio = data[0];
+ settings.dallas_gpio = data[1];
+ settings.rx_gpio = data[2];
+ settings.tx_gpio = data[3];
+ settings.pbutton_gpio = data[4];
+ return StateUpdateResult::CHANGED;
+ },
+ "local");
+ shell.printfln("Loaded board profile %s with %d,%d,%d,%d,%d", board_profile.c_str(), data[0], data[1], data[2], data[3], data[4]);
+ EMSESP::system_.network_init(true);
+ });
EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::USER, flash_string_vector{F_(set)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) {
EMSESP::esp8266React.getNetworkSettingsService()->read([&](NetworkSettings & networkSettings) {
- shell.print(F(" "));
shell.printfln(F_(hostname_fmt), networkSettings.hostname.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : networkSettings.hostname.c_str());
- shell.print(F(" "));
shell.printfln(F_(wifi_ssid_fmt), networkSettings.ssid.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : networkSettings.ssid.c_str());
- shell.print(F(" "));
shell.printfln(F_(wifi_password_fmt), networkSettings.ssid.isEmpty() ? F_(unset) : F_(asterisks));
});
+ EMSESP::webSettingsService.read([&](WebSettings & settings) { shell.printfln(F_(board_profile_fmt), settings.board_profile.c_str()); });
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM, CommandFlags::ADMIN, flash_string_vector{F_(show), F_(users)}, [](Shell & shell, const std::vector & arguments __attribute__((unused))) {
@@ -961,6 +968,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
node["api_enabled"] = settings.api_enabled;
node["analog_enabled"] = settings.analog_enabled;
node["pbutton_gpio"] = settings.pbutton_gpio;
+ node["board_profile"] = settings.board_profile;
});
return true;
@@ -1030,5 +1038,29 @@ bool System::command_test(const char * value, const int8_t id) {
}
#endif
+// takes a board profile and populates a data array with GPIO configurations
+bool System::load_board_profile(std::vector & data, const std::string & board_profile) {
+ if (board_profile == "S32") {
+ data = {2, 3, 23, 5, 0}; // Gateway S32
+ } else if (board_profile == "E32") {
+ data = {2, 4, 5, 17, 33}; // Gateway E32
+ } else if (board_profile == "MT-ET") {
+ data = {2, 4, 23, 5, 0}; // MT-ET Live D1 Mini
+ } else if (board_profile == "NODEMCU") {
+ data = {2, 4, 23, 5, 0}; // NodeMCU 32S
+ } else if (board_profile == "LOLIN") {
+ data = {2, 14, 17, 16, 0}; // Lolin D32
+ } else if (board_profile == "WEMOS") {
+ data = {2, 14, 17, 16, 0}; // Wemos Mini D1-32
+ } else if (board_profile == "OLIMEX") {
+ data = {32, 4, 5, 17, 34}; // Olimex ESP32-EVB-EA
+ } else if (board_profile == "TLK110") {
+ data = {2, 4, 5, 17, 33}; // Ethernet (TLK110)
+ } else {
+ return false;
+ }
+
+ return true;
+}
} // namespace emsesp
diff --git a/src/system.h b/src/system.h
index 135a47860..6228358a9 100644
--- a/src/system.h
+++ b/src/system.h
@@ -76,6 +76,8 @@ class System {
static bool is_valid_gpio(uint8_t pin);
+ static bool load_board_profile(std::vector & data, const std::string & board_profile);
+
bool check_upgrade();
void send_heartbeat();
@@ -115,7 +117,7 @@ class System {
static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence
static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min)
static constexpr uint32_t SYSTEM_MEASURE_ANALOG_INTERVAL = 500;
- static constexpr uint8_t LED_ON = LOW; // internal LED
+ static constexpr uint8_t LED_ON = HIGH; // LED
#ifndef EMSESP_STANDALONE
static uuid::syslog::SyslogService syslog_;
@@ -145,7 +147,7 @@ class System {
uint8_t led_gpio_;
bool syslog_enabled_;
bool analog_enabled_;
- uint8_t board_profile_;
+ String board_profile_;
uint8_t pbutton_gpio_;
int8_t syslog_level_;
uint32_t syslog_mark_interval_;
diff --git a/src/version.h b/src/version.h
index 963196b11..3962aa887 100644
--- a/src/version.h
+++ b/src/version.h
@@ -1,2 +1,2 @@
-#define EMSESP_APP_VERSION "3.0.1b3"
+#define EMSESP_APP_VERSION "3.0.1b4"
#define EMSESP_PLATFORM "ESP32"