This commit is contained in:
Johannes Wagner
2020-08-04 17:44:05 +02:00
81 changed files with 2205 additions and 2930 deletions

View File

@@ -1,35 +1,93 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "EMSESPDevicesService.h"
#include "emsesp.h"
#include "mqtt.h"
namespace emsesp {
EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager) {
EMSESPDevicesService::EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager)
: _device_dataHandler(DEVICE_DATA_SERVICE_PATH,
securityManager->wrapCallback(std::bind(&EMSESPDevicesService::device_data, this, _1, _2), AuthenticationPredicates::IS_AUTHENTICATED)) {
server->on(EMSESP_DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&EMSESPDevicesService::emsespDevicesService, this, std::placeholders::_1),
AuthenticationPredicates::IS_AUTHENTICATED));
securityManager->wrapRequest(std::bind(&EMSESPDevicesService::all_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
server->on(SCAN_DEVICES_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&EMSESPDevicesService::scan_devices, this, _1), AuthenticationPredicates::IS_AUTHENTICATED));
_device_dataHandler.setMethod(HTTP_POST);
_device_dataHandler.setMaxContentLength(256);
server->addHandler(&_device_dataHandler);
}
void EMSESPDevicesService::emsespDevicesService(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_STATUS_SIZE);
void EMSESPDevicesService::scan_devices(AsyncWebServerRequest * request) {
EMSESP::send_read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
request->send(200);
}
void EMSESPDevicesService::all_devices(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_DEVICE_SIZE);
JsonObject root = response->getRoot();
JsonArray devices = root.createNestedArray("devices");
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
JsonObject deviceRoot = devices.createNestedObject();
deviceRoot["type"] = emsdevice->device_type_name();
deviceRoot["brand"] = emsdevice->brand_to_string();
deviceRoot["name"] = emsdevice->name();
deviceRoot["deviceid"] = emsdevice->device_id();
deviceRoot["productid"] = emsdevice->product_id();
deviceRoot["version"] = emsdevice->version();
JsonObject obj = devices.createNestedObject();
obj["id"] = emsdevice->unique_id();
obj["type"] = emsdevice->device_type_name();
obj["brand"] = emsdevice->brand_to_string();
obj["name"] = emsdevice->name();
obj["deviceid"] = emsdevice->device_id();
obj["productid"] = emsdevice->product_id();
obj["version"] = emsdevice->version();
}
}
JsonArray sensors = root.createNestedArray("sensors");
if (!EMSESP::sensor_devices().empty()) {
for (const auto & sensor : EMSESP::sensor_devices()) {
JsonObject obj = sensors.createNestedObject();
obj["id"] = sensor.to_string();
obj["temp"] = sensor.temperature_c;
}
}
response->setLength();
request->send(response);
}
void EMSESPDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
uint8_t id = json["id"]; // get id from selected table row
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_DEVICE_SIZE);
#ifndef EMSESP_STANDALONE
EMSESP::device_info(id, (JsonObject &)response->getRoot());
#endif
response->setLength();
request->send(response);
} else {
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
}
}
} // namespace emsesp

View File

@@ -1,3 +1,21 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESPDevicesService_h
#define EMSESPDevicesService_h
@@ -6,23 +24,27 @@
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
// #include <HttpEndpoint.h>
// #include <MqttPubSub.h>
// #include <WebSocketTxRx.h>
// #define MAX_EMSESP_STATUS_SIZE 1024
#define MAX_EMSESP_DEVICE_SIZE 1280
#include "version.h"
#define MAX_EMSESP_STATUS_SIZE 1024
#define EMSESP_DEVICES_SERVICE_PATH "/rest/emsespDevices"
#define EMSESP_DEVICES_SERVICE_PATH "/rest/allDevices"
#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices"
#define DEVICE_DATA_SERVICE_PATH "/rest/deviceData"
namespace emsesp {
using namespace std::placeholders; // for `_1`
class EMSESPDevicesService {
public:
EMSESPDevicesService(AsyncWebServer * server, SecurityManager * securityManager);
private:
void emsespDevicesService(AsyncWebServerRequest * request);
void all_devices(AsyncWebServerRequest * request);
void scan_devices(AsyncWebServerRequest * request);
void device_data(AsyncWebServerRequest * request, JsonVariant & json);
AsyncCallbackJsonWebHandler _device_dataHandler;
};
} // namespace emsesp

View File

@@ -1,20 +0,0 @@
#include <EMSESPScanDevicesService.h>
#include "emsesp.h"
namespace emsesp {
EMSESPScanDevicesService::EMSESPScanDevicesService(AsyncWebServer * server, SecurityManager * securityManager) {
server->on(SCAN_DEVICES_SERVICE_PATH,
HTTP_POST,
securityManager->wrapRequest(std::bind(&EMSESPScanDevicesService::scan_devices, this, std::placeholders::_1), AuthenticationPredicates::IS_ADMIN));
}
void EMSESPScanDevicesService::scan_devices(AsyncWebServerRequest * request) {
request->onDisconnect([]() {
EMSESP::send_read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
});
request->send(200);
}
} // namespace emsesp

View File

@@ -1,21 +0,0 @@
#ifndef EMSESPScanDevicesService_h
#define EMSESPScanDevicesService_h
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#define SCAN_DEVICES_SERVICE_PATH "/rest/scanDevices"
namespace emsesp {
class EMSESPScanDevicesService {
public:
EMSESPScanDevicesService(AsyncWebServer * server, SecurityManager * securityManager);
private:
void scan_devices(AsyncWebServerRequest * request);
};
} // namespace emsesp
#endif

View File

@@ -1,3 +1,21 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "EMSESPSettingsService.h"
#include "emsesp.h"
@@ -6,62 +24,46 @@ namespace emsesp {
EMSESPSettingsService::EMSESPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager)
: _httpEndpoint(EMSESPSettings::read, EMSESPSettings::update, this, server, EMSESP_SETTINGS_SERVICE_PATH, securityManager)
, _fsPersistence(EMSESPSettings::read, EMSESPSettings::update, this, fs, EMSESP_SETTINGS_FILE) {
addUpdateHandler([&](const String & originId) { onUpdate(); }, false);
}
void EMSESPSettings::read(EMSESPSettings & settings, JsonObject & root) {
root["tx_mode"] = settings.tx_mode;
root["ems_bus_id"] = settings.ems_bus_id;
root["tx_mode"] = settings.tx_mode;
root["ems_bus_id"] = settings.ems_bus_id;
root["syslog_level"] = settings.syslog_level;
root["syslog_mark_interval"] = settings.syslog_mark_interval;
root["syslog_host"] = settings.syslog_host;
root["master_thermostat"] = settings.master_thermostat;
root["shower_timer"] = settings.shower_timer;
root["shower_alert"] = settings.shower_alert;
root["master_thermostat"] = settings.master_thermostat;
root["shower_timer"] = settings.shower_timer;
root["shower_alert"] = settings.shower_alert;
}
StateUpdateResult EMSESPSettings::update(JsonObject & root, EMSESPSettings & settings) {
EMSESPSettings newSettings = {};
newSettings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE;
newSettings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID;
settings.tx_mode = root["tx_mode"] | EMSESP_DEFAULT_TX_MODE;
settings.ems_bus_id = root["ems_bus_id"] | EMSESP_DEFAULT_EMS_BUS_ID;
settings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL;
settings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL;
settings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST;
settings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT;
settings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER;
settings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT;
newSettings.syslog_level = root["syslog_level"] | EMSESP_DEFAULT_SYSLOG_LEVEL;
newSettings.syslog_mark_interval = root["syslog_mark_interval"] | EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL;
newSettings.syslog_host = root["syslog_host"] | EMSESP_DEFAULT_SYSLOG_HOST;
return StateUpdateResult::CHANGED;
}
newSettings.master_thermostat = root["master_thermostat"] | EMSESP_DEFAULT_MASTER_THERMOSTAT;
newSettings.shower_timer = root["shower_timer"] | EMSESP_DEFAULT_SHOWER_TIMER;
newSettings.shower_alert = root["shower_alert"] | EMSESP_DEFAULT_SHOWER_ALERT;
bool changed = false;
if (newSettings.tx_mode != settings.tx_mode) {
EMSESP::reset_tx(newSettings.tx_mode); // reset counters
changed = true;
}
if ((newSettings.shower_timer != settings.shower_timer) || (newSettings.shower_alert != settings.shower_alert)) {
EMSESP::shower_.start();
changed = true;
}
if ((newSettings.syslog_level != settings.syslog_level) || (newSettings.syslog_mark_interval != settings.syslog_mark_interval)
|| !newSettings.syslog_host.equals(settings.syslog_host)) {
EMSESP::system_.syslog_init();
changed = true;
}
if (changed) {
settings = newSettings;
return StateUpdateResult::CHANGED;
}
return StateUpdateResult::UNCHANGED;
// this is called after the settings have been persisted to the filesystem
void EMSESPSettingsService::onUpdate() {
EMSESP::shower_.start();
EMSESP::system_.syslog_init();
EMSESP::reset_tx();
}
void EMSESPSettingsService::begin() {
_fsPersistence.readFromFS();
}
void EMSESPSettingsService::save() {
_fsPersistence.writeToFS();
}
} // namespace emsesp

View File

@@ -1,3 +1,21 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESPSettingsConfig_h
#define EMSESPSettingsConfig_h
@@ -7,7 +25,7 @@
#define EMSESP_SETTINGS_FILE "/config/emsespSettings.json"
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings"
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#define EMSESP_DEFAULT_TX_MODE 1 // EMS1.0
#define EMSESP_DEFAULT_EMS_BUS_ID 0x0B // service key
#define EMSESP_DEFAULT_SYSLOG_LEVEL -1 // OFF
@@ -44,10 +62,13 @@ class EMSESPSettingsService : public StatefulService<EMSESPSettings> {
EMSESPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
void save();
private:
HttpEndpoint<EMSESPSettings> _httpEndpoint;
FSPersistence<EMSESPSettings> _fsPersistence;
void onUpdate();
};
} // namespace emsesp

View File

@@ -1,3 +1,21 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "EMSESPStatusService.h"
#include "emsesp.h"
#include "mqtt.h"
@@ -12,38 +30,30 @@ EMSESPStatusService::EMSESPStatusService(AsyncWebServer * server, SecurityManage
securityManager->wrapRequest(std::bind(&EMSESPStatusService::emsespStatusService, this, std::placeholders::_1),
AuthenticationPredicates::IS_AUTHENTICATED));
// trigger on wifi connects
// trigger on wifi connects/disconnects
#ifdef ESP32
WiFi.onEvent(onStationModeConnected, WiFiEvent_t::SYSTEM_EVENT_STA_CONNECTED);
WiFi.onEvent(onStationModeDisconnected, WiFiEvent_t::SYSTEM_EVENT_STA_DISCONNECTED);
WiFi.onEvent(onStationModeGotIP, WiFiEvent_t::SYSTEM_EVENT_STA_GOT_IP);
#elif defined(ESP8266)
_onStationModeConnectedHandler = WiFi.onStationModeConnected(onStationModeConnected);
_onStationModeDisconnectedHandler = WiFi.onStationModeDisconnected(onStationModeDisconnected);
_onStationModeGotIPHandler = WiFi.onStationModeGotIP(onStationModeGotIP);
#endif
}
#ifdef ESP32
void EMSESPStatusService::onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info) {
EMSESP::logger().debug(F("Wifi Connected"));
}
void EMSESPStatusService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info) {
EMSESP::logger().debug(F("WiFi Disconnected. Reason code=%d"), info.disconnected.reason);
}
void EMSESPStatusService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
EMSESP::logger().debug(F("WiFi connected with IP=%s, hostname=%s"), WiFi.localIP().toString().c_str(), WiFi.getHostname());
EMSESP::logger().debug(F("WiFi Connected with IP=%s, hostname=%s"), WiFi.localIP().toString().c_str(), WiFi.getHostname());
EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection
}
#elif defined(ESP8266)
void EMSESPStatusService::onStationModeConnected(const WiFiEventStationModeConnected & event) {
EMSESP::logger().debug(F("Wifi connected with SSID %s"), event.ssid.c_str());
}
void EMSESPStatusService::onStationModeDisconnected(const WiFiEventStationModeDisconnected & event) {
EMSESP::logger().debug(F("WiFi Disconnected. Reason code=%d"), event.reason);
}
void EMSESPStatusService::onStationModeGotIP(const WiFiEventStationModeGotIP & event) {
EMSESP::logger().debug(F("WiFi connected with IP=%s, hostname=%s"), event.ip.toString().c_str(), WiFi.hostname().c_str());
EMSESP::logger().debug(F("WiFi Connected with IP=%s, hostname=%s"), event.ip.toString().c_str(), WiFi.hostname().c_str());
EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection
}
#endif

View File

@@ -1,3 +1,21 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2019 Paul Derbyshire
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef EMSESPStatusService_h
#define EMSESPStatusService_h
@@ -20,14 +38,11 @@ class EMSESPStatusService {
void emsespStatusService(AsyncWebServerRequest * request);
#ifdef ESP32
static void onStationModeConnected(WiFiEvent_t event, WiFiEventInfo_t info);
static void onStationModeDisconnected(WiFiEvent_t event, WiFiEventInfo_t info);
static void onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info);
#elif defined(ESP8266)
WiFiEventHandler _onStationModeConnectedHandler;
WiFiEventHandler _onStationModeDisconnectedHandler;
WiFiEventHandler _onStationModeGotIPHandler;
static void onStationModeConnected(const WiFiEventStationModeConnected & event);
static void onStationModeDisconnected(const WiFiEventStationModeDisconnected & event);
static void onStationModeGotIP(const WiFiEventStationModeGotIP & event);
#endif

View File

@@ -90,6 +90,7 @@ void EMSESPShell::display_banner() {
// turn off watch
emsesp::EMSESP::watch_id(WATCH_ID_NONE);
emsesp::EMSESP::watch(EMSESP::WATCH_OFF);
}
// pre-loads all the console commands into the MAIN context
@@ -107,11 +108,9 @@ void EMSESPShell::add_console_commands() {
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(refresh)},
flash_string_vector{F_(fetch)},
[&](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.printfln(F("Requesting data from EMS devices"));
console_commands_loaded_ = false;
add_console_commands();
EMSESP::fetch_device_values();
});
@@ -177,15 +176,14 @@ void EMSESPShell::add_console_commands() {
flash_string_vector{F_(n_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t tx_mode = std::strtol(arguments[0].c_str(), nullptr, 10);
// save the tx_mode
EMSESP::emsespSettingsService.update(
[&](EMSESPSettings & settings) {
settings.tx_mode = tx_mode;
shell.printfln(F_(tx_mode_fmt), tx_mode);
return StateUpdateResult::CHANGED;
},
"local");
EMSESP::reset_tx(tx_mode); // reset counters and set tx_mode
EMSESP::reset_tx(); // reset counters and set tx_mode
});
commands->add_command(ShellContext::MAIN,
@@ -516,6 +514,9 @@ void Console::start() {
shell = std::make_shared<EMSESPStreamConsole>(serial_console_, true);
shell->maximum_log_messages(100); // default is 50
shell->start();
#endif
#if defined(EMSESP_DEBUG)
shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all
#endif
@@ -524,8 +525,6 @@ void Console::start() {
shell->add_flags(CommandFlags::ADMIN);
#endif
emsesp::EMSESP::watch(EMSESP::WATCH_OFF); // turn watch off in case it was still set in the last session
// start the telnet service
// default idle is 10 minutes, default write timeout is 0 (automatic)
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash
@@ -533,6 +532,9 @@ void Console::start() {
telnet_.start();
telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
#endif
// turn watch off in case it was still set in the last session
emsesp::EMSESP::watch(EMSESP::WATCH_OFF);
}
// handles telnet sync and logging to console

View File

@@ -73,7 +73,7 @@ MAKE_PSTR_WORD(read)
MAKE_PSTR_WORD(version)
MAKE_PSTR_WORD(values)
MAKE_PSTR_WORD(system)
MAKE_PSTR_WORD(refresh)
MAKE_PSTR_WORD(fetch)
MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(format)
MAKE_PSTR_WORD(raw)
@@ -191,8 +191,8 @@ class EMSESPStreamConsole : public uuid::console::StreamConsole, public EMSESPSh
class Console {
public:
static void loop();
void start();
void loop();
void start();
uuid::log::Level log_level();

View File

@@ -28,6 +28,8 @@ MAKE_PSTR_WORD(comfort)
MAKE_PSTR_WORD(eco)
MAKE_PSTR_WORD(intelligent)
MAKE_PSTR_WORD(hot)
MAKE_PSTR_WORD(maxpower)
MAKE_PSTR_WORD(minpower)
MAKE_PSTR(comfort_mandatory, "<hot | eco | intelligent>")
@@ -59,7 +61,7 @@ Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const
register_telegram_type(0x33, F("UBAParameterWW"), true, std::bind(&Boiler::process_UBAParameterWW, this, _1));
register_telegram_type(0x14, F("UBATotalUptime"), false, std::bind(&Boiler::process_UBATotalUptime, this, _1));
register_telegram_type(0x35, F("UBAFlags"), false, std::bind(&Boiler::process_UBAFlags, this, _1));
register_telegram_type(0x15, F("UBAMaintenanceSettings"), false, std::bind(&Boiler::process_UBAMaintenanceSettings, this, _1));
register_telegram_type(0x15, F("UBAMaintenanceData"), false, std::bind(&Boiler::process_UBAMaintenanceData, this, _1));
register_telegram_type(0x16, F("UBAParameters"), true, std::bind(&Boiler::process_UBAParameters, this, _1));
register_telegram_type(0x1A, F("UBASetPoints"), false, std::bind(&Boiler::process_UBASetPoints, this, _1));
register_telegram_type(0xD1, F("UBAOutdoorTemp"), false, std::bind(&Boiler::process_UBAOutdoorTemp, this, _1));
@@ -103,18 +105,50 @@ void Boiler::boiler_cmd(const char * message) {
uint8_t t = doc["wwtemp"];
set_warmwater_temp(t);
}
if (nullptr != doc["boilhyston"]) {
int8_t t = doc["boilhyston"];
set_hyst_on(t);
}
if (nullptr != doc["boilhystoff"]) {
uint8_t t = doc["boilhystoff"];
set_hyst_off(t);
}
if (nullptr != doc["burnperiod"]) {
uint8_t t = doc["burnperiod"];
set_burn_period(t);
}
if (nullptr != doc["burnminpower"]) {
uint8_t p = doc["burnminpower"];
set_min_power(p);
}
if (nullptr != doc["burnmaxpower"]) {
uint8_t p = doc["burnmaxpower"];
set_max_power(p);
}
if (nullptr != doc["pumpdelay"]) {
uint8_t t = doc["pumpdelay"];
set_pump_delay(t);
}
if (nullptr != doc["comfort"]) {
const char * data = doc["comfort"];
if (strcmp((char *)data, "hot") == 0) {
set_warmwater_mode(1);
} else if (strcmp((char *)data, "eco") == 0) {
set_warmwater_mode(2);
} else if (strcmp((char *)data, "intelligent") == 0) {
set_warmwater_mode(3);
}
}
const char * command = doc["cmd"];
if (command == nullptr) {
if (command == nullptr || doc["data"] == nullptr) {
return;
}
// boiler ww comfort setting
if (strcmp(command, "comfort") == 0) {
const char * data = doc["data"];
if (data == nullptr) {
return;
}
if (strcmp((char *)data, "hot") == 0) {
set_warmwater_mode(1);
} else if (strcmp((char *)data, "eco") == 0) {
@@ -128,9 +162,45 @@ void Boiler::boiler_cmd(const char * message) {
// boiler flowtemp setting
if (strcmp(command, "flowtemp") == 0) {
uint8_t t = doc["data"];
if (t) {
set_flow_temp(t);
}
set_flow_temp(t);
return;
}
if (strcmp(command, "wwtemp") == 0) {
uint8_t t = doc["data"];
set_warmwater_temp(t);
return;
}
// boiler max power setting
if (strcmp(command, "burnmaxpower") == 0) {
uint8_t p = doc["data"];
set_max_power(p);
return;
}
// boiler min power setting
if (strcmp(command, "burnminpower") == 0) {
uint8_t p = doc["data"];
set_min_power(p);
return;
}
if (strcmp(command, "boilhyston") == 0) {
int8_t t = doc["data"];
set_hyst_on(t);
return;
}
if (strcmp(command, "boilhystoff") == 0) {
uint8_t t = doc["data"];
set_hyst_off(t);
return;
}
if (strcmp(command, "burnperiod") == 0) {
uint8_t t = doc["data"];
set_burn_period(t);
return;
}
if (strcmp(command, "pumpdelay") == 0) {
uint8_t t = doc["data"];
set_pump_delay(t);
return;
}
}
@@ -166,9 +236,27 @@ void Boiler::boiler_cmd_wwtemp(const char * message) {
}
}
void Boiler::device_info(JsonArray & root) {
JsonObject dataElement;
dataElement = root.createNestedObject();
dataElement["name"] = F("Hot tap water");
dataElement["value"] = tap_water_active_ ? F("running") : F("off");
dataElement = root.createNestedObject();
dataElement["name"] = F("Central heating");
dataElement["value"] = heating_active_ ? F("active") : F("off");
render_value_json(root, "", F("Selected flow temperature"), selFlowTemp_, F_(degrees));
render_value_json(root, "", F("Current flow temperature"), curFlowTemp_, F_(degrees), 10);
render_value_json(root, "", F("Warm Water selected temperature"), wWSelTemp_, F_(degrees));
render_value_json(root, "", F("Warm Water set temperature"), wWSetTmp_, F_(degrees));
render_value_json(root, "", F("Warm Water current temperature (intern)"), wWCurTmp_, F_(degrees), 10);
}
// publish values via MQTT
void Boiler::publish_values() {
const size_t capacity = JSON_OBJECT_SIZE(47); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/
const size_t capacity = JSON_OBJECT_SIZE(56); // must recalculate if more objects addded https://arduinojson.org/v6/assistant/
DynamicJsonDocument doc(capacity);
char s[10]; // for formatting strings
@@ -280,7 +368,7 @@ void Boiler::publish_values() {
doc["flameCurr"] = (float)(int16_t)flameCurr_ / 10;
}
if (Helpers::hasValue(heatPmp_, VALUE_BOOL)) {
doc["heatPmp"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL);
doc["heatPump"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(fanWork_, VALUE_BOOL)) {
doc["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL);
@@ -292,13 +380,37 @@ void Boiler::publish_values() {
doc["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(heating_temp_)) {
doc["heating_temp"] = heating_temp_;
doc["heatingTemp"] = heating_temp_;
}
if (Helpers::hasValue(pump_mod_max_)) {
doc["pump_mod_max"] = pump_mod_max_;
doc["pumpModMax"] = pump_mod_max_;
}
if (Helpers::hasValue(pump_mod_min_)) {
doc["pump_mod_min"] = pump_mod_min_;
doc["pumpModMin"] = pump_mod_min_;
}
if (Helpers::hasValue(pumpDelay_)) {
doc["pumpDelay"] = pumpDelay_;
}
if (Helpers::hasValue(burnPeriod_)) {
doc["burnMinPeriod"] = burnPeriod_;
}
if (Helpers::hasValue(burnPowermin_)) {
doc["burnMinPower"] = burnPowermin_;
}
if (Helpers::hasValue(burnPowermax_)) {
doc["burnMaxPower"] = burnPowermax_;
}
if (Helpers::hasValue(boilTemp_on_)) {
doc["boilHystOn"] = boilTemp_on_;
}
if (Helpers::hasValue(boilTemp_off_)) {
doc["boilHystOff"] = boilTemp_off_;
}
if (Helpers::hasValue(setFlowTemp_)) {
doc["setFlowTemp"] = setFlowTemp_;
}
if (Helpers::hasValue(setWWPumpPow_)) {
doc["wWSetPumpPower"] = setWWPumpPow_;
}
if (Helpers::hasValue(wWStarts_)) {
doc["wWStarts"] = wWStarts_;
@@ -318,7 +430,6 @@ void Boiler::publish_values() {
if (Helpers::hasValue(heatWorkMin_)) {
doc["heatWorkMin"] = heatWorkMin_;
}
if (Helpers::hasValue(serviceCode_)) {
doc["serviceCode"] = serviceCodeChar_;
doc["serviceCodeNumber"] = serviceCode_;
@@ -416,6 +527,17 @@ void Boiler::show_values(uuid::console::Shell & shell) {
print_value(shell, 2, F("Heating temperature setting on the boiler"), heating_temp_, F_(degrees));
print_value(shell, 2, F("Boiler circuit pump modulation max power"), pump_mod_max_, F_(percent));
print_value(shell, 2, F("Boiler circuit pump modulation min power"), pump_mod_min_, F_(percent));
print_value(shell, 2, F("Boiler circuit pump delay time"), pumpDelay_, F("min"));
print_value(shell, 2, F("Boiler temp hysteresis on"), boilTemp_on_, F_(degrees));
print_value(shell, 2, F("Boiler temp hysteresis off"), boilTemp_off_, F_(degrees));
print_value(shell, 2, F("Boiler burner min period"), burnPeriod_, F("min"));
print_value(shell, 2, F("Boiler burner min power"), burnPowermin_, F_(percent));
print_value(shell, 2, F("Boiler burner max power"), burnPowermax_, F_(percent));
// UBASetPoint - these may differ from the above
print_value(shell, 2, F("Set Flow temperature"), setFlowTemp_, F_(degrees));
print_value(shell, 2, F("Boiler burner set power"), setBurnPow_, F_(percent));
print_value(shell, 2, F("Warm Water pump set power"), setWWPumpPow_, F_(percent));
// UBAMonitorSlow
if (Helpers::hasValue(extTemp_)) {
@@ -528,6 +650,12 @@ void Boiler::process_UBATotalUptime(std::shared_ptr<const Telegram> telegram) {
*/
void Boiler::process_UBAParameters(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(heating_temp_, 1);
telegram->read_value(burnPowermax_,2);
telegram->read_value(burnPowermin_,3);
telegram->read_value(boilTemp_off_,4);
telegram->read_value(boilTemp_on_,5);
telegram->read_value(burnPeriod_,6);
telegram->read_value(pumpDelay_,8);
telegram->read_value(pump_mod_max_, 9);
telegram->read_value(pump_mod_min_, 10);
}
@@ -652,16 +780,16 @@ void Boiler::process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(extTemp_, 0);
}
// UBASetPoint 0x1A
void Boiler::process_UBASetPoints(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(setFlowTemp_, 0); // boiler set temp from thermostat
telegram->read_value(setBurnPow_, 1); // max output power in %
telegram->read_value(setWWPumpPow_, 2); // ww pump speed/power?
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// UBASetPoint 0x1A
// not yet implemented
void Boiler::process_UBASetPoints(std::shared_ptr<const Telegram> telegram) {
// uint8_t setpoint = telegram->message_data[0]; // boiler flow temp
// uint8_t ww_power = telegram->message_data[2]; // power in %
}
// 0x35
// not yet implemented
void Boiler::process_UBAFlags(std::shared_ptr<const Telegram> telegram) {
@@ -670,11 +798,7 @@ void Boiler::process_UBAFlags(std::shared_ptr<const Telegram> telegram) {
// 0x1C
// not yet implemented
void Boiler::process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegram) {
}
// 0x15
// not yet implemented
void Boiler::process_UBAMaintenanceSettings(std::shared_ptr<const Telegram> telegram) {
// first byte: Maintenance due (0 = no, 3 = yes, due to operating hours, 8 = yes, due to date)
}
// 0x10, 0x11, 0x12
@@ -685,12 +809,20 @@ void Boiler::process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram) {
#pragma GCC diagnostic pop
// 0x15
void Boiler::process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram) {
// first byte: Maintenance messages (0 = none, 1 = by operating hours, 2 = by date)
// I see a value of 3 in the 1st byte when the boiler is booted, so probably a flag
if (telegram->message_data[0] == 3) {
LOG_WARNING(F("Boiler has booted."));
}
}
// Set the warm water temperature 0x33
void Boiler::set_warmwater_temp(const uint8_t temperature) {
LOG_INFO(F("Setting boiler warm water temperature to %d C"), temperature);
write_command(EMS_TYPE_UBAParameterWW, 2, temperature);
// for i9000, see #397
write_command(EMS_TYPE_UBAFlags, 3, temperature);
write_command(EMS_TYPE_UBAFlags, 3, temperature); // for i9000, see #397
}
// flow temp
@@ -699,6 +831,42 @@ void Boiler::set_flow_temp(const uint8_t temperature) {
write_command(EMS_TYPE_UBASetPoints, 0, temperature);
}
// set min boiler output
void Boiler::set_min_power(const uint8_t power) {
LOG_INFO(F("Setting boiler min power to "), power);
write_command(EMS_TYPE_UBAParameters, 3, power);
}
// set max temp
void Boiler::set_max_power(const uint8_t power) {
LOG_INFO(F("Setting boiler max power to %d C"), power);
write_command(EMS_TYPE_UBAParameters, 2, power);
}
// set oiler on hysteresis
void Boiler::set_hyst_on(const uint8_t temp) {
LOG_INFO(F("Setting boiler hysteresis on to %d C"), temp);
write_command(EMS_TYPE_UBAParameters, 5, temp);
}
// set boiler off hysteresis
void Boiler::set_hyst_off(const uint8_t temp) {
LOG_INFO(F("Setting boiler hysteresis off to %d C"), temp);
write_command(EMS_TYPE_UBAParameters, 4, temp);
}
// set min burner period
void Boiler::set_burn_period(const uint8_t t) {
LOG_INFO(F("Setting burner min. period to %d min"), t);
write_command(EMS_TYPE_UBAParameters, 6, t);
}
// set pump delay
void Boiler::set_pump_delay(const uint8_t t) {
LOG_INFO(F("Setting boiler pump delay to %d min"), t);
write_command(EMS_TYPE_UBAParameters, 8, t);
}
// 1=hot, 2=eco, 3=intelligent
// note some boilers do not have this setting, than it's done by thermostat
// on a RC35 it's by EMSESP::send_write_request(0x37, 0x10, 2, &set, 1, 0); (set is 1,2,3)
@@ -803,6 +971,22 @@ void Boiler::console_commands(Shell & shell, unsigned int context) {
set_flow_temp(Helpers::atoint(arguments.front().c_str()));
});
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(maxpower)},
flash_string_vector{F_(n_mandatory)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
set_max_power(Helpers::atoint(arguments.front().c_str()));
});
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(minpower)},
flash_string_vector{F_(n_mandatory)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
set_min_power(Helpers::atoint(arguments.front().c_str()));
});
EMSESPShell::commands->add_command(
ShellContext::BOILER,
CommandFlags::ADMIN,

View File

@@ -40,6 +40,7 @@ class Boiler : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();
@@ -54,6 +55,7 @@ class Boiler : public EMSdevice {
static constexpr uint8_t EMS_TYPE_UBAFunctionTest = 0x1D;
static constexpr uint8_t EMS_TYPE_UBAFlags = 0x35;
static constexpr uint8_t EMS_TYPE_UBASetPoints = 0x1A;
static constexpr uint8_t EMS_TYPE_UBAParameters = 0x16;
static constexpr uint8_t EMS_BOILER_SELFLOWTEMP_HEATING = 20; // was originally 70, changed to 30 for issue #193, then to 20 with issue #344
@@ -119,11 +121,22 @@ class Boiler : public EMSdevice {
uint8_t heating_temp_ = EMS_VALUE_UINT_NOTSET; // Heating temperature setting on the boiler
uint8_t pump_mod_max_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation max. power %
uint8_t pump_mod_min_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation min. power
uint8_t burnPowermin_ = EMS_VALUE_UINT_NOTSET;
uint8_t burnPowermax_ = EMS_VALUE_UINT_NOTSET;
int8_t boilTemp_off_ = EMS_VALUE_INT_NOTSET;
int8_t boilTemp_on_ = EMS_VALUE_UINT_NOTSET;
uint8_t burnPeriod_ = EMS_VALUE_UINT_NOTSET;
uint8_t pumpDelay_ = EMS_VALUE_UINT_NOTSET;
// UBASetPoint
uint8_t setFlowTemp_ = EMS_VALUE_UINT_NOTSET; // boiler setpoint temp
uint8_t setBurnPow_ = EMS_VALUE_UINT_NOTSET; // max output power in %
uint8_t setWWPumpPow_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power?
// other internal calculated params
uint8_t tap_water_active_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off
uint8_t heating_active_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off
uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps)
uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps)
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);
@@ -140,7 +153,7 @@ class Boiler : public EMSdevice {
void process_UBAFlags(std::shared_ptr<const Telegram> telegram);
void process_MC10Status(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceStatus(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceSettings(std::shared_ptr<const Telegram> telegram);
void process_UBAMaintenanceData(std::shared_ptr<const Telegram> telegram);
void process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram);
void process_UBADHWStatus(std::shared_ptr<const Telegram> telegram);
@@ -154,6 +167,13 @@ class Boiler : public EMSdevice {
void set_tapwarmwater_activated(const bool activated);
void set_warmwater_onetime(const bool activated);
void set_warmwater_circulation(const bool activated);
void set_min_power(const uint8_t power);
void set_max_power(const uint8_t power);
void set_hyst_on(const uint8_t temp);
void set_hyst_off(const uint8_t temp);
void set_burn_period(const uint8_t t);
void set_pump_delay(const uint8_t t);
// mqtt callbacks
void boiler_cmd(const char * message);

View File

@@ -34,6 +34,9 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
// register_mqtt_topic("topic", std::bind(&Connect::cmd, this, _1));
}
void Connect::device_info(JsonArray & root) {
}
void Connect::add_context_menu() {
}

View File

@@ -37,6 +37,7 @@ class Connect : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -39,6 +39,9 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i
void Controller::add_context_menu() {
}
void Controller::device_info(JsonArray & root) {
}
// display all values into the shell console
void Controller::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header

View File

@@ -37,6 +37,7 @@ class Controller : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -39,6 +39,9 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
void Gateway::add_context_menu() {
}
void Gateway::device_info(JsonArray & root) {
}
// display all values into the shell console
void Gateway::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header

View File

@@ -37,6 +37,7 @@ class Gateway : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -50,6 +50,9 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c
void Heatpump::add_context_menu() {
}
void Heatpump::device_info(JsonArray & root) {
}
// display all values into the shell console
void Heatpump::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header

View File

@@ -37,6 +37,7 @@ class Heatpump : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -57,6 +57,24 @@ Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const
void Mixing::add_context_menu() {
}
// output json to web UI
void Mixing::device_info(JsonArray & root) {
if (type_ == Type::NONE) {
return; // don't have any values yet
}
if (type_ == Type::WWC) {
render_value_json(root, "", F("Warm Water Circuit"), hc_, nullptr);
} else {
render_value_json(root, "", F("Heating Circuit"), hc_, nullptr);
}
render_value_json(root, "", F("Current flow temperature"), flowTemp_, F_(degrees), 10);
render_value_json(root, "", F("Setpoint flow temperature"), flowSetTemp_, F_(degrees));
render_value_json(root, "", F("Current pump modulation"), pumpMod_, F_(percent));
render_value_json(root, "", F("Current valve status"), status_, nullptr);
}
// check to see if values have been updated
bool Mixing::updated_values() {
return false;

View File

@@ -37,6 +37,7 @@ class Mixing : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -58,6 +58,32 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
void Solar::add_context_menu() {
}
// print to web
void Solar::device_info(JsonArray & root) {
render_value_json(root, "", F("Collector temperature (TS1)"), collectorTemp_, F_(degrees), 10);
render_value_json(root, "", F("Bottom temperature (TS2)"), bottomTemp_, F_(degrees), 10);
render_value_json(root, "", F("Bottom temperature (TS5)"), bottomTemp2_, F_(degrees), 10);
render_value_json(root, "", F("Pump modulation"), pumpModulation_, F_(percent));
render_value_json(root, "", F("Valve (VS2) status"), valveStatus_, nullptr, EMS_VALUE_BOOL);
render_value_json(root, "", F("Pump (PS1) active"), pump_, nullptr, EMS_VALUE_BOOL);
if (Helpers::hasValue(pumpWorkMin_)) {
JsonObject dataElement;
dataElement = root.createNestedObject();
dataElement["name"] = F("Pump working time");
std::string time_str(60, '\0');
snprintf_P(&time_str[0], time_str.capacity() + 1, PSTR("%d days %d hours %d minutes"), pumpWorkMin_ / 1440, (pumpWorkMin_ % 1440) / 60, pumpWorkMin_ % 60);
dataElement["value"] = time_str;
}
render_value_json(root, "", F("Tank Heated"), tankHeated_, nullptr, EMS_VALUE_BOOL);
render_value_json(root, "", F("Collector shutdown"), collectorOnOff_, nullptr, EMS_VALUE_BOOL);
render_value_json(root, "", F("Energy last hour"), energyLastHour_, F_(wh), 10);
render_value_json(root, "", F("Energy today"), energyToday_, F_(wh));
render_value_json(root, "", F("Energy total"), energyTotal_, F_(kwh), 10);
}
// display all values into the shell console
void Solar::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
@@ -74,7 +100,7 @@ void Solar::show_values(uuid::console::Shell & shell) {
}
print_value(shell, 2, F("Tank Heated"), tankHeated_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Collector"), collectorOnOff_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Collector shutdown"), collectorOnOff_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Energy last hour"), energyLastHour_, F_(wh), 10);
print_value(shell, 2, F("Energy today"), energyToday_, F_(wh));
@@ -203,8 +229,8 @@ void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
pumpModulation_ = 15; // set to minimum
}
telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422
telegram->read_bitvalue(collectorOnOff_, 3, 0);
telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422
telegram->read_bitvalue(collectorOnOff_, 3, 0); // collector shutdown
}
/*
@@ -240,11 +266,14 @@ void Solar::process_ISM1StatusMessage(std::shared_ptr<const Telegram> telegram)
if (Wh != 0xFFFF) {
energyLastHour_ = Wh * 10; // set to *10
}
telegram->read_bitvalue(collectorOnOff_, 9, 0); // collector on/off
telegram->read_bitvalue(pump_, 8, 0); // Solar pump on (1) or off (0)
telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes
telegram->read_bitvalue(collectorOnOff_, 9, 0); // collector shutdown on/off
telegram->read_bitvalue(tankHeated_, 9, 2); // tank full
}
/*
* Junkers ISM1 Solar Module - type 0x0101 EMS+ for setting values
* e.g. 90 30 FF 06 00 01 50
*/
void Solar::process_ISM1Set(std::shared_ptr<const Telegram> telegram) {

View File

@@ -37,6 +37,7 @@ class Solar : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();
@@ -57,7 +58,7 @@ class Solar : public EMSdevice {
uint32_t energyTotal_ = EMS_VALUE_ULONG_NOTSET;
uint32_t pumpWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total solar pump operating time
uint8_t tankHeated_ = EMS_VALUE_BOOL_NOTSET;
uint8_t collectorOnOff_ = EMS_VALUE_BOOL_NOTSET;
uint8_t collectorOnOff_ = EMS_VALUE_BOOL_NOTSET; // Collector shutdown on/off
uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET;

View File

@@ -39,6 +39,9 @@ Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const
void Switch::add_context_menu() {
}
void Switch::device_info(JsonArray & root) {
}
// display all values into the shell console
void Switch::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header

View File

@@ -37,6 +37,7 @@ class Switch : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -141,8 +141,7 @@ Thermostat::Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_i
// if we're on auto mode, register this thermostat if it has a device id of 0x10 or 0x17
// or if its the master thermostat we defined
// see https://github.com/proddy/EMS-ESP/issues/362#issuecomment-629628161
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT) && ((device_id == 0x10) || (device_id == 0x17)))
|| (master_thermostat == device_id)) {
if (((num_devices == 1) && (actual_master_thermostat == EMSESP_DEFAULT_MASTER_THERMOSTAT)) || (master_thermostat == device_id)) {
EMSESP::actual_master_thermostat(device_id);
LOG_DEBUG(F("Registering new thermostat with device ID 0x%02X (as master)"), device_id);
init_mqtt();
@@ -169,6 +168,59 @@ void Thermostat::init_mqtt() {
register_mqtt_topic("thermostat_cmd_mode", std::bind(&Thermostat::thermostat_cmd_mode, this, _1));
}
// prepare data for Web UI
void Thermostat::device_info(JsonArray & root) {
uint8_t flags = (this->flags() & 0x0F); // specific thermostat characteristics, strip the option bits
for (const auto & hc : heating_circuits_) {
if (!Helpers::hasValue(hc->setpoint_roomTemp)) {
break; // skip this HC
}
// different thermostat types store their temperature values differently
uint8_t format_setpoint, format_curr;
switch (flags) {
case EMS_DEVICE_FLAG_EASY:
format_setpoint = 100; // *100
format_curr = 100; // *100
break;
case EMS_DEVICE_FLAG_JUNKERS:
format_setpoint = 10; // *10
format_curr = 10; // *10
break;
default: // RC30, RC35 etc...
format_setpoint = 2; // *2
format_curr = 10; // *10
break;
}
// create prefix with heating circuit number
std::string hc_str(5, '\0');
snprintf_P(&hc_str[0], hc_str.capacity() + 1, PSTR("hc%d: "), hc->hc_num());
render_value_json(root, hc_str, F("Current room temperature"), hc->curr_roomTemp, F_(degrees), format_curr);
render_value_json(root, hc_str, F("Setpoint room temperature"), hc->setpoint_roomTemp, F_(degrees), format_setpoint);
if (Helpers::hasValue(hc->mode)) {
JsonObject dataElement;
dataElement = root.createNestedObject();
std::string mode_str(15, '\0');
snprintf_P(&mode_str[0], mode_str.capacity() + 1, PSTR("%sMode"), hc_str.c_str());
dataElement["name"] = mode_str;
std::string modetype_str(20, '\0');
if (Helpers::hasValue(hc->summer_mode) && hc->summer_mode) {
snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - summer"), mode_tostring(hc->get_mode(flags)).c_str());
} else if (Helpers::hasValue(hc->holiday_mode) && hc->holiday_mode) {
snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - holiday"), mode_tostring(hc->get_mode(flags)).c_str());
} else if (Helpers::hasValue(hc->mode_type)) {
snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, PSTR("%s - %s"), mode_tostring(hc->get_mode(flags)).c_str(), mode_tostring(hc->get_mode_type(flags)).c_str());
} else {
snprintf_P(&modetype_str[0], modetype_str.capacity() + 1, mode_tostring(hc->get_mode(flags)).c_str());
}
dataElement["value"] = modetype_str;
}
}
}
// only add the menu for the master thermostat
void Thermostat::add_context_menu() {
if (device_id() != EMSESP::actual_master_thermostat()) {
@@ -258,7 +310,7 @@ void Thermostat::thermostat_cmd(const char * message) {
set_holiday(holiday.c_str(), hc_num);
}
}
// commands without heatingcircuit
if (nullptr != doc["wwmode"]) {
std::string mode = doc["wwmode"];
set_ww_mode(mode);
@@ -363,10 +415,10 @@ void Thermostat::thermostat_cmd(const char * message) {
// check for commands like {"hc":2,"cmd":"temp","data":21}
const char * command = doc["cmd"];
if (command == nullptr) {
if (command == nullptr || doc["data"] == nullptr) {
return;
}
// ok, we have command and data
if (strcmp(command, "temp") == 0) {
float f = doc["data"];
if (f) {
@@ -374,8 +426,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
// thermostat mode changes
if (strcmp(command, "mode") == 0) {
std::string mode = doc["data"];
if (mode.empty()) {
@@ -384,7 +434,6 @@ void Thermostat::thermostat_cmd(const char * message) {
set_mode(mode, hc_num);
return;
}
if (strcmp(command, "nighttemp") == 0) {
float f = doc["data"];
if (f) {
@@ -392,7 +441,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "daytemp") == 0) {
float f = doc["data"];
if (f) {
@@ -400,7 +448,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "holidaytemp") == 0) {
float f = doc["data"];
if (f) {
@@ -408,7 +455,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "ecotemp") == 0) {
float f = doc["data"];
if (f) {
@@ -416,7 +462,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "heattemp") == 0) {
float f = doc["data"];
if (f) {
@@ -424,7 +469,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "nofrosttemp") == 0) {
float f = doc["data"];
if (f) {
@@ -432,7 +476,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "summertemp") == 0) {
float f = doc["data"];
if (f) {
@@ -440,7 +483,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "designtemp") == 0) {
float f = doc["data"];
if (f) {
@@ -448,7 +490,6 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "offsettemp") == 0) {
float f = doc["data"];
if (f) {
@@ -456,6 +497,40 @@ void Thermostat::thermostat_cmd(const char * message) {
}
return;
}
if (strcmp(command, "remotetemp") == 0) {
float f = doc["data"];
if (f > 100 || f < 0) {
Roomctrl::set_remotetemp(hc_num - 1, EMS_VALUE_SHORT_NOTSET);
} else {
Roomctrl::set_remotetemp(hc_num - 1, (int16_t)(f * 10));
}
return;
}
if (strcmp(command, "control") == 0) {
uint8_t ctrl = doc["data"];
set_control(ctrl, hc_num);
return;
}
if (strcmp(command, "pause") == 0) {
uint8_t p = doc["data"];
set_pause(p, hc_num);
return;
}
if (strcmp(command, "party") == 0) {
uint8_t p = doc["data"];
set_party(p, hc_num);
return;
}
if (strcmp(command, "holiday") == 0) {
std::string holiday = doc["data"];
set_holiday(holiday.c_str(), hc_num);
return;
}
if (strcmp(command, "date") == 0) {
std::string date = doc["data"];
set_datetime(date.c_str());
return;
}
}
void Thermostat::thermostat_cmd_temp(const char * message) {
@@ -552,7 +627,8 @@ void Thermostat::publish_values() {
}
// send this specific data using the thermostat_data topic
if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::HA)) {
// if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::HA)) {
if (mqtt_format_ != MQTT_format::NESTED) {
Mqtt::publish("thermostat_data", doc);
rootThermostat = doc.to<JsonObject>(); // clear object
}
@@ -566,7 +642,8 @@ void Thermostat::publish_values() {
has_data = true;
// if the MQTT format is 'nested' or 'ha' then create the parent object hc<n>
if (mqtt_format_ != MQTT_format::SINGLE) {
// if (mqtt_format_ != MQTT_format::SINGLE) {
if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::HA)) {
char hc_name[10]; // hc{1-4}
strlcpy(hc_name, "hc", 10);
char s[3];
@@ -655,7 +732,8 @@ void Thermostat::publish_values() {
// if format is single, send immediately and clear object for next hc
// the topic will have the hc number appended
if (mqtt_format_ == MQTT_format::SINGLE) {
// if (mqtt_format_ == MQTT_format::SINGLE) {
if ((mqtt_format_ == MQTT_format::SINGLE) || (mqtt_format_ == MQTT_format::CUSTOM)) {
char topic[30];
char s[3];
strlcpy(topic, "thermostat_data", 30);
@@ -674,10 +752,9 @@ void Thermostat::publish_values() {
}
// if we're using nested json, send all in one go under one topic called thermostat_data
// if ((mqtt_format_ == MQTT_format::NESTED) || (mqtt_format_ == MQTT_format::CUSTOM)) {
if (mqtt_format_ == MQTT_format::NESTED) {
Mqtt::publish("thermostat_data", doc);
} else if (mqtt_format_ == MQTT_format::CUSTOM) {
Mqtt::publish("thermostat_data", doc);
}
}
@@ -987,7 +1064,7 @@ void Thermostat::show_values(uuid::console::Shell & shell) {
shell.printfln(F(" Display: time"));
} else if (ibaMainDisplay_ == 7) {
shell.printfln(F(" Display: date"));
} else if (ibaMainDisplay_ == 9) {
} else if (ibaMainDisplay_ == 8) {
shell.printfln(F(" Display: smoke temperature"));
}
}
@@ -1024,8 +1101,6 @@ void Thermostat::show_values(uuid::console::Shell & shell) {
}
}
// std::sort(heating_circuits_.begin(), heating_circuits_.end()); // sort based on hc number. This has moved to the heating_circuit() function
for (const auto & hc : heating_circuits_) {
if (!Helpers::hasValue(hc->setpoint_roomTemp)) {
break; // skip this HC
@@ -1168,7 +1243,7 @@ void Thermostat::process_EasyMonitor(std::shared_ptr<const Telegram> telegram) {
void Thermostat::process_IBASettings(std::shared_ptr<const Telegram> telegram) {
// 22 - display line on RC35
telegram->read_value(ibaMainDisplay_,
0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 9 smoke temp
0); // display on Thermostat: 0 int. temp, 1 int. setpoint, 2 ext. temp., 3 burner temp., 4 ww temp, 5 functioning mode, 6 time, 7 data, 8 smoke temp
telegram->read_value(ibaLanguage_, 1); // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian
telegram->read_value(ibaCalIntTemperature_, 2); // offset int. temperature sensor, by * 0.1 Kelvin
telegram->read_value(ibaBuildingType_, 6); // building type: 0 = light, 1 = medium, 2 = heavy
@@ -1310,11 +1385,16 @@ void Thermostat::process_RCTime(std::shared_ptr<const Telegram> telegram) {
if (flags() == EMS_DEVICE_FLAG_EASY) {
return; // not supported
}
if (telegram->message_length < 7) {
return;
}
if (telegram->message_data[7] & 0x0C) { // date and time not valid
set_datetime("NTP"); // set from NTP
return;
}
if (datetime_.empty()) {
datetime_.resize(25, '\0');
}
// render time to HH:MM:SS DD/MM/YYYY
// had to create separate buffers because of how printf works
char buf1[6];
@@ -1469,18 +1549,38 @@ void Thermostat::set_party(const uint8_t hrs, const uint8_t hc_num) {
}
}
// set date&time as string hh:mm:ss-dd.mm.yyyy-dw-dst
// set date&time as string hh:mm:ss-dd.mm.yyyy-dw-dst or "NTP" for setting to internet-time
// dw - day of week (0..6), dst- summertime (0/1)
void Thermostat::set_datetime(const char * dt) {
uint8_t data[9];
data[0] = (dt[16] - '0') * 100 + (dt[17] - '0') * 10 + (dt[18] - '0'); // year
data[1] = (dt[12] - '0') * 10 + (dt[13] - '0'); // month
data[2] = (dt[0] - '0') * 10 + (dt[1] - '0'); // hour
data[3] = (dt[9] - '0') * 10 + (dt[10] - '0'); // day
data[4] = (dt[3] - '0') * 10 + (dt[4] - '0'); // min
data[5] = (dt[6] - '0') * 10 + (dt[7] - '0'); // sec
data[6] = (dt[20] - '0'); // day of week
data[7] = (dt[22] - '0'); // summerime
if (strcmp(dt,"NTP") == 0) {
time_t now = time(nullptr);
tm * tm_ = localtime(&now);
if (tm_->tm_year < 110) { // no NTP time
LOG_WARNING(F("No NTP time. Cannot set RCtime"));
return;
}
data[0] = tm_->tm_year - 100; // Bosch counts from 2000
data[1] = tm_->tm_mon;
data[2] = tm_->tm_hour;
data[3] = tm_->tm_mday;
data[4] = tm_->tm_min;
data[5] = tm_->tm_sec;
data[6] = (tm_->tm_wday + 6) % 7; // Bosch counts from Mo, time from Su
data[7] = tm_->tm_isdst + 2; // set DST and flag for ext. clock
char time_string[25];
strftime(time_string, 25, "%FT%T%z", tm_);
LOG_INFO(F("Date and time: %s"), time_string);
} else {
data[0] = (dt[16] - '0') * 100 + (dt[17] - '0') * 10 + (dt[18] - '0'); // year
data[1] = (dt[12] - '0') * 10 + (dt[13] - '0'); // month
data[2] = (dt[0] - '0') * 10 + (dt[1] - '0'); // hour
data[3] = (dt[9] - '0') * 10 + (dt[10] - '0'); // day
data[4] = (dt[3] - '0') * 10 + (dt[4] - '0'); // min
data[5] = (dt[6] - '0') * 10 + (dt[7] - '0'); // sec
data[6] = (dt[20] - '0'); // day of week
data[7] = (dt[22] - '0') + 2; // DST and flag
}
if ((flags() & 0x0F) == EMS_DEVICE_FLAG_RC35 || (flags() & 0x0F) == EMS_DEVICE_FLAG_RC30_1) {
LOG_INFO(F("Setting date and time"));
write_command(6, 0, data, 8, 0);

View File

@@ -31,6 +31,7 @@
#include "mqtt.h"
#include <vector>
#include <time.h>
namespace emsesp {
@@ -94,6 +95,7 @@ class Thermostat : public EMSdevice {
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual void device_info(JsonArray & root);
virtual bool updated_values();
virtual void add_context_menu();

View File

@@ -127,7 +127,7 @@ uint8_t EMSdevice::decode_brand(uint8_t value) {
}
}
// print human friendly description of the EMS device
// returns string of a human friendly description of the EMS device
std::string EMSdevice::to_string() const {
std::string str(160, '\0');
@@ -153,6 +153,17 @@ std::string EMSdevice::to_string() const {
return str;
}
// returns out brand + device name
std::string EMSdevice::to_string_short() const {
std::string str(160, '\0');
if (brand_ == Brand::NO_BRAND) {
snprintf_P(&str[0], str.capacity() + 1, PSTR("%s: %s"), device_type_name().c_str(), name_.c_str());
} else {
snprintf_P(&str[0], str.capacity() + 1, PSTR("%s: %s %s"), device_type_name().c_str(), brand_to_string().c_str(), name_.c_str());
}
return str;
}
// prints the header for the section
void EMSdevice::show_values(uuid::console::Shell & shell) {
shell.printfln(F("%s: %s"), device_type_name().c_str(), to_string().c_str());
@@ -198,7 +209,7 @@ void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) {
Mqtt::show_topic_handlers(shell, this->device_id_);
}
void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_function_p f) {
void EMSdevice::register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f) {
LOG_DEBUG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_);
Mqtt::subscribe(this->device_id_, topic, f);
}

View File

@@ -102,12 +102,22 @@ class EMSdevice {
return name_;
}
inline uint8_t unique_id() const {
return unique_id_;
}
inline void unique_id(uint8_t unique_id) {
unique_id_ = unique_id;
}
std::string brand_to_string() const;
static uint8_t decode_brand(uint8_t value);
std::string to_string() const;
void show_telegram_handlers(uuid::console::Shell & shell);
void show_mqtt_handlers(uuid::console::Shell & shell);
std::string to_string_short() const;
void show_telegram_handlers(uuid::console::Shell & shell);
void show_mqtt_handlers(uuid::console::Shell & shell);
using process_function_p = std::function<void(std::shared_ptr<const Telegram>)>;
void register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, process_function_p cb);
@@ -119,13 +129,14 @@ class EMSdevice {
void read_command(const uint16_t type_id);
void register_mqtt_topic(const std::string & topic, mqtt_function_p f);
void register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f);
// virtual functions overrules by derived classes
virtual void show_values(uuid::console::Shell & shell) = 0;
virtual void publish_values() = 0;
virtual bool updated_values() = 0;
virtual void add_context_menu() = 0;
virtual void device_info(JsonArray & root) = 0;
std::string telegram_type_name(std::shared_ptr<const Telegram> telegram);
@@ -165,6 +176,37 @@ class EMSdevice {
}
}
// takes a value from an ems device and creates a nested json (name, value)
// which can be passed to the web UI
template <typename Value>
static void render_value_json(JsonArray & json,
const std::string & prefix,
const __FlashStringHelper * name,
Value & value,
const __FlashStringHelper * suffix,
const uint8_t format = 0) {
char buffer[15];
if (Helpers::render_value(buffer, value, format) == nullptr) {
return;
}
JsonObject dataElement = json.createNestedObject();
// copy flash into std::strings to ensure arduinojson can reference them without a copy
if (suffix != nullptr) {
std::string text(20, '\0');
snprintf_P(&text[0], text.capacity() + 1, PSTR("%s%s"), buffer, uuid::read_flash_string(suffix).c_str());
dataElement["value"] = text;
} else {
dataElement["value"] = buffer;
}
std::string text2(100, '\0');
snprintf_P(&text2[0], text2.capacity() + 1, PSTR("%s%s"), prefix.c_str(), uuid::read_flash_string(name).c_str());
dataElement["name"] = text2;
}
static void print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const __FlashStringHelper * value);
static void print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const char * value);
@@ -229,6 +271,7 @@ class EMSdevice {
static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS_2 = (1 << 6); // 6th bit set if older models
private:
uint8_t unique_id_;
uint8_t device_type_ = DeviceType::UNKNOWN;
uint8_t device_id_ = 0;
uint8_t product_id_ = 0;

View File

@@ -39,9 +39,8 @@ ESP8266React EMSESP::esp8266React(&webServer, &dummyFS);
EMSESPSettingsService EMSESP::emsespSettingsService = EMSESPSettingsService(&webServer, &dummyFS, EMSESP::esp8266React.getSecurityManager());
#endif
EMSESPStatusService EMSESP::emsespStatusService = EMSESPStatusService(&webServer, EMSESP::esp8266React.getSecurityManager());
EMSESPDevicesService EMSESP::emsespDevicesService = EMSESPDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager());
EMSESPScanDevicesService EMSESP::emsespScanDevicesService = EMSESPScanDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager());
EMSESPStatusService EMSESP::emsespStatusService = EMSESPStatusService(&webServer, EMSESP::esp8266React.getSecurityManager());
EMSESPDevicesService EMSESP::emsespDevicesService = EMSESPDevicesService(&webServer, EMSESP::esp8266React.getSecurityManager());
std::vector<std::unique_ptr<EMSdevice>> EMSESP::emsdevices; // array of all the detected EMS devices
std::vector<emsesp::EMSESP::Device_record> EMSESP::device_library_; // libary of all our known EMS devices so far
@@ -63,6 +62,7 @@ uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; /
uint8_t EMSESP::watch_ = 0; // trace off
bool EMSESP::tap_water_active_ = false; // for when Boiler states we having running warm water. used in Shower()
uint32_t EMSESP::last_fetch_ = 0;
uint8_t EMSESP::unique_id_count_ = 0;
// for a specific EMS device go and request data values
// or if device_id is 0 it will fetch from all our known and active devices
@@ -110,13 +110,17 @@ void EMSESP::watch_id(uint16_t watch_id) {
// change the tx_mode
// resets all counters and bumps the UART
void EMSESP::reset_tx(uint8_t const tx_mode) {
txservice_.telegram_read_count(0);
txservice_.telegram_write_count(0);
txservice_.telegram_fail_count(0);
if (tx_mode) {
EMSuart::stop();
EMSuart::start(tx_mode);
void EMSESP::reset_tx() {
// get the tx_mode
uint8_t tx_mode;
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode = settings.tx_mode; });
EMSuart::stop();
EMSuart::start(tx_mode);
txservice_.start();
// force a fetch for all new values, unless Tx is set to off
if (tx_mode != 0) {
EMSESP::fetch_device_values();
}
}
@@ -151,7 +155,22 @@ uint8_t EMSESP::bus_status() {
// show the EMS bus status plus both Rx and Tx queues
void EMSESP::show_ems(uuid::console::Shell & shell) {
// EMS bus information
if (rxservice_.bus_connected()) {
switch (bus_status()) {
case BUS_STATUS_OFFLINE:
shell.printfln(F("EMS Bus is disconnected."));
break;
case BUS_STATUS_TX_ERRORS:
shell.printfln(F("EMS Bus is connected, but Tx is not stable."));
break;
case BUS_STATUS_CONNECTED:
default:
shell.printfln(F("EMS Bus is connected."));
break;
}
shell.println();
if (bus_status() != BUS_STATUS_OFFLINE) {
uint8_t success_rate = 0;
if (rxservice_.telegram_error_count()) {
success_rate = ((float)rxservice_.telegram_error_count() / (float)rxservice_.telegram_count()) * 100;
@@ -165,8 +184,6 @@ void EMSESP::show_ems(uuid::console::Shell & shell) {
shell.printfln(F(" #write requests sent: %d"), txservice_.telegram_write_count());
shell.printfln(F(" #corrupted telegrams: %d (%d%%)"), rxservice_.telegram_error_count(), success_rate);
shell.printfln(F(" #tx fails (after %d retries): %d"), TxService::MAXIMUM_TX_RETRIES, txservice_.telegram_fail_count());
} else {
shell.printfln(F("EMS Bus is disconnected."));
}
shell.println();
@@ -231,7 +248,7 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
}
}
// show Dallas sensors
// show Dallas temperature sensors
void EMSESP::show_sensor_values(uuid::console::Shell & shell) {
if (sensor_devices().empty()) {
return;
@@ -240,7 +257,7 @@ void EMSESP::show_sensor_values(uuid::console::Shell & shell) {
char valuestr[8] = {0}; // for formatting temp
shell.printfln(F("External temperature sensors:"));
for (const auto & device : sensor_devices()) {
shell.printfln(F(" Sensor ID %s: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c_, 2));
shell.printfln(F(" ID: %s, Temperature: %s°C"), device.to_string().c_str(), Helpers::render_value(valuestr, device.temperature_c, 2));
}
shell.println();
}
@@ -474,6 +491,20 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
return found;
}
// calls the device handler's function to populate a json doc with device info
void EMSESP::device_info(const uint8_t unique_id, JsonObject & root) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->unique_id() == unique_id) {
root["deviceName"] = emsdevice->to_string_short(); // can;t use c_str() because of scope
JsonArray data = root.createNestedArray("deviceData");
emsdevice->device_info(data);
return;
}
}
}
}
// return true if we have this device already registered
bool EMSESP::device_exists(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
@@ -513,6 +544,9 @@ void EMSESP::show_devices(uuid::console::Shell & shell) {
// shell.printf(F("[factory ID: %d] "), device_class.first);
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == device_class.first)) {
#if defined(EMSESP_DEBUG)
shell.printf(F("[id=%d] "), emsdevice->unique_id());
#endif
shell.printf(F("%s: %s"), emsdevice->device_type_name().c_str(), emsdevice->to_string().c_str());
if ((emsdevice->device_type() == EMSdevice::DeviceType::THERMOSTAT) && (emsdevice->device_id() == actual_master_thermostat())) {
shell.printf(F(" ** master device **"));
@@ -584,6 +618,7 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
} else {
emsdevices.push_back(
EMSFactory::add(device_p->device_type, device_id, device_p->product_id, version, uuid::read_flash_string(device_p->name), device_p->flags, brand));
emsdevices.back()->unique_id(++unique_id_count_);
LOG_DEBUG(F("Adding new device with device ID 0x%02X with product ID %d and version %s"), device_id, product_id, version.c_str());
// go and fetch its data,
fetch_device_values(device_id);
@@ -629,14 +664,14 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
}
// are we waiting for a response from a recent Tx Read or Write?
uint8_t op = EMSbus::tx_waiting();
if (op != Telegram::Operation::NONE) {
uint8_t tx_state = EMSbus::tx_state();
if (tx_state != Telegram::Operation::NONE) {
bool tx_successful = false;
EMSbus::tx_waiting(Telegram::Operation::NONE); // reset Tx wait state
EMSbus::tx_state(Telegram::Operation::NONE); // reset Tx wait state
// txservice_.print_last_tx();
// if we're waiting on a Write operation, we want a single byte 1 or 4
if ((op == Telegram::Operation::TX_WRITE) && (length == 1)) {
if ((tx_state == Telegram::Operation::TX_WRITE) && (length == 1)) {
if (first_value == TxService::TX_WRITE_SUCCESS) {
LOG_DEBUG(F("Last Tx write successful"));
txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok
@@ -649,7 +684,7 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
txservice_.send_poll(); // close the bus
txservice_.reset_retry_count();
}
} else if (op == Telegram::Operation::TX_READ) {
} else if (tx_state == Telegram::Operation::TX_READ) {
// got a telegram with data in it. See if the src/dest matches that from the last one we sent and continue to process it
uint8_t src = data[0];
uint8_t dest = data[1];
@@ -664,13 +699,15 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// if Tx wasn't successful, retry or just give up
if (!tx_successful) {
txservice_.retry_tx(op, data, length);
txservice_.retry_tx(tx_state, data, length);
return;
}
}
// check for poll
if (length == 1) {
EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active
#ifdef EMSESP_DEBUG
char s[4];
if (first_value & 0x80) {
@@ -684,7 +721,6 @@ void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
// check for poll to us, if so send top message from Tx queue immediately and quit
// if ht3 poll must be ems_bus_id else if Buderus poll must be (ems_bus_id | 0x80)
if ((first_value ^ 0x80 ^ rxservice_.ems_mask()) == txservice_.ems_bus_id()) {
EMSbus::last_bus_activity(uuid::get_uptime()); // set the flag indication the EMS bus is active
txservice_.send();
}
// send remote room temperature if active
@@ -741,15 +777,17 @@ void EMSESP::loop() {
// if we're doing an OTA upload, skip MQTT and EMS
if (system_.upload_status()) {
#if defined(ESP32)
delay(10); // slow down OTA update to avoid getting killed by task watchdog (task_wdt)
#endif
return;
}
system_.loop(); // does LED and checks system health, and syslog service
mqtt_.loop(); // starts mqtt, and sends out anything in the queue
rxservice_.loop(); // process what ever is in the rx queue
txservice_.loop(); // check that the Tx is all ok
shower_.loop(); // check for shower on/off
sensors_.loop(); // this will also send out via MQTT
mqtt_.loop(); // sends out anything in the queue via MQTT
console_.loop(); // telnet/serial console
// force a query on the EMS devices to fetch latest data at a set interval (1 min)

View File

@@ -37,7 +37,6 @@
#include "EMSESPStatusService.h"
#include "EMSESPDevicesService.h"
#include "EMSESPSettingsService.h"
#include "EMSESPScanDevicesService.h"
#include "emsdevice.h"
#include "emsfactory.h"
@@ -82,6 +81,8 @@ class EMSESP {
static void send_raw_telegram(const char * data);
static bool device_exists(const uint8_t device_id);
static void device_info(const uint8_t unique_id, JsonObject & root);
static uint8_t count_devices(const uint8_t device_type);
static uint8_t actual_master_thermostat();
@@ -95,7 +96,7 @@ class EMSESP {
static void add_context_menus();
static void reset_tx(uint8_t const tx_mode);
static void reset_tx();
static void incoming_telegram(uint8_t * data, const uint8_t length);
@@ -146,11 +147,10 @@ class EMSESP {
static TxService txservice_;
// web controllers
static ESP8266React esp8266React;
static EMSESPSettingsService emsespSettingsService;
static EMSESPStatusService emsespStatusService;
static EMSESPDevicesService emsespDevicesService;
static EMSESPScanDevicesService emsespScanDevicesService;
static ESP8266React esp8266React;
static EMSESPSettingsService emsespSettingsService;
static EMSESPStatusService emsespStatusService;
static EMSESPDevicesService emsespDevicesService;
static uuid::log::Logger logger() {
return logger_;
@@ -182,6 +182,8 @@ class EMSESP {
static uint16_t watch_id_;
static uint8_t watch_;
static bool tap_water_active_;
static uint8_t unique_id_count_;
};
} // namespace emsesp

View File

@@ -89,6 +89,7 @@ class ConcreteEMSFactory : EMSFactory {
ConcreteEMSFactory(const uint8_t device_type) {
EMSFactory::registerFactory(device_type, this);
}
auto construct(uint8_t device_type, uint8_t device_id, uint8_t product_id, std::string version, std::string name, uint8_t flags, uint8_t brand) const
-> EMSdevice * {
return new DerivedClass(device_type, device_id, product_id, version, name, flags, brand);

View File

@@ -35,7 +35,7 @@ std::string Mqtt::hostname_;
uint8_t Mqtt::mqtt_qos_;
uint16_t Mqtt::publish_time_;
std::vector<Mqtt::MQTTFunction> Mqtt::mqtt_functions_;
std::vector<Mqtt::MQTTSubFunction> Mqtt::mqtt_subfunctions_;
uint16_t Mqtt::mqtt_publish_fails_ = 0;
size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES;
uint16_t Mqtt::mqtt_message_id_ = 0;
@@ -51,62 +51,56 @@ Mqtt::QueuedMqttMessage::QueuedMqttMessage(uint16_t id, std::shared_ptr<MqttMess
packet_id_ = 0;
}
MqttMessage::MqttMessage(uint8_t operation, const std::string & topic, const std::string & payload, bool retain)
MqttMessage::MqttMessage(const uint8_t operation, const std::string & topic, const std::string & payload, bool retain)
: operation(operation)
, topic(topic)
, payload(payload)
, retain(retain) {
}
Mqtt::MQTTFunction::MQTTFunction(uint8_t device_id, const std::string && topic, mqtt_function_p mqtt_function)
Mqtt::MQTTSubFunction::MQTTSubFunction(const uint8_t device_id, const std::string && topic, mqtt_subfunction_p mqtt_subfunction)
: device_id_(device_id)
, topic_(topic)
, mqtt_function_(mqtt_function) {
, mqtt_subfunction_(mqtt_subfunction) {
}
// subscribe to an MQTT topic, and store the associated callback function
void Mqtt::subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb) {
/*
// We don't want to store the whole topic string in our lookup, just the last cmd, as this can take up too much memory
// strip out everything until the last /
size_t topic_pos = topic.find_last_of("/"); // returns npos which is -1
topic_pos += 1; // skip the /
*/
void Mqtt::subscribe(const uint8_t device_id, const std::string & topic, mqtt_subfunction_p cb) {
auto message = queue_subscribe_message(topic); // add subscription to queue. The hostname will automatically be appended
// convert the topic to it's full path, so either prefixed with the hostname unless hardcoded like 'homeassistant'
char full_topic[MQTT_TOPIC_MAX_SIZE];
make_topic(full_topic, topic);
if (message == nullptr) {
return;
}
// the message will contain the full topic, with the hostname prefixed
// check if we already have the topic subscribed, if so don't add it again
bool exists = false;
if (!mqtt_functions_.empty()) {
for (const auto & mqtt_function : mqtt_functions_) {
if ((mqtt_function.device_id_ == device_id) && (strcmp(mqtt_function.topic_.c_str(), full_topic) == 0)) {
if (!mqtt_subfunctions_.empty()) {
for (const auto & mqtt_subfunction : mqtt_subfunctions_) {
if ((mqtt_subfunction.device_id_ == device_id) && (strcmp(mqtt_subfunction.topic_.c_str(), message->topic.c_str()) == 0)) {
exists = true;
}
}
}
if (!exists) {
mqtt_functions_.emplace_back(device_id, std::move(full_topic), cb); // register a call back function for a specific telegram type
mqtt_subfunctions_.emplace_back(device_id, std::move(message->topic), cb); // register a call back function for a specific telegram type
}
queue_subscribe_message(topic); // add subscription to queue
}
// subscribe to an MQTT topic, and store the associated callback function. For generic functions not tied to a specific device
void Mqtt::subscribe(const std::string & topic, mqtt_function_p cb) {
void Mqtt::subscribe(const std::string & topic, mqtt_subfunction_p cb) {
subscribe(0, topic, cb); // no device_id needed, if generic to EMS-ESP
}
// resubscribe to all MQTT topics again
void Mqtt::resubscribe() {
if (mqtt_functions_.empty()) {
if (mqtt_subfunctions_.empty()) {
return;
}
for (const auto & mqtt_function : mqtt_functions_) {
queue_subscribe_message(mqtt_function.topic_);
for (const auto & mqtt_subfunction : mqtt_subfunctions_) {
queue_message(Operation::SUBSCRIBE, mqtt_subfunction.topic_, "", false, true); // no payload, no topic prefixing
}
}
@@ -120,6 +114,7 @@ void Mqtt::loop() {
}
uint32_t currentMillis = uuid::get_uptime();
// create publish messages for each of the EMS device values, adding to queue
if (publish_time_ && (currentMillis - last_publish_ > publish_time_)) {
last_publish_ = currentMillis;
@@ -146,8 +141,8 @@ void Mqtt::show_mqtt(uuid::console::Shell & shell) {
// show subscriptions
shell.printfln(F("MQTT subscriptions:"));
for (const auto & mqtt_function : mqtt_functions_) {
shell.printfln(F(" %s"), mqtt_function.topic_.c_str());
for (const auto & mqtt_subfunction : mqtt_subfunctions_) {
shell.printfln(F(" %s"), mqtt_subfunction.topic_.c_str());
}
shell.println();
@@ -206,19 +201,11 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
strlcpy(message, payload, len + 1);
LOG_DEBUG(F("[DEBUG] Received %s => %s (length %d)"), topic, message, len);
/*
// strip out everything until the last /
char * topic_magnitude = strrchr(topic, '/');
if (topic_magnitude != nullptr) {
topic = topic_magnitude + 1;
}
*/
// see if we have this topic in our subscription list, then call its callback handler
// note: this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail. Not sure if this is going to be an issue?
for (const auto & mf : mqtt_functions_) {
for (const auto & mf : mqtt_subfunctions_) {
if (strcmp(topic, mf.topic_.c_str()) == 0) {
(mf.mqtt_function_)(message);
(mf.mqtt_subfunction_)(message);
return;
}
}
@@ -229,15 +216,17 @@ void Mqtt::on_message(char * topic, char * payload, size_t len) {
// print all the topics related to a specific device_id
void Mqtt::show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_id) {
if (std::count_if(mqtt_functions_.cbegin(), mqtt_functions_.cend(), [=](MQTTFunction const & mqtt_function) { return device_id == mqtt_function.device_id_; })
if (std::count_if(mqtt_subfunctions_.cbegin(),
mqtt_subfunctions_.cend(),
[=](MQTTSubFunction const & mqtt_subfunction) { return device_id == mqtt_subfunction.device_id_; })
== 0) {
return;
}
shell.print(F(" Subscribed MQTT topics: "));
for (const auto & mqtt_function : mqtt_functions_) {
if (mqtt_function.device_id_ == device_id) {
shell.printf(F("%s "), mqtt_function.topic_.c_str());
for (const auto & mqtt_subfunction : mqtt_subfunctions_) {
if (mqtt_subfunction.device_id_ == device_id) {
shell.printf(F("%s "), mqtt_subfunction.topic_.c_str());
}
}
shell.println();
@@ -268,24 +257,7 @@ void Mqtt::on_publish(uint16_t packetId) {
mqtt_messages_.pop_front(); // always remove from queue, regardless if there was a successful ACK
}
// builds up a topic by prefixing the hostname
// unless it's hardcoded like "homeassistant"
char * Mqtt::make_topic(char * result, const std::string & topic) {
// check for homesassistant
if (strncmp(topic.c_str(), "homeassistant/", 13) == 0) {
strlcpy(result, topic.c_str(), MQTT_TOPIC_MAX_SIZE);
return result;
}
strlcpy(result, hostname_.c_str(), MQTT_TOPIC_MAX_SIZE);
strlcat(result, "/", MQTT_TOPIC_MAX_SIZE);
strlcat(result, topic.c_str(), MQTT_TOPIC_MAX_SIZE);
return result;
}
void Mqtt::start() {
mqttClient_ = EMSESP::esp8266React.getMqttClient();
// get the hostname, which we'll use to prefix to all topics
@@ -298,15 +270,36 @@ void Mqtt::start() {
});
mqttClient_->onConnect([this](bool sessionPresent) { on_connect(); });
mqttClient_->setWill(make_topic(will_topic_, "status"), 1, true, "offline"); // with qos 1, retain true
mqttClient_->onDisconnect([this](AsyncMqttClientDisconnectReason reason) {
if (reason == AsyncMqttClientDisconnectReason::TCP_DISCONNECTED) {
LOG_INFO(F("MQTT disconnected: TCP"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_IDENTIFIER_REJECTED) {
LOG_INFO(F("MQTT disconnected: Identifier Rejected"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_SERVER_UNAVAILABLE) {
LOG_INFO(F("MQTT disconnected: Server unavailable"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_MALFORMED_CREDENTIALS) {
LOG_INFO(F("MQTT disconnected: Malformed credentials"));
}
if (reason == AsyncMqttClientDisconnectReason::MQTT_NOT_AUTHORIZED) {
LOG_INFO(F("MQTT disconnected: Not authorized"));
}
});
// create will_topic with the hostname prefixed. It has to be static because asyncmqttclient destroys the reference
static char will_topic[MQTT_TOPIC_MAX_SIZE];
strlcpy(will_topic, hostname_.c_str(), MQTT_TOPIC_MAX_SIZE);
strlcat(will_topic, "/", MQTT_TOPIC_MAX_SIZE);
strlcat(will_topic, "status", MQTT_TOPIC_MAX_SIZE);
mqttClient_->setWill(will_topic, 1, true, "offline"); // with qos 1, retain true
mqttClient_->onMessage([this](char * topic, char * payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) {
on_message(topic, payload, len);
mqttClient_->onPublish([this](uint16_t packetId) { on_publish(packetId); });
});
// add the system MQTT subscriptions
Mqtt::subscribe("cmd", System::mqtt_commands);
// Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, std::placeholders::_1));
}
void Mqtt::set_publish_time(uint16_t publish_time) {
@@ -334,47 +327,50 @@ void Mqtt::on_connect() {
resubscribe(); // in case this is a reconnect, re-subscribe again to all MQTT topics
// add the system MQTT subscriptions, only if its a fresh start with no previous subscriptions
if (mqtt_subfunctions_.empty()) {
Mqtt::subscribe("cmd", System::mqtt_commands);
}
LOG_INFO(F("MQTT connected"));
}
// add MQTT message to queue, payload is a string
void Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) {
// can't have bogus topics, but empty payloads are ok
// add sub or pub task to the queue. When the message is created, the topic will have
// automatically the hostname prefixed.
std::shared_ptr<const MqttMessage>
Mqtt::queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain, bool no_prefix) {
if (topic.empty()) {
return;
return nullptr;
}
// prefix the hostname to the topic
char full_topic[MQTT_TOPIC_MAX_SIZE];
make_topic(full_topic, topic);
auto message = std::make_shared<MqttMessage>(Operation::PUBLISH, full_topic, payload, retain);
// take the topic and prefix the hostname, unless its for HA
std::shared_ptr<MqttMessage> message;
if ((strncmp(topic.c_str(), "homeassistant/", 13) == 0) || no_prefix) {
// leave topic as it is
message = std::make_shared<MqttMessage>(operation, topic, payload, retain);
} else {
// prefix the hostname
std::string full_topic = Mqtt::hostname_ + "/" + topic;
message = std::make_shared<MqttMessage>(operation, full_topic, payload, retain);
}
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
mqtt_messages_.pop_front();
}
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
return mqtt_messages_.back().content_; // this is because the message has been moved
}
// add MQTT message to queue, payload is a string
std::shared_ptr<const MqttMessage> Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, const bool retain) {
return queue_message(Operation::PUBLISH, topic, payload, retain);
}
// add MQTT subscribe message to queue
void Mqtt::queue_subscribe_message(const std::string & topic) {
if (topic.empty()) {
return;
}
auto message = std::make_shared<MqttMessage>(Operation::SUBSCRIBE, topic, "", false);
#ifdef DEBUG
LOG_DEBUG(F("Adding a subscription for %s"), topic.c_str());
#endif
// if the queue is full, make room but removing the last one
if (mqtt_messages_.size() >= maximum_mqtt_messages_) {
mqtt_messages_.pop_front();
}
mqtt_messages_.emplace_back(mqtt_message_id_++, std::move(message));
std::shared_ptr<const MqttMessage> Mqtt::queue_subscribe_message(const std::string & topic) {
return queue_message(Operation::SUBSCRIBE, topic, "", false); // no payload
}
// MQTT Publish, using a specific retain flag
@@ -383,9 +379,8 @@ void Mqtt::publish(const std::string & topic, const std::string & payload, bool
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) {
// convert json to string
std::string payload_text;
serializeJson(payload, payload_text);
serializeJson(payload, payload_text); // convert json to string
queue_publish_message(topic, payload_text, retain);
}
@@ -413,6 +408,26 @@ void Mqtt::process_queue() {
return;
}
// show queue - Debug only
/*
Serial.printf("MQTT queue:\n\r");
for (const auto & message : mqtt_messages_) {
auto content = message.content_;
if (content->operation == Operation::PUBLISH) {
// Publish messages
Serial.printf(" [%02d] (Pub) topic=%s payload=%s (pid %d, retry #%d)\n\r",
message.id_,
content->topic.c_str(),
content->payload.c_str(),
message.packet_id_,
message.retry_count_);
} else {
// Subscribe messages
Serial.printf(" [%02d] (Sub) topic=%s\n\r", message.id_, content->topic.c_str());
}
}
*/
// fetch first from queue and create the full topic name
auto mqtt_message = mqtt_messages_.front();
auto message = mqtt_message.content_;

View File

@@ -43,11 +43,11 @@ using uuid::console::Shell;
namespace emsesp {
using mqtt_function_p = std::function<void(const char * message)>;
using mqtt_subfunction_p = std::function<void(const char * message)>;
using namespace std::placeholders; // for `_1`
struct MqttMessage {
MqttMessage(uint8_t operation, const std::string & topic, const std::string & payload, bool retain);
MqttMessage(const uint8_t operation, const std::string & topic, const std::string & payload, bool retain);
~MqttMessage() = default;
const uint8_t operation;
@@ -68,8 +68,8 @@ class Mqtt {
static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 100;
static void subscribe(const uint8_t device_id, const std::string & topic, mqtt_function_p cb);
static void subscribe(const std::string & topic, mqtt_function_p cb);
static void subscribe(const uint8_t device_id, const std::string & topic, mqtt_subfunction_p cb);
static void subscribe(const std::string & topic, mqtt_subfunction_p cb);
static void resubscribe();
static void publish(const std::string & topic, const std::string & payload, bool retain = false);
@@ -100,6 +100,8 @@ class Mqtt {
mqtt_publish_fails_ = 0;
}
static std::string hostname_;
private:
static uuid::log::Logger logger_;
@@ -125,36 +127,36 @@ class Mqtt {
static constexpr uint32_t MQTT_PUBLISH_WAIT = 200; // delay between sending publishes, to account for large payloads
static constexpr uint8_t MQTT_PUBLISH_MAX_RETRY = 3; // max retries for giving up on publishing
static void queue_publish_message(const std::string & topic, const std::string & payload, const bool retain);
static void queue_subscribe_message(const std::string & topic);
static std::shared_ptr<const MqttMessage> queue_message(const uint8_t operation, const std::string & topic, const std::string & payload, const bool retain, bool no_prefix = false);
static std::shared_ptr<const MqttMessage> queue_publish_message(const std::string & topic, const std::string & payload, const bool retain);
static std::shared_ptr<const MqttMessage> queue_subscribe_message(const std::string & topic);
void on_publish(uint16_t packetId);
void on_message(char * topic, char * payload, size_t len);
static char * make_topic(char * result, const std::string & topic);
void process_queue();
void process_all_queue();
static uint16_t mqtt_publish_fails_;
class MQTTFunction {
// function handlers for MQTT subscriptions
class MQTTSubFunction {
public:
MQTTFunction(uint8_t device_id, const std::string && topic, mqtt_function_p mqtt_function);
~MQTTFunction() = default;
MQTTSubFunction(const uint8_t device_id, const std::string && topic, mqtt_subfunction_p mqtt_subfunction);
~MQTTSubFunction() = default;
uint8_t device_id_; // which device ID owns this
std::string topic_;
mqtt_function_p mqtt_function_;
const uint8_t device_id_; // which device ID owns this
const std::string topic_;
mqtt_subfunction_p mqtt_subfunction_;
};
static std::vector<MQTTFunction> mqtt_functions_; // list of mqtt subscribe callbacks for all devices
static std::vector<MQTTSubFunction> mqtt_subfunctions_; // list of mqtt subscribe callbacks for all devices
uint32_t last_mqtt_poll_ = 0;
uint32_t last_publish_ = 0;
// settings, copied over
static std::string hostname_;
static uint8_t mqtt_qos_;
static uint16_t publish_time_;
static uint8_t mqtt_qos_;
static uint16_t publish_time_;
};
} // namespace emsesp

View File

@@ -65,6 +65,10 @@ void Roomctrl::check(const uint8_t addr, const uint8_t * data) {
if (hc_ > 3) {
return;
}
// no reply if the temperature is not set
if (remotetemp[hc_] == EMS_VALUE_SHORT_NOTSET) {
return;
}
// reply to writes with write nack byte
if (addr & 0x80) { // it's a write to us
nack_write(); // we don't accept writes.

View File

@@ -23,6 +23,12 @@
MAKE_PSTR(logger_name, "sensors")
#ifdef ESP32
#define YIELD
#else
#define YIELD yield()
#endif
namespace emsesp {
uuid::log::Logger Sensors::logger_{F_(logger_name), uuid::log::Facility::DAEMON};
@@ -63,10 +69,9 @@ void Sensors::loop() {
if (time_now - last_activity_ >= READ_INTERVAL_MS) {
// LOG_DEBUG(F("Read sensor temperature")); // uncomment for debug
if (bus_.reset()) {
yield();
YIELD;
bus_.skip();
bus_.write(CMD_CONVERT_TEMP);
state_ = State::READING;
} else {
// no sensors found
@@ -80,20 +85,15 @@ void Sensors::loop() {
// LOG_DEBUG(F("Scanning for sensors")); // uncomment for debug
bus_.reset_search();
found_.clear();
state_ = State::SCANNING;
last_activity_ = time_now;
state_ = State::SCANNING;
} else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
LOG_ERROR(F("Sensor read timeout"));
state_ = State::IDLE;
last_activity_ = time_now;
state_ = State::IDLE;
}
} else if (state_ == State::SCANNING) {
if (time_now - last_activity_ > SCAN_TIMEOUT_MS) {
LOG_ERROR(F("Sensor scan timeout"));
state_ = State::IDLE;
last_activity_ = time_now;
state_ = State::IDLE;
} else {
uint8_t addr[ADDR_LEN] = {0};
@@ -107,7 +107,7 @@ void Sensors::loop() {
case TYPE_DS1822:
case TYPE_DS1825:
found_.emplace_back(addr);
found_.back().temperature_c_ = get_temperature_c(addr);
found_.back().temperature_c = get_temperature_c(addr);
/*
// comment out for debugging
@@ -127,11 +127,15 @@ void Sensors::loop() {
}
} else {
bus_.depower();
devices_ = std::move(found_);
if ((found_.size() >= devices_.size()) || (retrycnt_ > 5)) {
devices_ = std::move(found_);
retrycnt_ = 0;
} else {
retrycnt_++;
}
found_.clear();
// LOG_DEBUG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
state_ = State::IDLE;
last_activity_ = time_now;
state_ = State::IDLE;
}
}
}
@@ -155,17 +159,17 @@ float Sensors::get_temperature_c(const uint8_t addr[]) {
LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
yield();
YIELD;
uint8_t scratchpad[SCRATCHPAD_LEN] = {0};
bus_.select(addr);
bus_.write(CMD_READ_SCRATCHPAD);
bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
yield();
YIELD;
if (!bus_.reset()) {
LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
yield();
YIELD;
if (bus_.crc8(scratchpad, SCRATCHPAD_LEN - 1) != scratchpad[SCRATCHPAD_LEN - 1]) {
LOG_WARNING(F("Invalid scratchpad CRC: %02X%02X%02X%02X%02X%02X%02X%02X%02X from device %s"),
scratchpad[0],
@@ -202,7 +206,8 @@ float Sensors::get_temperature_c(const uint8_t addr[]) {
break;
}
return (float)raw_value / 16;
uint32_t raw = (raw_value *625) / 100; // round to 0.01
return (float)raw / 100;
#else
return NAN;
#endif
@@ -253,7 +258,7 @@ void Sensors::publish_values() {
StaticJsonDocument<100> doc;
for (const auto & device : devices_) {
char s[5];
doc["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
doc["temp"] = Helpers::render_value(s, device.temperature_c, 2);
char topic[60]; // sensors{1-n}
strlcpy(topic, "sensor_", 50); // create topic, e.g. home/ems-esp/sensor_28-EA41-9497-0E03-5F
strlcat(topic, device.to_string().c_str(), 60);
@@ -279,7 +284,7 @@ void Sensors::publish_values() {
for (const auto & device : devices_) {
if (mqtt_format_ == MQTT_format::CUSTOM) {
char s[5];
doc[device.to_string()] = Helpers::render_value(s, device.temperature_c_, 2);
doc[device.to_string()] = Helpers::render_value(s, device.temperature_c, 2);
} else {
char sensorID[10]; // sensor{1-n}
strlcpy(sensorID, "sensor", 10);
@@ -287,7 +292,7 @@ void Sensors::publish_values() {
strlcat(sensorID, Helpers::itoa(s, i++), 10);
JsonObject dataSensor = doc.createNestedObject(sensorID);
dataSensor["id"] = device.to_string();
dataSensor["temp"] = Helpers::render_value(s, device.temperature_c_, 2);
dataSensor["temp"] = Helpers::render_value(s, device.temperature_c, 2);
}
}

View File

@@ -46,7 +46,7 @@ class Sensors {
uint64_t id() const;
std::string to_string() const;
float temperature_c_ = NAN;
float temperature_c = NAN;
private:
const uint64_t id_;
@@ -90,7 +90,7 @@ class Sensors {
static constexpr uint32_t READ_INTERVAL_MS = 5000; // 5 seconds
static constexpr uint32_t CONVERSION_MS = 1000; // 1 seconds
static constexpr uint32_t READ_TIMEOUT_MS = 2000; // 2 seconds
static constexpr uint32_t SCAN_TIMEOUT_MS = 30000; // 30 seconds
static constexpr uint32_t SCAN_TIMEOUT_MS = 3000; // 3 seconds
static constexpr uint8_t CMD_CONVERT_TEMP = 0x44;
static constexpr uint8_t CMD_READ_SCRATCHPAD = 0xBE;
@@ -111,6 +111,8 @@ class Sensors {
std::vector<Device> devices_;
uint8_t mqtt_format_;
uint8_t retrycnt_ = 0;
};
} // namespace emsesp

View File

@@ -24,8 +24,10 @@
MAKE_PSTR_WORD(passwd)
MAKE_PSTR_WORD(hostname)
MAKE_PSTR_WORD(wifi)
MAKE_PSTR_WORD(reconnect)
MAKE_PSTR_WORD(ssid)
MAKE_PSTR_WORD(heartbeat)
MAKE_PSTR_WORD(users)
MAKE_PSTR(host_fmt, "Host = %s")
MAKE_PSTR(hostname_fmt, "WiFi Hostname = %s")
@@ -141,7 +143,6 @@ void System::mqtt_commands(const char * message) {
// restart EMS-ESP
void System::restart() {
LOG_NOTICE("Restarting system...");
Shell::loop_all();
delay(1000); // wait a second
#if defined(ESP8266)
@@ -151,10 +152,19 @@ void System::restart() {
#endif
}
// saves all settings
void System::wifi_reconnect() {
LOG_NOTICE("The wifi will reconnect...");
Shell::loop_all();
delay(1000); // wait a second
EMSESP::emsespSettingsService.save(); // local settings
EMSESP::esp8266React.getWiFiSettingsService()->callUpdateHandlers("local"); // in case we've changed ssid or password
}
// format fs
// format the FS. Wipes everything.
void System::format(uuid::console::Shell & shell) {
auto msg = F("Formatting file system. This will also reset all settings to their defaults");
auto msg = F("Formatting file system. This will reset all settings to their defaults");
shell.logger().warning(msg);
shell.flush();
@@ -189,19 +199,17 @@ void System::syslog_init() {
});
#ifndef EMSESP_STANDALONE
syslog_.start(); // syslog service
syslog_.start(); // syslog service re-start
// configure syslog
IPAddress addr;
if (!addr.fromString(syslog_host_.c_str())) {
addr = (uint32_t)0;
}
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); });
syslog_.log_level((uuid::log::Level)syslog_level_);
syslog_.mark_interval(syslog_mark_interval_);
syslog_.destination(addr);
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { syslog_.hostname(wifiSettings.hostname.c_str()); });
#endif
}
@@ -220,32 +228,32 @@ void System::start() {
#endif
}
// fetch settings
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; });
// fetch system heartbeat
EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & settings) { system_heartbeat_ = settings.system_heartbeat; });
syslog_init(); // init SysLog
// print boot message
EMSESP::esp8266React.getWiFiSettingsService()->read(
[&](WiFiSettings & wifiSettings) { LOG_INFO(F("System %s booted (EMS-ESP version %s)"), wifiSettings.hostname.c_str(), EMSESP_APP_VERSION); });
#if defined(ESP32)
LOG_INFO(F("System booted (EMS-ESP version %s ESP32)"), EMSESP_APP_VERSION);
#else
LOG_INFO(F("System booted (EMS-ESP version %s)"), EMSESP_APP_VERSION);
#endif
syslog_init(); // init SysLog
if (LED_GPIO) {
pinMode(LED_GPIO, OUTPUT); // LED pin, 0 means disabled
}
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { tx_mode_ = settings.tx_mode; });
#ifndef EMSESP_FORCE_SERIAL
if (tx_mode_) {
EMSuart::start(tx_mode_); // start UART, if tx_mode is not 0
}
EMSuart::start(tx_mode_); // start UART
#endif
}
// returns true if OTA is uploading
bool System::upload_status() {
#if defined(EMSESP_STANDALONE)
return false;
#else
return upload_status_ || Update.isRunning();
#endif
}
void System::upload_status(bool in_progress) {
@@ -372,10 +380,23 @@ int8_t System::wifi_quality() {
return 2 * (dBm + 100);
}
void System::show_system(uuid::console::Shell & shell) {
shell.print(F("Uptime: "));
shell.print(uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3));
// print users to console
void System::show_users(uuid::console::Shell & shell) {
shell.printfln(F("Users:"));
#ifndef EMSESP_STANDALONE
EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) {
for (User user : securitySettings.users) {
shell.printfln(F(" username: %s, password: %s, is_admin: %s"), user.username.c_str(), user.password.c_str(), user.admin ? F("yes") : F("no"));
}
});
#endif
shell.println();
}
void System::show_system(uuid::console::Shell & shell) {
shell.printfln(F("Uptime: %s"), uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3).c_str());
#if defined(ESP8266)
shell.printfln(F("Chip ID: 0x%08x"), ESP.getChipId());
@@ -389,7 +410,7 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(F("Sketch size: %u bytes (%u bytes free)"), ESP.getSketchSize(), ESP.getFreeSketchSpace());
shell.printfln(F("Reset reason: %s"), ESP.getResetReason().c_str());
shell.printfln(F("Reset info: %s"), ESP.getResetInfo().c_str());
shell.println();
shell.printfln(F("Free heap: %lu bytes"), (unsigned long)ESP.getFreeHeap());
shell.printfln(F("Free mem: %d %%"), free_mem());
shell.printfln(F("Maximum free block size: %lu bytes"), (unsigned long)ESP.getMaxFreeBlockSize());
@@ -408,21 +429,19 @@ void System::show_system(uuid::console::Shell & shell) {
#ifndef EMSESP_STANDALONE
switch (WiFi.status()) {
case WL_IDLE_STATUS:
shell.printfln(F("WiFi: idle"));
shell.printfln(F("WiFi: Idle"));
break;
case WL_NO_SSID_AVAIL:
shell.printfln(F("WiFi: network not found"));
shell.printfln(F("WiFi: Network not found"));
break;
case WL_SCAN_COMPLETED:
shell.printfln(F("WiFi: network scan complete"));
shell.printfln(F("WiFi: Network scan complete"));
break;
case WL_CONNECTED: {
shell.printfln(F("WiFi: connected"));
shell.println();
shell.printfln(F("WiFi: Connected"));
shell.printfln(F("SSID: %s"), WiFi.SSID().c_str());
shell.printfln(F("BSSID: %s"), WiFi.BSSIDstr().c_str());
shell.printfln(F("RSSI: %d dBm (%d %%)"), WiFi.RSSI(), wifi_quality());
@@ -432,27 +451,26 @@ void System::show_system(uuid::console::Shell & shell) {
#elif defined(ESP32)
shell.printfln(F("Hostname: %s"), WiFi.getHostname());
#endif
shell.println();
shell.printfln(F("IPv4 address: %s/%s"), uuid::printable_to_string(WiFi.localIP()).c_str(), uuid::printable_to_string(WiFi.subnetMask()).c_str());
shell.printfln(F("IPv4 gateway: %s"), uuid::printable_to_string(WiFi.gatewayIP()).c_str());
shell.printfln(F("IPv4 nameserver: %s"), uuid::printable_to_string(WiFi.dnsIP()).c_str());
} break;
case WL_CONNECT_FAILED:
shell.printfln(F("WiFi: connection failed"));
shell.printfln(F("WiFi: Connection failed"));
break;
case WL_CONNECTION_LOST:
shell.printfln(F("WiFi: connection lost"));
shell.printfln(F("WiFi: Connection lost"));
break;
case WL_DISCONNECTED:
shell.printfln(F("WiFi: disconnected"));
shell.printfln(F("WiFi: Disconnected"));
break;
case WL_NO_SHIELD:
default:
shell.printfln(F("WiFi: unknown"));
shell.printfln(F("WiFi: Unknown"));
break;
}
@@ -479,6 +497,13 @@ void System::console_commands(Shell & shell, unsigned int context) {
restart();
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(wifi), F_(reconnect)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) {
wifi_reconnect();
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(format)},
@@ -535,8 +560,9 @@ void System::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(set), F_(wifi), F_(hostname)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
shell.println("Note, connection will be reset...");
Console::loop();
shell.println("The wifi connection will be reset...");
Shell::loop_all();
delay(1000); // wait a second
EMSESP::esp8266React.getWiFiSettingsService()->update(
[&](WiFiSettings & wifiSettings) {
wifiSettings.hostname = arguments.front().c_str();
@@ -550,14 +576,11 @@ void System::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(set), F_(wifi), F_(ssid)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
shell.println("Note, connection will be reset...");
Console::loop();
EMSESP::esp8266React.getWiFiSettingsService()->update(
[&](WiFiSettings & wifiSettings) {
wifiSettings.ssid = arguments.front().c_str();
return StateUpdateResult::CHANGED;
},
"local");
EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation([&](WiFiSettings & wifiSettings) {
wifiSettings.ssid = arguments.front().c_str();
return StateUpdateResult::CHANGED;
});
shell.println("Use `wifi reconnect` to apply the new settings");
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
@@ -570,13 +593,12 @@ void System::console_commands(Shell & shell, unsigned int context) {
[password1](Shell & shell, bool completed, const std::string & password2) {
if (completed) {
if (password1 == password2) {
EMSESP::esp8266React.getWiFiSettingsService()->update(
EMSESP::esp8266React.getWiFiSettingsService()->updateWithoutPropagation(
[&](WiFiSettings & wifiSettings) {
wifiSettings.password = password2.c_str();
return StateUpdateResult::CHANGED;
},
"local");
shell.println(F("WiFi password updated"));
});
shell.println("Use `wifi reconnect` to apply the new settings");
} else {
shell.println(F("Passwords do not match"));
}
@@ -612,6 +634,11 @@ void System::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(show), F_(mqtt)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { Mqtt::show_mqtt(shell); });
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(show), F_(users)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { System::show_users(shell); });
// enter the context
Console::enter_custom_context(shell, context);
@@ -621,8 +648,7 @@ void System::console_commands(Shell & shell, unsigned int context) {
void System::check_upgrade() {
// check for v1.9. It uses SPIFFS and only on the ESP8266
#if defined(ESP8266)
Serial.begin(115200); // TODO remove
Serial.begin(115200); // TODO remove, just for debugging
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

View File

@@ -50,8 +50,8 @@ class System {
static void mqtt_commands(const char * message);
static uint8_t free_mem();
static void upload_status(bool in_progress);
static bool upload_status();
static void upload_status(bool in_progress);
static bool upload_status();
void syslog_init();
@@ -97,6 +97,8 @@ class System {
void system_check();
static void show_system(uuid::console::Shell & shell);
static void show_users(uuid::console::Shell & shell);
static void wifi_reconnect();
static int8_t wifi_quality();
bool system_healthy_ = false;

View File

@@ -42,8 +42,8 @@ uint32_t EMSbus::last_bus_activity_ = 0; // timestamp of last time
bool EMSbus::bus_connected_ = false; // start assuming the bus hasn't been connected
uint8_t EMSbus::ems_mask_ = EMS_MASK_UNSET; // unset so its triggered when booting, the its 0x00=buderus, 0x80=junker/ht3
uint8_t EMSbus::ems_bus_id_ = EMSESP_DEFAULT_EMS_BUS_ID;
uint8_t EMSbus::tx_waiting_ = Telegram::Operation::NONE;
bool EMSbus::tx_active_ = false;
uint8_t EMSbus::tx_mode_ = EMSESP_DEFAULT_TX_MODE;
uint8_t EMSbus::tx_state_ = Telegram::Operation::NONE;
uuid::log::Logger EMSbus::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
@@ -82,9 +82,10 @@ Telegram::Telegram(const uint8_t operation,
// returns telegram's message data bytes in hex
std::string Telegram::to_string() const {
if (message_length == 0) {
if (this->message_length == 0) {
return read_flash_string(F("<empty>"));
}
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
uint8_t length = 0;
data[0] = this->src ^ RxService::ems_mask();
@@ -100,8 +101,7 @@ std::string Telegram::to_string() const {
data[2] = this->type_id;
length = 5;
}
}
if (this->operation == Telegram::Operation::TX_WRITE) {
} else if (this->operation == Telegram::Operation::TX_WRITE) {
data[1] = this->dest;
if (this->type_id > 0xFF) {
data[2] = 0xFF;
@@ -115,7 +115,12 @@ std::string Telegram::to_string() const {
for (uint8_t i = 0; i < this->message_length; i++) {
data[length++] = this->message_data[i];
}
} else {
for (uint8_t i = 0; i < this->message_length; i++) {
data[length++] = this->message_data[i];
}
}
return Helpers::data_to_hex(data, length);
}
@@ -160,17 +165,18 @@ void RxService::add(uint8_t * data, uint8_t length) {
// validate the CRC
uint8_t crc = calculate_crc(data, length - 1);
if ((data[length - 1] != crc) && (EMSESP::watch() != EMSESP::Watch::WATCH_OFF)) {
LOG_ERROR(F("Rx: %s %s(CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET);
if (data[length - 1] != crc) {
increment_telegram_error_count();
if (EMSESP::watch() != EMSESP::Watch::WATCH_OFF) {
LOG_ERROR(F("Rx: %s %s(CRC %02X != %02X)%s"), Helpers::data_to_hex(data, length).c_str(), COLOR_RED, data[length - 1], crc, COLOR_RESET);
}
return;
}
// since it's a valid telegram, work out the ems mask
// we check the 1st byte, which assumed is the src ID and see if the MSB (8th bit) is set
// this is used to identify if the protocol should be Junkers/HT3 or Buderus
// this only happens once with the first rx telegram is processed
// this only happens once with the first valid rx telegram is processed
if (ems_mask() == EMS_MASK_UNSET) {
ems_mask(data[0]);
}
@@ -251,48 +257,51 @@ void TxService::flush_tx_queue() {
// start and initialize Tx
void TxService::start() {
// grab the bus ID
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { ems_bus_id(settings.ems_bus_id); });
// grab the bus ID and tx_mode
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
ems_bus_id(settings.ems_bus_id);
tx_mode(settings.tx_mode);
});
// reset counters
telegram_read_count(0);
telegram_write_count(0);
telegram_fail_count(0);
// send first Tx request to bus master (boiler) for its registered devices
// this will be added to the queue and sent during the first tx loop()
read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
}
// Tx loop
// here we check if the Tx is not full and report an error
void TxService::loop() {
#ifndef EMSESP_STANDALONE
if ((uuid::get_uptime() - last_tx_check_) > TX_LOOP_WAIT) {
last_tx_check_ = uuid::get_uptime();
if (!tx_active() && (EMSbus::bus_connected())) {
LOG_ERROR(F("Tx is not active. Please check settings and the circuit connection."));
}
}
#endif
}
// sends a 1 byte poll which is our own device ID
void TxService::send_poll() {
//LOG_DEBUG(F("Ack %02X"),ems_bus_id() ^ ems_mask());
EMSuart::send_poll(ems_bus_id() ^ ems_mask());
if (tx_mode()) {
EMSuart::send_poll(ems_bus_id() ^ ems_mask());
}
}
// Process the next telegram on the Tx queue
// This is sent when we receieve a poll request
void TxService::send() {
// don't process if we don't have a connection to the EMS bus
// or we're in read-only mode
if (!bus_connected()) {
return;
}
// if there's nothing in the queue to transmit, send back a poll and quit
// unless tx_mode is 0
if (tx_telegrams_.empty()) {
send_poll();
return;
}
// if we're in read-only mode (tx_mode 0) forget the Tx call
if (tx_mode() == 0) {
tx_telegrams_.pop_front();
return;
}
// send next telegram in the queue (which is actually a list!)
send_telegram(tx_telegrams_.front());
@@ -380,12 +389,12 @@ void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
if (status == EMS_TX_STATUS_ERR) {
LOG_ERROR(F("Failed to transmit Tx via UART."));
increment_telegram_fail_count(); // another Tx fail
tx_waiting(Telegram::Operation::NONE); // nothing send, tx not in wait state
increment_telegram_fail_count(); // another Tx fail
tx_state(Telegram::Operation::NONE); // nothing send, tx not in wait state
return;
}
tx_waiting(telegram->operation); // tx now in a wait state
tx_state(telegram->operation); // tx now in a wait state
}
// send an array of bytes as a telegram
@@ -399,7 +408,7 @@ void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
}
telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC
tx_waiting(Telegram::Operation::NONE); // no post validation needed
tx_state(Telegram::Operation::NONE); // no post validation needed
// send the telegram to the UART Tx
uint16_t status = EMSuart::transmit(telegram_raw, length);

View File

@@ -126,11 +126,38 @@ class EMSbus {
public:
static uuid::log::Logger logger_;
static constexpr uint8_t EMS_MASK_UNSET = 0xFF; // EMS bus type (budrus/junkers) hasn't been detected yet
static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3
static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus
static constexpr uint8_t EMS_MASK_UNSET = 0xFF; // EMS bus type (budrus/junkers) hasn't been detected yet
static constexpr uint8_t EMS_MASK_HT3 = 0x80; // EMS bus type Junkers/HT3
static constexpr uint8_t EMS_MASK_BUDERUS = 0xFF; // EMS bus type Buderus
static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning
static constexpr uint8_t EMS_TX_ERROR_LIMIT = 10; // % limit of failed Tx read/write attempts before showing a warning
static bool is_ht3() {
return (ems_mask_ == EMS_MASK_HT3);
}
static uint8_t ems_mask() {
return ems_mask_;
}
static void ems_mask(uint8_t ems_mask) {
ems_mask_ = ems_mask & 0x80; // only keep the MSB (8th bit)
}
static uint8_t tx_mode() {
return tx_mode_;
}
static void tx_mode(uint8_t tx_mode) {
tx_mode_ = tx_mode;
}
static uint8_t ems_bus_id() {
return ems_bus_id_;
}
static void ems_bus_id(uint8_t ems_bus_id) {
ems_bus_id_ = ems_bus_id;
}
static bool bus_connected() {
#ifndef EMSESP_STANDALONE
@@ -143,53 +170,17 @@ class EMSbus {
#endif
}
static bool is_ht3() {
return (ems_mask_ == EMS_MASK_HT3);
}
static uint8_t protocol() {
return ems_mask_;
}
static uint8_t ems_mask() {
return ems_mask_;
}
static void ems_mask(uint8_t ems_mask) {
ems_mask_ = ems_mask & 0x80; // only keep the MSB (8th bit)
}
static uint8_t ems_bus_id() {
return ems_bus_id_;
}
static void ems_bus_id(uint8_t ems_bus_id) {
ems_bus_id_ = ems_bus_id;
}
// sets the flag for EMS bus connected
static void last_bus_activity(uint32_t timestamp) {
last_bus_activity_ = timestamp;
bus_connected_ = true;
}
static bool tx_active() {
return tx_active_;
static uint8_t tx_state() {
return tx_state_;
}
static void tx_active(bool tx_active) {
tx_active_ = tx_active;
}
static uint8_t tx_waiting() {
return tx_waiting_;
}
static void tx_waiting(uint8_t tx_waiting) {
tx_waiting_ = tx_waiting;
// if NONE, then it's been reset which means we have an active Tx
if ((tx_waiting == Telegram::Operation::NONE) && !(tx_active_)) {
tx_active_ = true;
}
static void tx_state(uint8_t tx_state) {
tx_state_ = tx_state;
}
static uint8_t calculate_crc(const uint8_t * data, const uint8_t length);
@@ -201,8 +192,8 @@ class EMSbus {
static bool bus_connected_; // start assuming the bus hasn't been connected
static uint8_t ems_mask_; // unset=0xFF, buderus=0x00, junkers/ht3=0x80
static uint8_t ems_bus_id_; // the bus id, which configurable and stored in settings
static uint8_t tx_waiting_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE)
static bool tx_active_; // is true is we have a working Tx connection
static uint8_t tx_mode_; // local copy of the tx mode
static uint8_t tx_state_; // state of the Tx line (NONE or waiting on a TX_READ or TX_WRITE)
};
class RxService : public EMSbus {
@@ -269,7 +260,6 @@ class TxService : public EMSbus {
~TxService() = default;
void start();
void loop();
void send();
void add(const uint8_t operation,
@@ -364,8 +354,7 @@ class TxService : public EMSbus {
private:
uint8_t tx_telegram_id_ = 0; // queue counter
static constexpr uint32_t TX_LOOP_WAIT = 10000; // when to check if Tx is up and running (10 sec)
uint32_t last_tx_check_ = 0;
uint32_t last_tx_check_ = 0;
std::deque<QueuedTxTelegram> tx_telegrams_;

View File

@@ -17,6 +17,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(EMSESP_STANDALONE)
#include "test.h"
@@ -692,3 +693,5 @@ void Test::dummy_mqtt_commands(const char * message) {
#pragma GCC diagnostic pop
} // namespace emsesp
#endif

View File

@@ -16,6 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#if defined(EMSESP_STANDALONE)
#ifndef EMSESP_TEST_H
#define EMSESP_TEST_H
@@ -51,3 +53,5 @@ class Test {
} // namespace emsesp
#endif
#endif

View File

@@ -34,8 +34,8 @@ static hw_timer_t * timer = NULL;
bool drop_next_rx = true;
uint8_t tx_mode_ = 0xFF;
uint8_t emsTxBuf[EMS_MAXBUFFERSIZE];
uint8_t emsTxBufIdx;
uint8_t emsTxBufLen;
uint8_t emsTxBufIdx = 0;
uint8_t emsTxBufLen = 0;
uint32_t emsTxWait;
/*
@@ -88,12 +88,15 @@ void IRAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() {
portENTER_CRITICAL(&mux);
if (emsTxBufIdx < emsTxBufLen) {
EMS_UART.fifo.rw_byte = emsTxBuf[emsTxBufIdx];
if (emsTxBufIdx == 1) {
timerAlarmWrite(timer, emsTxWait, true);
}
} else if (emsTxBufIdx == emsTxBufLen) {
EMS_UART.conf0.txd_inv = 1;
timerAlarmWrite(timer, EMSUART_TX_WAIT_BRK, true);
timerAlarmWrite(timer, EMSUART_TX_BRK_TIMER, true);
} else if (emsTxBufIdx == emsTxBufLen + 1) {
// delayMicroseconds(EMSUART_TX_WAIT_BRK);
EMS_UART.conf0.txd_inv = 0;
emsTxBufLen = 0;
timerAlarmDisable(timer);
}
emsTxBufIdx++;
@@ -122,28 +125,31 @@ void EMSuart::start(const uint8_t tx_mode) {
uart_set_pin(EMSUART_UART, EMSUART_TXPIN, EMSUART_RXPIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
EMS_UART.int_ena.val = 0; // disable all intr.
EMS_UART.int_clr.val = 0xFFFFFFFF; // clear all intr. flags
EMS_UART.idle_conf.tx_brk_num = 11; // breaklength 11 bit
EMS_UART.idle_conf.tx_brk_num = 10; // breaklength 10 bit
EMS_UART.idle_conf.rx_idle_thrhd = 256;
drop_next_rx = true;
buf_handle = xRingbufferCreate(128, RINGBUF_TYPE_NOSPLIT);
uart_isr_register(EMSUART_UART, emsuart_rx_intr_handler, NULL, ESP_INTR_FLAG_IRAM, NULL);
xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, configMAX_PRIORITIES - 1, NULL);
timer = timerBegin(1, 80, true); // timer prescale to 1 µs, countup
timer = timerBegin(0, 80, true); // timer prescale to 1 us, countup
timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, true); // Timer with edge interrupt
restart();
}
/*
* Stop, disables interrupt
* Stop, disable interrupt
*/
void EMSuart::stop() {
EMS_UART.int_ena.val = 0; // disable all intr.
// timerAlarmDisable(timer);
EMS_UART.int_ena.val = 0; // disable all intr.
EMS_UART.conf0.txd_inv = 0; // stop break
if (emsTxBufLen > 0) {
timerAlarmDisable(timer);
}
};
/*
* Restart Interrupt
* Restart uart and make mode dependent configs.
*/
void EMSuart::restart() {
if (EMS_UART.int_raw.brk_det) { // we received a break in the meantime
@@ -153,7 +159,11 @@ void EMSuart::restart() {
EMS_UART.int_ena.brk_det = 1; // activate only break
emsTxBufIdx = 0;
emsTxBufLen = 0;
emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ + 10);
if (tx_mode_ > 100) {
emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ - 90);
} else {
emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ + 10);
}
if(tx_mode_ == EMS_TXMODE_NEW) {
EMS_UART.conf0.txd_brk = 1;
} else {
@@ -161,44 +171,11 @@ void EMSuart::restart() {
}
}
/*
* Sends a 11-bit break by inverting the tx-port
*/
void EMSuart::tx_brk() {
EMS_UART.conf0.txd_inv = 1;
delayMicroseconds(EMSUART_TX_WAIT_BRK);
EMS_UART.conf0.txd_inv = 0;
}
/*
* Sends a 1-byte poll, ending with a <BRK>
*/
void EMSuart::send_poll(const uint8_t data) {
if (tx_mode_ > 5) { // timer controlled modes
emsTxBuf[0] = data;
emsTxBufIdx = 0;
emsTxBufLen = 1;
timerAlarmWrite(timer, emsTxWait, true); // start timer with autoreload
timerAlarmEnable(timer); // first interrupt comes immediately
} else if (tx_mode_ == EMS_TXMODE_DEFAULT) {
volatile uint8_t _usrxc = EMS_UART.status.rxfifo_cnt;
uint16_t timeoutcnt = EMSUART_TX_TIMEOUT;
EMS_UART.fifo.rw_byte = data;
while ((EMS_UART.status.rxfifo_cnt == _usrxc) && (--timeoutcnt > 0)) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT);
}
tx_brk();
} else if (tx_mode_ == EMS_TXMODE_EMSPLUS) {
EMS_UART.fifo.rw_byte = data;
delayMicroseconds(EMSUART_TX_WAIT_PLUS);
tx_brk();
} else if (tx_mode_ == EMS_TXMODE_HT3) {
EMS_UART.fifo.rw_byte = data;
delayMicroseconds(EMSUART_TX_WAIT_HT3);
tx_brk();
} else {
EMS_UART.fifo.rw_byte = data;
}
transmit(&data, 1);
}
/*
@@ -217,7 +194,11 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
}
emsTxBufIdx = 0;
emsTxBufLen = len;
timerAlarmWrite(timer, emsTxWait, true); // start with autoreload
if (tx_mode_ > 100 && len > 1) {
timerAlarmWrite(timer, EMSUART_TX_WAIT_REPLY, true);
} else {
timerAlarmWrite(timer, emsTxWait, true); // start with autoreload
}
timerAlarmEnable(timer);
return EMS_TX_STATUS_OK;
}
@@ -234,7 +215,9 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
EMS_UART.fifo.rw_byte = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_PLUS);
}
tx_brk();
EMS_UART.conf0.txd_inv = 1; // send <brk>
delayMicroseconds(EMSUART_TX_BRK_PLUS);
EMS_UART.conf0.txd_inv = 0;
return EMS_TX_STATUS_OK;
}
@@ -243,11 +226,13 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
EMS_UART.fifo.rw_byte = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_HT3);
}
tx_brk();
EMS_UART.conf0.txd_inv = 1; // send <brk>
delayMicroseconds(EMSUART_TX_BRK_HT3);
EMS_UART.conf0.txd_inv = 0;
return EMS_TX_STATUS_OK;
}
// mode 1
// mode 1: wait for echo after each byte
// flush fifos -- not supported in ESP32 uart #2!
// EMS_UART.conf0.rxfifo_rst = 1;
// EMS_UART.conf0.txfifo_rst = 1;
@@ -259,7 +244,9 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles...
}
}
tx_brk();
EMS_UART.conf0.txd_inv = 1;
delayMicroseconds(EMSUART_TX_BRK_EMS);
EMS_UART.conf0.txd_inv = 0;
return EMS_TX_STATUS_OK;
}

View File

@@ -46,19 +46,25 @@
#define EMS_TXMODE_NEW 4 // for michael's testing
// LEGACY
#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud
#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 11) // 1144
#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud
// Timer controlled modes
#define EMSUART_TX_BRK_TIMER (EMSUART_TX_BIT_TIME * 10 + 28) // 10.25 bit times
#define EMSUART_TX_WAIT_REPLY 100000 // delay 100ms after first byte
// EMS 1.0
#define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13
#define EMSUART_TX_TIMEOUT (32 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) // 256
#define EMSUART_TX_TIMEOUT (20 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT)
#define EMSUART_TX_BRK_EMS (EMSUART_TX_BIT_TIME * 10)
// HT3/Junkers - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) plus 7 bit delay. The -8 is for lag compensation.
// since we use a faster processor the lag is negligible
#define EMSUART_TX_WAIT_HT3 (EMSUART_TX_BIT_TIME * 17) // 1768
#define EMSUART_TX_BRK_HT3 (EMSUART_TX_BIT_TIME * 11)
// EMS+ - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) and delay of another Bytetime.
#define EMSUART_TX_WAIT_PLUS (EMSUART_TX_BIT_TIME * 20) // 2080
#define EMSUART_TX_BRK_PLUS (EMSUART_TX_BIT_TIME * 11)
// customize the GPIO pins for RX and TX here
@@ -90,10 +96,9 @@ class EMSuart {
static void emsuart_recvTask(void * para);
static void IRAM_ATTR emsuart_rx_intr_handler(void * para);
static void IRAM_ATTR emsuart_tx_timer_intr_handler();
static void tx_brk();
};
} // namespace emsesp
#endif
#endif
#endif

View File

@@ -95,10 +95,10 @@ void ICACHE_RAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() {
timer1_write(emsTxWait);
} else if (emsTxBufIdx == emsTxBufLen) {
USC0(EMSUART_UART) |= (1 << UCBRK); // set <BRK>
timer1_write(EMSUART_TX_WAIT_BRK * 5);
timer1_write(EMSUART_TX_BRK_TIMER);
} else {
USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset <BRK>
sending_ = false;
sending_ = false;
}
}
@@ -114,7 +114,6 @@ void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() {
* init UART0 driver
*/
void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode + 10); // bittimes wait between bytes
if (tx_mode_ != 0xFF) { // it's a restart no need to configure uart
tx_mode_ = tx_mode;
restart();
@@ -129,7 +128,6 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
}
pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer
ETS_UART_INTR_DISABLE();
ETS_UART_INTR_ATTACH(nullptr, nullptr);
// pin settings
@@ -149,22 +147,14 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
// UCFFT = RX FIFO Full Threshold (7 bit) = want this to be 31 for 32 bytes of buffer (default was 127)
// see https://www.espressif.com/sites/default/files/documentation/esp8266-technical_reference_en.pdf
//
// change: we set UCFFT to 1 to get an immediate indicator about incoming traffic.
// Otherwise, we're only noticed by UCTOT or RxBRK!
// change: don't care, we do not use these interrupts
USC1(EMSUART_UART) = 0; // reset config first
USC1(EMSUART_UART) = 0; // reset config
// USC1(EMSUART_UART) = (0x7F << UCFFT) | (0x01 << UCTOT) | (1 << UCTOE); // enable interupts
// set interrupts for triggers
USIC(EMSUART_UART) = 0xFFFF; // clear all interupts
USIE(EMSUART_UART) = 0; // disable all interrupts
// enable rx break, fifo full and timeout.
// but not frame error UIFR (because they are too frequent) or overflow UIOF because our buffer is only max 32 bytes
// change: we don't care about Rx Timeout - it may lead to wrong readouts
// change:we don't care about Fifo full and read only on break-detect
USIE(EMSUART_UART) = (1 << UIBD) | (0 << UIFF) | (0 << UITO);
// set up interrupt callbacks for Rx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
@@ -175,13 +165,12 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
system_uart_swap();
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr);
// ETS_UART_INTR_ENABLE();
drop_next_rx = true;
// for sending with large delay in EMS+ mode we use a timer interrupt
timer1_attachInterrupt(emsuart_tx_timer_intr_handler); // Add ISR Function
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); // 5 MHz timer
USIE(EMSUART_UART) = (1 << UIBD);
timer1_attachInterrupt(emsuart_tx_timer_intr_handler);
restart();
}
/*
@@ -190,7 +179,9 @@ void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
*/
void ICACHE_FLASH_ATTR EMSuart::stop() {
USIE(EMSUART_UART) = 0;
// timer1_disable();
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear BRK bit
timer1_disable();
sending_ = false;
}
/*
@@ -198,67 +189,25 @@ void ICACHE_FLASH_ATTR EMSuart::stop() {
*/
void ICACHE_FLASH_ATTR EMSuart::restart() {
if (USIR(EMSUART_UART) & ((1 << UIBD))) {
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the <brk> detect interrupt
drop_next_rx = true;
}
if (tx_mode_ > 100) {
emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode_ - 90);
} else {
emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode_ + 10); // bittimes wait to next bytes
}
emsTxBufIdx = 0;
emsTxBufLen = 0;
// timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
USIE(EMSUART_UART) = (1 << UIBD);
}
/*
* Send a BRK signal
* Which is a 11-bit set of zero's (11 cycles)
*/
void ICACHE_FLASH_ATTR EMSuart::tx_brk() {
// make sure Tx FIFO is empty
while (((USS(EMSUART_UART) >> USTXC) & 0xFF)) {
}
USC0(EMSUART_UART) |= (1 << UCBRK); // set bit
// also for EMS+ there is no need to wait longer, we are finished and can free the bus.
delayMicroseconds(EMSUART_TX_WAIT_BRK); // 1144
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear BRK bit
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
USIE(EMSUART_UART) = (1 << UIBD); // enable <brk> interrupt
}
/*
* Sends a 1-byte poll, ending with a <BRK>
* It's a bit dirty. there is no special wait logic per tx_mode type, fifo flushes or error checking
*/
void EMSuart::send_poll(uint8_t data) {
// reset tx-brk, just in case it is accidentally set
USC0(EMSUART_UART) &= ~(1 << UCBRK);
sending_ = true;
if (tx_mode_ >= 5) { // timer controlled modes
emsTxBuf[0] = data;
emsTxBufIdx = 0;
emsTxBufLen = 1;
timer1_write(emsTxWait);
} else if (tx_mode_ == EMS_TXMODE_NEW) { // hardware controlled modes
USF(EMSUART_UART) = data;
USC0(EMSUART_UART) |= (1 << UCBRK);
} else if (tx_mode_ == EMS_TXMODE_HT3) {
USF(EMSUART_UART) = data;
delayMicroseconds(EMSUART_TX_WAIT_HT3);
tx_brk(); // send <BRK>
sending_ = false;
} else if (tx_mode_ == EMS_TXMODE_EMSPLUS) {
USF(EMSUART_UART) = data;
delayMicroseconds(EMSUART_TX_WAIT_PLUS);
tx_brk(); // send <BRK>
sending_ = false;
} else {
// tx_mode 1
volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF;
USF(EMSUART_UART) = data;
uint16_t timeoutcnt = EMSUART_TX_TIMEOUT;
while ((((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) && (--timeoutcnt > 0)) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles...
}
tx_brk(); // send <BRK>
sending_ = false;
}
void ICACHE_FLASH_ATTR EMSuart::send_poll(uint8_t data) {
transmit(&data, 1);
}
/*
@@ -270,19 +219,21 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
if (len == 0 || len >= EMS_MAXBUFFERSIZE) {
return EMS_TX_STATUS_ERR; // nothing or to much to send
}
// reset tx-brk, just in case it is accidentally set
USC0(EMSUART_UART) &= ~(1 << UCBRK);
sending_ = true;
// timer controlled modes with extra delay
if (tx_mode_ >= 5) {
sending_ = true;
for (uint8_t i = 0; i < len; i++) {
emsTxBuf[i] = buf[i];
}
USF(EMSUART_UART) = buf[0]; // send first byte
emsTxBufIdx = 0;
emsTxBufLen = len;
timer1_write(emsTxWait);
if (tx_mode_ > 100 && len > 1) {
timer1_write(EMSUART_TX_WAIT_REPLY); // large delay after first byte
} else {
timer1_write(emsTxWait);
}
return EMS_TX_STATUS_OK;
}
@@ -291,7 +242,7 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
}
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end, clear by interrupt
return EMS_TX_STATUS_OK;
}
@@ -301,8 +252,9 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
USF(EMSUART_UART) = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_PLUS); // 2070
}
tx_brk(); // send <BRK>
sending_ = false;
USC0(EMSUART_UART) |= (1 << UCBRK); // set break
delayMicroseconds(EMSUART_TX_BRK_PLUS);
USC0(EMSUART_UART) &= ~(1 << UCBRK);
return EMS_TX_STATUS_OK;
}
@@ -316,8 +268,9 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
// wait until bits are sent on wire
delayMicroseconds(EMSUART_TX_WAIT_HT3);
}
tx_brk(); // send <BRK>
sending_ = false;
USC0(EMSUART_UART) |= (1 << UCBRK); // set break bit
delayMicroseconds(EMSUART_TX_BRK_HT3);
USC0(EMSUART_UART) &= ~(1 << UCBRK);
return EMS_TX_STATUS_OK;
}
@@ -346,9 +299,7 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
*
*/
// disable rx interrupt
// clear Rx status register, resetting the Rx FIFO and flush it
// ETS_UART_INTR_DISABLE();
emsuart_flush_fifos();
// send the bytes along the serial line
@@ -361,21 +312,12 @@ uint16_t ICACHE_FLASH_ATTR EMSuart::transmit(uint8_t * buf, uint8_t len) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles...
}
}
// we got the whole telegram in the Rx buffer
// on Rx-BRK (bus collision), we simply enable Rx and leave it
// otherwise we send the final Tx-BRK
// worst case, we'll see an additional Rx-BRK...
// neither bus collision nor timeout - send terminating BRK signal
if (!(USIS(EMSUART_UART) & (1 << UIBD))) {
// no bus collision - send terminating BRK signal
tx_brk();
}
// ETS_UART_INTR_ENABLE(); // open up the FIFO again to start receiving
sending_ = false;
USC0(EMSUART_UART) |= (1 << UCBRK); // snd break
delayMicroseconds(EMSUART_TX_BRK_EMS);
USC0(EMSUART_UART) &= ~(1 << UCBRK);
return EMS_TX_STATUS_OK; // send the Tx ok status back
}
} // namespace emsesp
#endif
#endif

View File

@@ -40,20 +40,26 @@
#define EMS_TXMODE_NEW 4 // for michael's testing
// LEGACY
#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud
#define EMSUART_TX_WAIT_BRK (EMSUART_TX_BIT_TIME * 11) // 1144
#define EMSUART_TX_BIT_TIME 104 // bit time @9600 baud
// TIMER modes
#define EMSUART_TX_BRK_TIMER (EMSUART_TX_BIT_TIME * 52) // > 10 bittimes for timer modes
#define EMSUART_TX_WAIT_REPLY 500000 // delay 100ms after first byte
// EMS 1.0
#define EMSUART_TX_BUSY_WAIT (EMSUART_TX_BIT_TIME / 8) // 13
// #define EMSUART_TX_TIMEOUT (22 * EMSUART_TX_BIT_TIME / EMSUART_TX_BUSY_WAIT) // 176
#define EMSUART_TX_TIMEOUT (32 * 8) // 256 for tx_mode 1 - see https://github.com/proddy/EMS-ESP/issues/398#issuecomment-645886277
// #define EMSUART_TX_TIMEOUT (32 * 8) // 256 for tx_mode 1 - see https://github.com/proddy/EMS-ESP/issues/398#issuecomment-645886277
#define EMSUART_TX_TIMEOUT (220 * 8) // 1760 as in v1.9 (180 ms)
#define EMSUART_TX_BRK_EMS (EMSUART_TX_BIT_TIME * 10)
// HT3/Junkers - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) plus 7 bit delay. The -8 is for lag compensation.
// since we use a faster processor the lag is negligible
#define EMSUART_TX_WAIT_HT3 (EMSUART_TX_BIT_TIME * 17) // 1768
#define EMSUART_TX_BRK_HT3 (EMSUART_TX_BIT_TIME * 11)
// EMS+ - Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit) and delay of another Bytetime.
#define EMSUART_TX_WAIT_PLUS (EMSUART_TX_BIT_TIME * 20) // 2080
#define EMSUART_TX_BRK_PLUS (EMSUART_TX_BIT_TIME * 11)
namespace emsesp {
@@ -83,7 +89,6 @@ class EMSuart {
static void ICACHE_RAM_ATTR emsuart_rx_intr_handler(void * para);
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events);
static void ICACHE_FLASH_ATTR emsuart_flush_fifos();
static void ICACHE_FLASH_ATTR tx_brk();
static void ICACHE_RAM_ATTR emsuart_tx_timer_intr_handler();
static bool sending_;
};
@@ -91,4 +96,4 @@ class EMSuart {
} // namespace emsesp
#endif
#endif
#endif

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.0.0b7"
#define EMSESP_APP_VERSION "2.0.0b11"