initial commit

This commit is contained in:
proddy
2020-07-05 18:29:08 +02:00
parent 26b201ea2f
commit c5933e8c14
739 changed files with 86566 additions and 20952 deletions

View File

@@ -0,0 +1,93 @@
#include "EMSESPSettingsService.h"
#include "emsesp.h"
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) {
}
void EMSESPSettings::read(EMSESPSettings & settings, JsonObject & root) {
root["tx_mode"] = settings.tx_mode;
root["ems_bus_id"] = settings.ems_bus_id;
root["system_heartbeat"] = settings.system_heartbeat;
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["publish_time"] = settings.publish_time;
root["mqtt_format"] = settings.mqtt_format;
root["mqtt_qos"] = settings.mqtt_qos;
}
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;
newSettings.system_heartbeat = root["system_heartbeat"] | EMSESP_DEFAULT_SYSTEM_HEARTBEAT;
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;
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;
newSettings.publish_time = root["publish_time"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.mqtt_format = root["mqtt_format"] | EMSESP_DEFAULT_MQTT_FORMAT;
newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS;
bool changed = false;
// changing master thermostat, bus ID, mqtt_format requires a reboot
if ((newSettings.master_thermostat != settings.master_thermostat) || (newSettings.ems_bus_id != settings.ems_bus_id)
|| (newSettings.mqtt_format != settings.mqtt_format)) {
changed = true;
}
if (newSettings.system_heartbeat != settings.system_heartbeat) {
EMSESP::system_.set_heartbeat(newSettings.system_heartbeat);
changed = true;
}
if (newSettings.tx_mode != settings.tx_mode) {
EMSESP::reset_tx(newSettings.tx_mode); // reset counters
changed = true;
}
if (newSettings.mqtt_qos != settings.mqtt_qos) {
EMSESP::mqtt_.set_qos(newSettings.mqtt_qos);
EMSESP::mqtt_.disconnect(); // force a disconnect & reconnect
changed = true;
}
if (newSettings.publish_time != settings.publish_time) {
EMSESP::mqtt_.set_publish_time(newSettings.publish_time);
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;
}
void EMSESPSettingsService::begin() {
_fsPersistence.readFromFS();
}
} // namespace emsesp

View File

@@ -0,0 +1,64 @@
#ifndef EMSESPSettingsConfig_h
#define EMSESPSettingsConfig_h
#include <HttpEndpoint.h>
#include <FSPersistence.h>
#define EMSESP_SETTINGS_FILE "/config/emsespSettings.json"
#define EMSESP_SETTINGS_SERVICE_PATH "/rest/emsespSettings"
#define EMSESP_DEFAULT_TX_MODE 1
#define EMSESP_DEFAULT_EMS_BUS_ID 0x0B // service key
#define EMSESP_DEFAULT_SYSTEM_HEARTBEAT true
#define EMSESP_DEFAULT_SYSLOG_LEVEL -1
#define EMSESP_DEFAULT_SYSLOG_MARK_INTERVAL 0
#define EMSESP_DEFAULT_SYSLOG_HOST ""
#define EMSESP_DEFAULT_MASTER_THERMOSTAT 0 // not set
#define EMSESP_DEFAULT_SHOWER_TIMER false
#define EMSESP_DEFAULT_SHOWER_ALERT false
#define EMSESP_DEFAULT_MQTT_FORMAT 2 // nested
#define EMSESP_DEFAULT_MQTT_QOS 0
#ifndef EMSESP_STANDALONE
#define EMSESP_DEFAULT_PUBLISH_TIME 10
#else
#define EMSESP_DEFAULT_PUBLISH_TIME 0
#endif
namespace emsesp {
enum MQTT_format : uint8_t { SINGLE = 1, NESTED, HA, CUSTOM };
class EMSESPSettings {
public:
uint8_t tx_mode;
uint8_t ems_bus_id;
bool system_heartbeat;
int8_t syslog_level; // uuid::log::Level
uint32_t syslog_mark_interval;
String syslog_host;
uint8_t master_thermostat;
bool shower_timer;
bool shower_alert;
uint16_t publish_time; // seconds
uint8_t mqtt_format; // 1=single, 2=nested, 3=ha, 4=custom
uint8_t mqtt_qos;
static void read(EMSESPSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, EMSESPSettings & settings);
};
class EMSESPSettingsService : public StatefulService<EMSESPSettings> {
public:
EMSESPSettingsService(AsyncWebServer * server, FS * fs, SecurityManager * securityManager);
void begin();
private:
HttpEndpoint<EMSESPSettings> _httpEndpoint;
FSPersistence<EMSESPSettings> _fsPersistence;
};
} // namespace emsesp
#endif

View File

@@ -0,0 +1,44 @@
#include "EMSESPStatusService.h"
#include "emsesp.h"
#include "mqtt.h"
namespace emsesp {
EMSESPStatusService::EMSESPStatusService(AsyncWebServer * server, SecurityManager * securityManager, AsyncMqttClient * mqttClient)
: _mqttClient(mqttClient) {
_mqttClient->onConnect(std::bind(&EMSESPStatusService::init_mqtt, this)); // configure MQTT callback
server->on(EMSESP_STATUS_SERVICE_PATH,
HTTP_GET,
securityManager->wrapRequest(std::bind(&EMSESPStatusService::emsespStatusService, this, std::placeholders::_1),
AuthenticationPredicates::IS_AUTHENTICATED));
}
void EMSESPStatusService::emsespStatusService(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, MAX_EMSESP_STATUS_SIZE);
JsonObject root = response->getRoot();
root["version"] = EMSESP_APP_VERSION;
root["status"] = EMSESP::bus_status(); // 0, 1 or 2
root["rx_received"] = EMSESP::rxservice_.telegram_count();
root["tx_sent"] = EMSESP::txservice_.telegram_read_count() + EMSESP::txservice_.telegram_write_count();
root["crc_errors"] = EMSESP::rxservice_.telegram_error_count();
root["tx_errors"] = EMSESP::txservice_.telegram_fail_count();
root["mqtt_fails"] = Mqtt::publish_fails();
root["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
root["free_mem"] = System::free_mem();
response->setLength();
request->send(response);
}
void EMSESPStatusService::init_mqtt() {
if (!_mqttClient->connected()) {
return;
}
Mqtt::on_connect();
}
} // namespace emsesp

36
src/EMSESPStatusService.h Normal file
View File

@@ -0,0 +1,36 @@
#ifndef EMSESPStatusService_h
#define EMSESPStatusService_h
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <ESPAsyncWebServer.h>
#include <SecurityManager.h>
#include <HttpEndpoint.h>
#include <MqttPubSub.h>
#include <WebSocketTxRx.h>
#include "EMSESPSettingsService.h"
#include "version.h"
#define MAX_EMSESP_STATUS_SIZE 1024
#define EMSESP_STATUS_SERVICE_PATH "/rest/emsespStatus"
namespace emsesp {
class EMSESPStatusService {
public:
EMSESPStatusService(AsyncWebServer * server, SecurityManager * securityManager, AsyncMqttClient * mqttClient);
private:
EMSESPSettingsService * _emsespSettingsService;
void emsespStatusService(AsyncWebServerRequest * request);
AsyncMqttClient * _mqttClient;
void init_mqtt();
};
} // namespace emsesp
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,498 +0,0 @@
/*
* MyESP.h - does all the basics like WiFI/MQTT/NTP/Debug logs etc
*
* Paul Derbyshire - first version December 2018
*/
#pragma once
#ifndef MyESP_h
#define MyESP_h
#define MYESP_VERSION "1.2.37"
#include <ArduinoJson.h>
#include <ArduinoOTA.h>
#include <AsyncMqttClient.h>
#include <ESPAsyncWebServer.h>
#include <FS.h>
#include <JustWifi.h>
#include <deque> // for MQTT publish queue
// SysLog
#include <uuid/common.h>
#include <uuid/log.h>
#include <uuid/syslog.h>
static uuid::syslog::SyslogService syslog;
enum MYESP_SYSLOG_LEVEL : uint8_t { MYESP_SYSLOG_INFO, MYESP_SYSLOG_ERROR };
// local libraries
#include "Ntp.h"
#include "TelnetSpy.h" // modified from https://github.com/yasheena/telnetspy
#ifdef CRASH
#include <EEPROM_Rotate.h>
#endif
extern "C" {
void custom_crash_callback(struct rst_info *, uint32_t, uint32_t);
#include "user_interface.h"
extern struct rst_info resetInfo;
}
#if defined(ARDUINO_ARCH_ESP32)
#include <SPIFFS.h> // added for ESP32
#define ets_vsnprintf vsnprintf // added for ESP32
#define OTA_PORT 3232
#else
#include <ESPAsyncTCP.h>
#define OTA_PORT 8266
#endif
// web files
// reference libs
#include "webh/glyphicons-halflings-regular.woff.gz.h"
#include "webh/required.css.gz.h"
#include "webh/required.js.gz.h"
// custom stuff
#include "webh/index.html.gz.h"
#include "webh/myesp.html.gz.h"
#include "webh/myesp.js.gz.h"
#define MYESP_CONFIG_FILE "/myesp.json"
#define MYESP_CUSTOMCONFIG_FILE "/customconfig.json"
#define MYESP_OLD_EVENTLOG_FILE "/eventlog.json" // depreciated
#define MYESP_OLD_CONFIG_FILE "/config.json" // depreciated
#define MYESP_HTTP_USERNAME "admin" // HTTP username
#define MYESP_HTTP_PASSWORD "admin" // default password
#define MYESP_NTP_SERVER "pool.ntp.org" // default ntp server
#define MYESP_LOADAVG_INTERVAL 30000 // Interval between calculating load average (in ms) = 30 seconds
// WIFI
#define MYESP_WIFI_CONNECT_TIMEOUT 20000 // Connecting timeout for WIFI in ms (20 seconds)
#define MYESP_WIFI_RECONNECT_INTERVAL 600000 // If could not connect to WIFI, retry after this time in ms. 10 minutes
// set to value >0 if the ESP is overheating or there are timing issues. Recommend a value of 1.
#define MYESP_DELAY 1
// MQTT
#define MQTT_PORT 1883 // MQTT port
#define MQTT_RECONNECT_DELAY_MIN 2000 // Try to reconnect in 3 seconds upon disconnection
#define MQTT_RECONNECT_DELAY_STEP 3000 // Increase the reconnect delay in 3 seconds after each failed attempt
#define MQTT_RECONNECT_DELAY_MAX 120000 // Set reconnect time to 2 minutes at most
#define MQTT_TOPIC_START "start"
#define MQTT_TOPIC_HEARTBEAT "heartbeat"
#define MQTT_TOPIC_RESTART "restart"
#define MQTT_WILL_ONLINE_PAYLOAD "online" // for last will & testament payload
#define MQTT_WILL_OFFLINE_PAYLOAD "offline" // for last will & testament payload
#define MQTT_BASE_DEFAULT "home" // default MQTT prefix to topics
#define MQTT_RETAIN false // default false
#define MQTT_KEEPALIVE 60 // default keepalive 1 minute
#define MQTT_QOS 0 // default qos 0
#define MQTT_WILL_TOPIC "status" // for last will & testament topic name
#define MQTT_MAX_TOPIC_SIZE 50 // max length of MQTT topic
#define MQTT_QUEUE_MAX_SIZE 50 // Size of the MQTT queue
#define MQTT_PUBLISH_WAIT 750 // time in ms before sending MQTT messages
#define MQTT_PUBLISH_MAX_RETRY 4 // max retries for giving up on publishing
#define MYESP_JSON_MAXSIZE_LARGE 2000 // for large Dynamic json files - https://arduinojson.org/v6/assistant/
#define MYESP_JSON_MAXSIZE_MEDIUM 800 // for medium Dynamic json files - https://arduinojson.org/v6/assistant/
#define MYESP_JSON_MAXSIZE_SMALL 200 // for smaller Static json documents - https://arduinojson.org/v6/assistant/
// Internal MQTT events
#define MQTT_CONNECT_EVENT 0
#define MQTT_DISCONNECT_EVENT 1
#define MQTT_MESSAGE_EVENT 2
#define MYESP_MQTT_PAYLOAD_ON '1' // for MQTT switch on
#define MYESP_MQTT_PAYLOAD_OFF '0' // for MQTT switch off
// Telnet
#define TELNET_SERIAL_BAUD 115200
#define TELNET_MAX_COMMAND_LENGTH 80 // length of a command
#define TELNET_MAX_BUFFER_LENGTH 700 // max length of telnet string
#define TELNET_EVENT_CONNECT 1
#define TELNET_EVENT_DISCONNECT 0
#define TELNET_EVENT_SHOWCMD 10
#define TELNET_EVENT_SHOWSET 20
// ANSI Colors
#define COLOR_RESET "\x1B[0m"
#define COLOR_BLACK "\x1B[0;30m"
#define COLOR_RED "\x1B[0;31m"
#define COLOR_GREEN "\x1B[0;32m"
#define COLOR_YELLOW "\x1B[0;33m"
#define COLOR_BLUE "\x1B[0;34m"
#define COLOR_MAGENTA "\x1B[0;35m"
#define COLOR_CYAN "\x1B[0;36m"
#define COLOR_WHITE "\x1B[0;37m"
#define COLOR_BOLD_ON "\x1B[1m"
#define COLOR_BOLD_OFF "\x1B[22m"
#define COLOR_BRIGHT_BLACK "\x1B[0;90m"
#define COLOR_BRIGHT_RED "\x1B[0;91m"
#define COLOR_BRIGHT_GREEN "\x1B[0;92m"
#define COLOR_BRIGHT_YELLOW "\x1B[0;99m"
#define COLOR_BRIGHT_BLUE "\x1B[0;94m"
#define COLOR_BRIGHT_MAGENTA "\x1B[0;95m"
#define COLOR_BRIGHT_CYAN "\x1B[0;96m"
#define COLOR_BRIGHT_WHITE "\x1B[0;97m"
// reset reason codes
PROGMEM const char custom_reset_hardware[] = "Hardware button";
PROGMEM const char custom_reset_terminal[] = "Restart from terminal";
PROGMEM const char custom_reset_mqtt[] = "Restart from MQTT";
PROGMEM const char custom_reset_ota[] = "Restart after successful OTA update";
PROGMEM const char custom_reset_factory[] = "Factory reset";
PROGMEM const char * const custom_reset_string[] = {custom_reset_hardware, custom_reset_terminal, custom_reset_mqtt, custom_reset_ota, custom_reset_factory};
#define CUSTOM_RESET_HARDWARE 1 // Reset from hardware button
#define CUSTOM_RESET_TERMINAL 2 // Reset from terminal
#define CUSTOM_RESET_MQTT 3 // Reset via MQTT
#define CUSTOM_RESET_OTA 4 // Reset after successful OTA update
#define CUSTOM_RESET_FACTORY 5 // Factory reset
#define CUSTOM_RESET_MAX 5
// SPIFFS - max allocation is 1000 KB
// https://arduinojson.org/v6/assistant/
#define MYESP_SPIFFS_MAXSIZE_CONFIG 999 // max size for a config file
// CRASH
/**
* Structure of the single crash data set
*
* 1. Crash time
* 2. Restart reason
* 3. Exception cause
* 4. epc1
* 5. epc2
* 6. epc3
* 7. excvaddr
* 8. depc
* 9. address of stack start
* 10. address of stack end
* 11. stack trace bytes
* ...
*/
#define SAVE_CRASH_EEPROM_OFFSET 0x0100 // initial address for crash data
#define SAVE_CRASH_CRASH_TIME 0x00 // 4 bytes
#define SAVE_CRASH_RESTART_REASON 0x04 // 1 byte
#define SAVE_CRASH_EXCEPTION_CAUSE 0x05 // 1 byte
#define SAVE_CRASH_EPC1 0x06 // 4 bytes
#define SAVE_CRASH_EPC2 0x0A // 4 bytes
#define SAVE_CRASH_EPC3 0x0E // 4 bytes
#define SAVE_CRASH_EXCVADDR 0x12 // 4 bytes
#define SAVE_CRASH_DEPC 0x16 // 4 bytes
#define SAVE_CRASH_STACK_START 0x1A // 4 bytes
#define SAVE_CRASH_STACK_END 0x1E // 4 bytes
#define SAVE_CRASH_STACK_TRACE 0x22 // variable
// Base address of USER RTC memory
// https://github.com/esp8266/esp8266-wiki/wiki/Memory-Map#memmory-mapped-io-registers
#define RTCMEM_ADDR_BASE (0x60001200)
// RTC memory is accessed using blocks of 4 bytes.
// Blocks 0..63 are reserved by the SDK, 64..192 are available to the user.
// Blocks 64..96 are reserved by the eboot 'struct eboot_command' (128 -> (128 / 4) -> 32):
// https://github.com/esp8266/Arduino/blob/master/bootloaders/eboot/eboot_command.h
#define RTCMEM_OFFSET 32u
#define RTCMEM_ADDR (RTCMEM_ADDR_BASE + (RTCMEM_OFFSET * 4u))
#define RTCMEM_BLOCKS 96u
#define RTCMEM_MAGIC 0x45535076
struct RtcmemData {
uint32_t magic; // RTCMEM_MAGIC
uint32_t sys; // system details
};
static_assert(sizeof(RtcmemData) <= (RTCMEM_BLOCKS * 4u), "RTCMEM struct is too big");
#define MYESP_SYSTEM_CHECK_TIME 60000 // The system is considered stable after these many millis (1 min)
#define MYESP_SYSTEM_CHECK_MAX 10 // After this many crashes on boot
#define MYESP_HEARTBEAT_INTERVAL 60000 // in milliseconds, how often the MQTT heartbeat is sent (1 min)
typedef struct {
bool set; // is it a set command?
char key[60];
char description[110];
} command_t;
typedef enum {
MYESP_FSACTION_SET,
MYESP_FSACTION_LIST,
MYESP_FSACTION_SAVE,
MYESP_FSACTION_LOAD,
MYESP_FSACTION_ERR,
MYESP_FSACTION_OK,
MYESP_FSACTION_RESTART
} MYESP_FSACTION_t;
typedef enum {
MYESP_BOOTSTATUS_POWERON = 0,
MYESP_BOOTSTATUS_BOOTED = 1,
MYESP_BOOTSTATUS_BOOTING = 2,
MYESP_BOOTSTATUS_RESETNEEDED = 3
} MYESP_BOOTSTATUS_t; // boot messages
typedef std::function<void(unsigned int, const char *, const char *)> mqtt_callback_f;
typedef std::function<void()> wifi_callback_f;
typedef std::function<void()> ota_callback_f;
typedef std::function<void(uint8_t, const char *)> telnetcommand_callback_f;
typedef std::function<void(uint8_t)> telnet_callback_f;
typedef std::function<bool(MYESP_FSACTION_t, JsonObject json)> fs_loadsave_callback_f;
typedef std::function<MYESP_FSACTION_t(MYESP_FSACTION_t, uint8_t, const char *, const char *)> fs_setlist_callback_f;
typedef std::function<void(JsonObject root)> web_callback_f;
// calculates size of an 2d array at compile time
template <typename T, size_t N>
constexpr size_t ArraySize(T (&)[N]) {
return N;
}
#define MYESP_UPTIME_OVERFLOW 4294967295 // Uptime overflow value
#define MYESP_MAX_STR_LEN 16 // web min and max length of wifi ssid and password
#define MYESP_BOOTUP_FLASHDELAY 50 // flash duration for LED at bootup sequence
#define MYESP_BOOTUP_DELAY 2000 // time before we open the window to reset. This is to stop resetting values when uploading firmware via USB
// class definition
class MyESP {
protected:
// webserver
AsyncWebServer * _webServer;
AsyncWebSocket * _ws;
// NTP
NtpClient NTP;
public:
MyESP();
~MyESP();
// wifi
void setWIFICallback(void (*callback)());
void setWIFI(wifi_callback_f callback);
bool isWifiConnected();
bool isAPmode();
// mqtt
bool isMQTTConnected();
bool mqttSubscribe(const char * topic);
void mqttUnsubscribe(const char * topic);
bool mqttPublish(const char * topic, const char * payload);
bool mqttPublish(const char * topic, const char * payload, bool retain);
bool mqttPublish(const char * topic, JsonDocument & payload);
bool mqttPublish(const char * topic, JsonDocument & payload, bool retain);
void setMQTT(mqtt_callback_f callback);
bool mqttUseNestedJson();
// OTA
void setOTA(ota_callback_f OTACallback_pre, ota_callback_f OTACallback_post);
// debug & telnet
void myDebug(const char * format, ...);
void myDebug_P(PGM_P format_P, ...);
void setTelnet(telnetcommand_callback_f callback_cmd, telnet_callback_f callback);
bool getUseSerial();
void setUseSerial(bool toggle);
// syslog
void writeLogEvent(const uint8_t type, const char * msg);
// FS
void setSettings(fs_loadsave_callback_f loadsave, fs_setlist_callback_f setlist, bool useSerial = true);
void saveSettings();
bool fs_saveConfig(JsonObject root);
bool fs_saveCustomConfig(JsonObject root);
bool fs_setSettingValue(char ** setting, const char * value, const char * value_default);
bool fs_setSettingValue(uint16_t * setting, const char * value, uint16_t value_default);
bool fs_setSettingValue(uint8_t * setting, const char * value, uint8_t value_default);
bool fs_setSettingValue(int8_t * setting, const char * value, int8_t value_default);
bool fs_setSettingValue(bool * setting, const char * value, bool value_default);
// Web
void setWeb(web_callback_f callback_web);
// Crash
void crashClear();
void crashDump();
void crashInfo();
// general
void end();
void loop();
void begin(const char * app_hostname, const char * app_name, const char * app_version, const char * app_url, const char * app_url_api);
void resetESP();
int getWifiQuality();
void showSystemStats();
bool getHeartbeat();
uint32_t getSystemLoadAverage();
uint32_t getSystemResetReason();
uint8_t getSystemBootStatus();
unsigned long getSystemTime();
void heartbeatPrint();
void heartbeatCheck(bool force = false);
private:
// mqtt
void _mqttOnMessage(char * topic, char * payload, size_t len);
void _mqttOnPublish(uint16_t packetId);
void _mqttConnect();
void _mqtt_setup();
void _mqttOnConnect();
void _sendStart();
char * _mqttTopic(const char * topic);
bool _mqttQueue(const char * topic, const char * payload, bool retain);
bool _mqttQueue(const char * topic, JsonDocument & payload, bool retain);
void _printMQTTQueue();
void _mqttPublishQueue();
void _mqttRemoveLastPublish();
void _sendStartTopic();
AsyncMqttClient mqttClient; // the MQTT class
uint32_t _mqtt_reconnect_delay;
mqtt_callback_f _mqtt_callback_f;
char * _mqtt_ip;
char * _mqtt_user;
char * _mqtt_password;
uint16_t _mqtt_port;
char * _mqtt_base;
bool _mqtt_enabled;
uint16_t _mqtt_keepalive;
uint8_t _mqtt_qos;
bool _mqtt_retain;
char * _mqtt_will_topic;
char * _mqtt_will_online_payload;
char * _mqtt_will_offline_payload;
uint32_t _mqtt_last_connection;
bool _mqtt_connecting;
bool _mqtt_heartbeat;
bool _mqtt_nestedjson;
uint16_t _mqtt_publish_fails;
// wifi
void _wifiCallback(justwifi_messages_t code, char * parameter);
void _wifi_setup();
wifi_callback_f _wifi_callback_f;
char * _network_ssid;
char * _network_password;
uint8_t _network_wmode;
char * _network_staticip;
char * _network_gatewayip;
char * _network_nmask;
char * _network_dnsip;
bool _wifi_connected;
String _getESPhostname();
// ota
ota_callback_f _ota_pre_callback_f;
ota_callback_f _ota_post_callback_f;
void _ota_setup();
void _OTACallback();
// crash
void _eeprom_setup();
// telnet
TelnetSpy SerialAndTelnet;
void _telnetConnected();
void _telnetDisconnected();
void _telnetHandle();
void _telnetCommand(char * commandLine);
char * _telnet_readWord(bool allow_all_chars);
void _telnet_setup();
char _command[TELNET_MAX_COMMAND_LENGTH]; // the input command from either Serial or Telnet
void _consoleShowHelp();
telnetcommand_callback_f _telnetcommand_callback_f; // Callable for projects commands
telnet_callback_f _telnet_callback_f; // callback for connect/disconnect
bool _changeSetting(uint8_t wc, const char * setting, const char * value);
// syslog
void _syslog_setup();
// fs and settings
void _fs_setup();
bool _fs_loadConfig();
bool _fs_loadCustomConfig();
void _fs_eraseConfig();
bool _fs_writeConfig();
bool _fs_createCustomConfig();
bool _fs_sendConfig();
size_t _fs_validateConfigFile(const char * filename, size_t maxsize, JsonDocument & doc);
size_t _fs_validateLogFile(const char * filename);
fs_loadsave_callback_f _fs_loadsave_callback_f;
fs_setlist_callback_f _fs_setlist_callback_f;
void _printSetCommands();
// general
char * _general_hostname;
char * _app_name;
char * _app_version;
char * _app_url;
char * _app_updateurl;
char * _app_updateurl_dev;
bool _suspendOutput;
bool _general_serial;
bool _general_log_events;
char * _general_log_ip;
char * _buildTime;
bool _timerequest;
bool _formatreq;
unsigned long _getUptime();
char * _getBuildTime();
bool _hasValue(const char * s);
void _printHeap(const char * s);
void _kick();
// reset reason and rtcmem
bool _rtcmem_status;
bool _rtcmemStatus();
bool _getRtcmemStatus();
void _rtcmemInit();
void _rtcmemSetup();
void _deferredReset(unsigned long delay, uint8_t reason);
uint8_t _getSystemStabilityCounter();
void _setSystemStabilityCounter(uint8_t counter);
uint8_t _getSystemDropoutCounter();
void _setSystemDropoutCounter(uint8_t counter);
void _increaseSystemDropoutCounter();
void _setSystemResetReason(uint8_t reason);
uint8_t _getCustomResetReason();
void _setCustomResetReason(uint8_t reason);
uint8_t _getSystemResetReason();
void _setSystemBootStatus(uint8_t status);
bool _systemStable;
void _bootupSequence();
bool _getSystemCheck();
void _systemCheckLoop();
void _setSystemCheck(bool stable);
// load average (0..100) and heap ram
void _calculateLoad();
uint32_t _load_average;
uint32_t _getInitialFreeHeap();
uint32_t _getUsedHeap();
// web
web_callback_f _web_callback_f;
const char * _http_username;
// web
void _onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t * data, size_t len);
void _procMsg(AsyncWebSocketClient * client, size_t sz);
void _sendStatus();
void _sendCustomStatus();
void _printScanResult(int networksFound);
void _sendTime();
void _webserver_setup();
// ntp
char * _ntp_server;
uint16_t _ntp_interval;
bool _ntp_enabled;
uint8_t _ntp_timezone;
bool _have_ntp_time;
};
extern MyESP myESP;
#endif

View File

@@ -1,129 +0,0 @@
/*
* Ntp.cpp
*/
#include "Ntp.h"
#include "MyESP.h"
char * NtpClient::TimeServerName;
Timezone * NtpClient::tz;
TimeChangeRule * NtpClient::tcr;
time_t NtpClient::syncInterval;
IPAddress NtpClient::timeServer;
AsyncUDP NtpClient::udpListener;
byte NtpClient::NTPpacket[NTP_PACKET_SIZE];
// references:
// https://github.com/filipdanic/compact-timezone-list/blob/master/index.js
// https://github.com/sanohin/google-timezones-json/blob/master/timezones.json
// https://github.com/dmfilipenko/timezones.json/blob/master/timezones.json
// https://home.kpn.nl/vanadovv/time/TZworld.html
// https://www.timeanddate.com/time/zones/
// Australia Eastern Time Zone (Sydney, Melbourne)
TimeChangeRule aEDT = {"AEDT", First, Sun, Oct, 2, 660}; // UTC + 11 hours
TimeChangeRule aEST = {"AEST", First, Sun, Apr, 3, 600}; // UTC + 10 hours
Timezone ausET(aEDT, aEST);
// Moscow Standard Time (MSK, does not observe DST)
TimeChangeRule msk = {"MSK", Last, Sun, Mar, 1, 180};
Timezone MSK(msk);
// Turkey
TimeChangeRule trt = {"TRT", Last, Sun, Mar, 1, 180};
Timezone TRT(trt);
// Central European Time (Frankfurt, Paris)
TimeChangeRule CEST = {"CEST", Last, Sun, Mar, 2, 120}; // Central European Summer Time
TimeChangeRule CET = {"CET ", Last, Sun, Oct, 3, 60}; // Central European Standard Time
Timezone CE(CEST, CET);
// United Kingdom (London, Belfast)
TimeChangeRule BST = {"BST", Last, Sun, Mar, 1, 60}; // British Summer Time
TimeChangeRule GMT = {"GMT", Last, Sun, Oct, 2, 0}; // Standard Time
Timezone UK(BST, GMT);
// UTC
TimeChangeRule utcRule = {"UTC", Last, Sun, Mar, 1, 0}; // UTC
Timezone UTC(utcRule);
// US Eastern Time Zone (New York, Detroit)
TimeChangeRule usEDT = {"EDT", Second, Sun, Mar, 2, -240}; // Eastern Daylight Time = UTC - 4 hours
TimeChangeRule usEST = {"EST", First, Sun, Nov, 2, -300}; // Eastern Standard Time = UTC - 5 hours
Timezone usET(usEDT, usEST);
// US Central Time Zone (Chicago, Houston)
TimeChangeRule usCDT = {"CDT", Second, Sun, Mar, 2, -300};
TimeChangeRule usCST = {"CST", First, Sun, Nov, 2, -360};
Timezone usCT(usCDT, usCST);
// US Mountain Time Zone (Denver, Salt Lake City)
TimeChangeRule usMDT = {"MDT", Second, Sun, Mar, 2, -360};
TimeChangeRule usMST = {"MST", First, Sun, Nov, 2, -420};
Timezone usMT(usMDT, usMST);
// Arizona is US Mountain Time Zone but does not use DST
Timezone usAZ(usMST);
// US Pacific Time Zone (Las Vegas, Los Angeles)
TimeChangeRule usPDT = {"PDT", Second, Sun, Mar, 2, -420};
TimeChangeRule usPST = {"PST", First, Sun, Nov, 2, -480};
Timezone usPT(usPDT, usPST);
// build index of all timezones
Timezone * timezones[] = {&ausET, &MSK, &CE, &UK, &UTC, &usET, &usCT, &usMT, &usAZ, &usPT, &TRT};
void ICACHE_FLASH_ATTR NtpClient::Ntp(const char * server, time_t syncMins, uint8_t tz_index) {
TimeServerName = strdup(server);
syncInterval = syncMins * 60; // convert to seconds
// check for out of bounds
if (tz_index >= NTP_TIMEZONE_MAX) {
tz_index = NTP_TIMEZONE_DEFAULT;
}
tz = timezones[tz_index]; // set timezone
WiFi.hostByName(TimeServerName, timeServer);
setSyncProvider(getNtpTime);
setSyncInterval(syncInterval);
}
ICACHE_FLASH_ATTR NtpClient::~NtpClient() {
udpListener.close();
}
// send an NTP request to the time server at the given address
time_t ICACHE_FLASH_ATTR NtpClient::getNtpTime() {
memset(NTPpacket, 0, sizeof(NTPpacket));
NTPpacket[0] = 0b11100011;
NTPpacket[1] = 0;
NTPpacket[2] = 6;
NTPpacket[3] = 0xEC;
NTPpacket[12] = 49;
NTPpacket[13] = 0x4E;
NTPpacket[14] = 49;
NTPpacket[15] = 52;
if (udpListener.connect(timeServer, 123)) {
udpListener.onPacket([](AsyncUDPPacket packet) {
unsigned long highWord = word(packet.data()[40], packet.data()[41]);
unsigned long lowWord = word(packet.data()[42], packet.data()[43]);
time_t UnixUTCtime = (highWord << 16 | lowWord) - 2208988800UL;
time_t adjustedtime = (*tz).toLocal(UnixUTCtime, &tcr);
myESP.myDebug_P(PSTR("[NTP] Internet time: %02d:%02d:%02d UTC on %d/%d. Local time: %02d:%02d:%02d %s"),
to_hour(UnixUTCtime),
to_minute(UnixUTCtime),
to_second(UnixUTCtime),
to_day(UnixUTCtime),
to_month(UnixUTCtime),
to_hour(adjustedtime),
to_minute(adjustedtime),
to_second(adjustedtime),
tcr->abbrev);
setTime(adjustedtime);
});
}
udpListener.write(NTPpacket, sizeof(NTPpacket));
return 0;
}

View File

@@ -1,39 +0,0 @@
/*
* Ntp.h
*
*/
#pragma once
#ifndef NTP_H_
#define NTP_H_
#include <ESP8266WiFi.h>
#include <ESPAsyncUDP.h>
#include "TimeLib.h" // customized version of the Time library
#include "Timezone.h"
#define NTP_PACKET_SIZE 48 // NTP time is in the first 48 bytes of the message
#define NTP_INTERVAL_DEFAULT 720 // every 12 hours
#define NTP_TIMEZONE_DEFAULT 2 // CE
#define NTP_TIMEZONE_MAX 11
class NtpClient {
public:
void ICACHE_FLASH_ATTR Ntp(const char * server, time_t syncMins, uint8_t tz_index);
ICACHE_FLASH_ATTR virtual ~NtpClient();
static char * TimeServerName;
static IPAddress timeServer;
static time_t syncInterval;
static Timezone * tz;
static TimeChangeRule * tcr;
static AsyncUDP udpListener;
static byte NTPpacket[NTP_PACKET_SIZE];
static ICACHE_FLASH_ATTR time_t getNtpTime();
};
#endif

View File

@@ -1,632 +0,0 @@
/*
* TELNET SERVER FOR ESP8266 / ESP32
* Cloning the serial port via Telnet.
*
* Written by Wolfgang Mattis (arduino@yasheena.de).
* Version 1.1 / September 7, 2018.
* MIT license, all text above must be included in any redistribution.
*/
#ifdef ESP8266
extern "C" {
#include "user_interface.h"
}
#endif
#include "TelnetSpy.h"
#ifndef min
#define min(a, b) ((a) < (b) ? (a) : (b))
#endif
#ifndef max
#define max(a, b) ((a) > (b) ? (a) : (b))
#endif
static TelnetSpy * actualObject = NULL;
TelnetSpy::TelnetSpy() {
port = TELNETSPY_PORT;
telnetServer = NULL;
started = false;
listening = false;
firstMainLoop = true;
usedSer = &Serial;
storeOffline = true;
connected = false;
callbackConnect = NULL;
callbackDisconnect = NULL;
welcomeMsg = strdup(TELNETSPY_WELCOME_MSG);
rejectMsg = strdup(TELNETSPY_REJECT_MSG);
minBlockSize = TELNETSPY_MIN_BLOCK_SIZE;
collectingTime = TELNETSPY_COLLECTING_TIME;
maxBlockSize = TELNETSPY_MAX_BLOCK_SIZE;
pingTime = TELNETSPY_PING_TIME;
pingRef = 0xFFFFFFFF;
waitRef = 0xFFFFFFFF;
telnetBuf = NULL;
bufLen = 0;
uint16_t size = TELNETSPY_BUFFER_LEN;
while (!setBufferSize(size)) {
size = size >> 1;
if (size < minBlockSize) {
setBufferSize(minBlockSize);
break;
}
}
debugOutput = TELNETSPY_CAPTURE_OS_PRINT;
if (debugOutput) {
setDebugOutput(true);
}
}
TelnetSpy::~TelnetSpy() {
end();
}
// added by proddy
void TelnetSpy::disconnectClient() {
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
}
void TelnetSpy::setPort(uint16_t portToUse) {
port = portToUse;
if (listening) {
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
telnetServer->close();
delete telnetServer;
telnetServer = new WiFiServer(port);
if (started) {
telnetServer->begin();
telnetServer->setNoDelay(bufLen > 0);
}
}
}
void TelnetSpy::setWelcomeMsg(const char * msg) {
if (welcomeMsg) {
free(welcomeMsg);
}
welcomeMsg = strdup(msg);
}
void TelnetSpy::setRejectMsg(const char * msg) {
if (rejectMsg) {
free(rejectMsg);
}
rejectMsg = strdup(msg);
}
void TelnetSpy::setMinBlockSize(uint16_t minSize) {
minBlockSize = min(max((uint16_t)1, minSize), maxBlockSize);
}
void TelnetSpy::setCollectingTime(uint16_t colTime) {
collectingTime = colTime;
}
void TelnetSpy::setMaxBlockSize(uint16_t maxSize) {
maxBlockSize = max(maxSize, minBlockSize);
}
bool TelnetSpy::setBufferSize(uint16_t newSize) {
if (telnetBuf && (bufLen == newSize)) {
return true;
}
if (newSize == 0) {
bufLen = 0;
if (telnetBuf) {
free(telnetBuf);
telnetBuf = NULL;
}
if (telnetServer) {
telnetServer->setNoDelay(false);
}
return true;
}
newSize = max(newSize, minBlockSize);
uint16_t oldBufLen = bufLen;
bufLen = newSize;
uint16_t tmp;
if (!telnetBuf || (bufUsed == 0)) {
bufRdIdx = 0;
bufWrIdx = 0;
bufUsed = 0;
} else {
if (bufLen < oldBufLen) {
if (bufRdIdx < bufWrIdx) {
if (bufWrIdx > bufLen) {
tmp = min(bufLen, (uint16_t)(bufWrIdx - max(bufLen, bufRdIdx)));
memcpy(telnetBuf, &telnetBuf[bufWrIdx - tmp], tmp);
bufWrIdx = tmp;
if (bufWrIdx > bufRdIdx) {
bufRdIdx = bufWrIdx;
} else {
if (bufRdIdx > bufLen) {
bufRdIdx = 0;
}
}
if (bufRdIdx == bufWrIdx) {
bufUsed = bufLen;
} else {
bufUsed = bufWrIdx - bufRdIdx;
}
}
} else {
if (bufWrIdx > bufLen) {
memcpy(telnetBuf, &telnetBuf[bufWrIdx - bufLen], bufLen);
bufRdIdx = 0;
bufWrIdx = 0;
bufUsed = bufLen;
} else {
tmp = min(bufLen - bufWrIdx, oldBufLen - bufRdIdx);
memcpy(&telnetBuf[bufLen - tmp], &telnetBuf[oldBufLen - tmp], tmp);
bufRdIdx = bufLen - tmp;
bufUsed = bufWrIdx + tmp;
}
}
}
}
char * temp = (char *)realloc(telnetBuf, bufLen);
if (!temp) {
return false;
}
telnetBuf = temp;
if (telnetBuf && (bufLen > oldBufLen) && (bufRdIdx > bufWrIdx)) {
tmp = bufLen - (oldBufLen - bufRdIdx);
memcpy(&telnetBuf[tmp], &telnetBuf[bufRdIdx], oldBufLen - bufRdIdx);
bufRdIdx = tmp;
}
if (telnetServer) {
telnetServer->setNoDelay(true);
}
return true;
}
uint16_t TelnetSpy::getBufferSize() {
if (!telnetBuf) {
return 0;
}
return bufLen;
}
void TelnetSpy::setStoreOffline(bool store) {
storeOffline = store;
}
bool TelnetSpy::getStoreOffline() {
return storeOffline;
}
void TelnetSpy::setPingTime(uint16_t pngTime) {
pingTime = pngTime;
if (pingTime == 0) {
pingRef = 0xFFFFFFFF;
} else {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
}
}
void TelnetSpy::setSerial(HardwareSerial * usedSerial) {
usedSer = usedSerial;
}
size_t TelnetSpy::write(uint8_t data) {
if (telnetBuf) {
if (storeOffline || client.connected()) {
if (bufUsed == bufLen) {
if (client.connected()) {
sendBlock();
}
if (bufUsed == bufLen) {
while (bufUsed > 0) {
char c = pullTelnetBuf();
if (c == '\n') {
addTelnetBuf('\r');
break;
}
}
if (peekTelnetBuf() == '\r') {
pullTelnetBuf();
}
}
}
addTelnetBuf(data);
/*
if (data == '\n') {
addTelnetBuf('\r'); // added by proddy, fix for Windows
}
*/
}
} else {
if (client.connected()) {
client.write(data);
}
}
if (usedSer) {
return usedSer->write(data);
}
return 1;
}
// this still needs some work
bool TelnetSpy::isSerialAvailable(void) {
if (usedSer) {
usedSer->write("test");
//Wait for four seconds or till data is available on serial, whichever occurs first.
while (usedSer->available() == 0 && millis() < 4000)
;
if (usedSer->available() > 0) {
(void)usedSer->read(); // We then clear the input buffer
return true;
}
}
return false;
}
int TelnetSpy::available(void) {
if (usedSer) {
int avail = usedSer->available();
if (avail > 0) {
return avail;
}
}
if (client.connected()) {
return telnetAvailable();
}
return 0;
}
int TelnetSpy::read(void) {
int val = 0;
if (usedSer) {
val = usedSer->read();
if (val != -1) {
return val;
}
}
if (client.connected()) {
if (telnetAvailable()) {
val = client.read();
}
}
return val;
}
int TelnetSpy::peek(void) {
int val = 0;
if (usedSer) {
val = usedSer->peek();
if (val != -1) {
return val;
}
}
if (client.connected()) {
if (telnetAvailable()) {
val = client.peek();
}
}
return val;
}
void TelnetSpy::flush(void) {
if (usedSer) {
usedSer->flush();
}
}
#ifdef ESP8266
void TelnetSpy::begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin) {
if (usedSer) {
usedSer->begin(baud, config, mode, tx_pin);
}
started = true;
}
#else // ESP32
void TelnetSpy::begin(unsigned long baud, uint32_t config, int8_t rxPin, int8_t txPin, bool invert) {
if (usedSer) {
usedSer->begin(baud, config, rxPin, txPin, invert);
}
started = true;
}
#endif
void TelnetSpy::end() {
if (debugOutput) {
setDebugOutput(false);
}
if (usedSer) {
usedSer->end();
}
if (client.connected()) {
client.flush();
client.stop();
}
if (connected && (callbackDisconnect != NULL)) {
callbackDisconnect();
}
connected = false;
telnetServer->close();
delete telnetServer;
telnetServer = NULL;
listening = false;
started = false;
}
#ifdef ESP8266
void TelnetSpy::swap(uint8_t tx_pin) {
if (usedSer) {
usedSer->swap(tx_pin);
}
}
void TelnetSpy::set_tx(uint8_t tx_pin) {
if (usedSer) {
usedSer->set_tx(tx_pin);
}
}
void TelnetSpy::pins(uint8_t tx, uint8_t rx) {
if (usedSer) {
usedSer->pins(tx, rx);
}
}
bool TelnetSpy::isTxEnabled(void) {
if (usedSer) {
return usedSer->isTxEnabled();
}
return true;
}
bool TelnetSpy::isRxEnabled(void) {
if (usedSer) {
return usedSer->isRxEnabled();
}
return true;
}
#endif
int TelnetSpy::availableForWrite(void) {
if (usedSer) {
return min(usedSer->availableForWrite(), bufLen - bufUsed);
}
return bufLen - bufUsed;
}
TelnetSpy::operator bool() const {
if (usedSer) {
return (bool)*usedSer;
}
return true;
}
void TelnetSpy::setDebugOutput(bool en) {
debugOutput = en;
if (debugOutput) {
actualObject = this;
} else {
if (actualObject == this) {
actualObject = NULL;
}
}
}
uint32_t TelnetSpy::baudRate(void) {
if (usedSer) {
return usedSer->baudRate();
}
return 115200;
}
void TelnetSpy::sendBlock() {
uint16_t len = bufUsed;
if (len > maxBlockSize) {
len = maxBlockSize;
}
len = min(len, (uint16_t)(bufLen - bufRdIdx));
client.write(&telnetBuf[bufRdIdx], len);
bufRdIdx += len;
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
bufUsed -= len;
if (bufUsed == 0) {
bufRdIdx = 0;
bufWrIdx = 0;
}
waitRef = 0xFFFFFFFF;
if (pingRef != 0xFFFFFFFF) {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
if (pingRef > 0x7FFFFFFF) {
pingRef -= 0x80000000;
}
}
}
void TelnetSpy::addTelnetBuf(char c) {
telnetBuf[bufWrIdx] = c;
if (bufUsed == bufLen) {
bufRdIdx++;
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
} else {
bufUsed++;
}
bufWrIdx++;
if (bufWrIdx >= bufLen) {
bufWrIdx = 0;
}
}
char TelnetSpy::pullTelnetBuf() {
if (bufUsed == 0) {
return 0;
}
char c = telnetBuf[bufRdIdx++];
if (bufRdIdx >= bufLen) {
bufRdIdx = 0;
}
bufUsed--;
return c;
}
char TelnetSpy::peekTelnetBuf() {
if (bufUsed == 0) {
return 0;
}
return telnetBuf[bufRdIdx];
}
int TelnetSpy::telnetAvailable() {
int n = client.available();
while (n > 0) {
if (0xff == client.peek()) { // If esc char for telnet NVT protocol data remove that telegram:
client.read(); // Remove esc char
n--;
if (0xff == client.peek()) { // If esc sequence for 0xFF data byte...
return n; // ...return info about available data (just this 0xFF data byte)
}
client.read(); // Skip the rest of the telegram of the telnet NVT protocol data
client.read();
n--;
n--;
} else { // If next char is a normal data byte...
return n; // ...return info about available data
}
}
return 0;
}
bool TelnetSpy::isClientConnected() {
return connected;
}
void TelnetSpy::setCallbackOnConnect(telnetSpyCallback callback) {
callbackConnect = callback;
}
void TelnetSpy::setCallbackOnDisconnect(telnetSpyCallback callback) {
callbackDisconnect = callback;
}
void TelnetSpy::serialPrint(char c) {
if (usedSer) {
usedSer->print(c);
}
}
void TelnetSpy::handle() {
if (firstMainLoop) {
firstMainLoop = false;
// Between setup() and loop() the configuration for os_print may be changed so it must be renewed
if (debugOutput && (actualObject == this)) {
setDebugOutput(true);
}
}
if (!started) {
return;
}
if (!listening) {
if ((WiFi.status() == WL_DISCONNECTED) && (WiFi.getMode() & WIFI_AP)) {
if (usedSer) {
usedSer->println("[TELNET] in AP mode"); // added by Proddy
}
} else if (WiFi.status() != WL_CONNECTED) {
return;
}
telnetServer = new WiFiServer(port);
telnetServer->begin();
telnetServer->setNoDelay(bufLen > 0);
listening = true;
if (usedSer) {
usedSer->println("[TELNET] Telnet server started"); // added by Proddy
}
}
if (telnetServer->hasClient()) {
if (client.connected()) {
WiFiClient rejectClient = telnetServer->available();
if (strlen(rejectMsg) > 0) {
rejectClient.write((const uint8_t *)rejectMsg, strlen(rejectMsg));
}
rejectClient.flush();
rejectClient.stop();
} else {
client = telnetServer->available();
if (strlen(welcomeMsg) > 0) {
client.write((const uint8_t *)welcomeMsg, strlen(welcomeMsg));
}
}
}
if (client.connected()) {
if (!connected) {
connected = true;
if (pingTime != 0) {
pingRef = (millis() & 0x7FFFFFF) + pingTime;
}
if (callbackConnect != NULL) {
callbackConnect();
}
}
} else {
if (connected) {
connected = false;
client.flush();
client.stop();
pingRef = 0xFFFFFFFF;
waitRef = 0xFFFFFFFF;
if (callbackDisconnect != NULL) {
callbackDisconnect();
}
}
}
if (client.connected() && (bufUsed > 0)) {
if (bufUsed >= minBlockSize) {
sendBlock();
} else {
unsigned long m = millis() & 0x7FFFFFF;
if (waitRef == 0xFFFFFFFF) {
waitRef = m + collectingTime;
if (waitRef > 0x7FFFFFFF) {
waitRef -= 0x80000000;
}
} else {
if (!((waitRef < 0x20000000) && (m > 0x60000000)) && (m >= waitRef)) {
sendBlock();
}
}
}
}
if (client.connected() && (pingRef != 0xFFFFFFFF)) {
unsigned long m = millis() & 0x7FFFFFF;
if (!((pingRef < 0x20000000) && (m > 0x60000000)) && (m >= pingRef)) {
addTelnetBuf(0);
sendBlock();
}
}
}

View File

@@ -1,281 +0,0 @@
/*
* TELNET SERVER FOR ESP8266 / ESP32
* Cloning the serial port via Telnet.
*
* Written by Wolfgang Mattis (arduino@yasheena.de).
* Version 1.1 / September 7, 2018.
* MIT license, all text above must be included in any redistribution.
*/
/*
* DESCRIPTION
*
* This module allows you "Debugging over the air". So if you already use
* ArduinoOTA this is a helpful extension for wireless development. Use
* "TelnetSpy" instead of "Serial" to send data to the serial port and a copy
* to a telnet connection. There is a circular buffer which allows to store the
* data while the telnet connection is not established. So its possible to
* collect data even when the Wifi and Telnet connections are still not
* established. Its also possible to create a telnet session only if it is
* neccessary: then you will get the already collected data as far as it is
* still stored in the circular buffer. Data send from telnet terminal to
* ESP8266 / ESP32 will be handled as data received by serial port. It is also
* possible to use more than one instance of TelnetSpy, for example to send
* control information on the first instance and data dumps on the second
* instance.
*
* USAGE
*
* Add the following line to your sketch:
* #include <TelnetSpy.h>
* TelnetSpy LOG;
*
* Add the following line to your initialisation block ( void setup() ):
* LOG.begin();
*
* Add the following line at the beginning of your main loop ( void loop() ):
* LOG.handle();
*
* Use the following functions of the TelnetSpy object to modify behavior
*
* Change the port number of this telnet server. If a client is already
* connected it will be disconnected.
* Default: 23
* void setPort(uint16_t portToUse);
*
* Change the message which will be send to the telnet client after a session
* is established.
* Default: "Connection established via TelnetSpy.\n"
* void setWelcomeMsg(char* msg);
*
* Change the message which will be send to the telnet client if another
* session is already established.
* Default: "TelnetSpy: Only one connection possible.\n"
* void setRejectMsg(char* msg);
*
* Change the amount of characters to collect before sending a telnet block.
* Default: 64
* void setMinBlockSize(uint16_t minSize);
*
* Change the time (in ms) to wait before sending a telnet block if its size is
* less than <minSize> (defined by setMinBlockSize).
* Default: 100
* void setCollectingTime(uint16_t colTime);
*
* Change the maximum size of the telnet packets to send.
* Default: 512
* void setMaxBlockSize(uint16_t maxSize);
*
* Change the size of the ring buffer. Set it to 0 to disable buffering.
* Changing size tries to preserve the already collected data. If the new
* buffer size is too small the youngest data will be preserved only. Returns
* false if the requested buffer size cannot be set.
* Default: 3000
* bool setBufferSize(uint16_t newSize);
*
* This function returns the actual size of the ring buffer.
* uint16_t getBufferSize();
*
* Enable / disable storing new data in the ring buffer if no telnet connection
* is established. This function allows you to store important data only. You
* can do this by disabling "storeOffline" for sending less important data.
* Default: true
* void setStoreOffline(bool store);
*
* Get actual state of storing data when offline.
* bool getStoreOffline();
*
* If no data is sent via TelnetSpy the detection of a disconnected client has
* a long timeout. Use setPingTime to define the time (in ms) without traffic
* after which a ping (chr(0)) is sent to the telnet client to detect a
* disconnect earlier. Use 0 as parameter to disable pings.
* Default: 1500
* void setPingTime(uint16_t pngTime);
*
* Set the serial port you want to use with this object (especially for ESP32)
* or NULL if no serial port should be used (telnet only).
* Default: Serial
* void setSerial(HardwareSerial* usedSerial);
*
* This function returns true, if a telnet client is connected.
* bool isClientConnected();
*
* This function installs a callback function which will be called on every
* telnet connect of this object (except rejected connect tries). Use NULL to
* remove the callback.
* Default: NULL
* void setCallbackOnConnect(void (*callback)());
*
* This function installs a callback function which will be called on every
* telnet disconnect of this object (except rejected connect tries). Use NULL
* to remove the callback.
* Default: NULL
* void setCallbackOnDisconnect(void (*callback)());
*
* HINT
*
* Add the following lines to your sketch:
* #define SERIAL TelnetSpy
* //#define SERIAL Serial
*
* Replace "Serial" with "SERIAL" in your sketch. Now you can switch between
* serial only and serial with telnet by changing the comments of the defines
* only.
*
* IMPORTANT
*
* To connect to the telnet server you have to:
* - establish the Wifi connection
* - execute "TelnetSpy.begin(WhatEverYouWant);"
*
* The order is not important.
*
* All you do with "Serial" you can also do with "TelnetSpy", but remember:
* Transfering data also via telnet will need more performance than the serial
* port only. So time critical things may be influenced.
*
* It is not possible to establish more than one telnet connection at the same
* time. But its possible to use more than one instance of TelnetSpy.
*
* If you have problems with low memory you may reduce the value of the define
* TELNETSPY_BUFFER_LEN for a smaller ring buffer on initialisation.
*
* Usage of void setDebugOutput(bool) to enable / disable of capturing of
* os_print calls when you have more than one TelnetSpy instance: That
* TelnetSpy object will handle this functionality where you used
* setDebugOutput at last. On default TelnetSpy has the capturing of OS_print
* calls enabled. So if you have more instances the last created instance will
* handle the capturing.
*/
#ifndef TelnetSpy_h
#define TelnetSpy_h
#define TELNETSPY_BUFFER_LEN 1000 // was 3000
#define TELNETSPY_MIN_BLOCK_SIZE 64
#define TELNETSPY_COLLECTING_TIME 100
#define TELNETSPY_MAX_BLOCK_SIZE 512
#define TELNETSPY_PING_TIME 1500
#define TELNETSPY_PORT 23
#define TELNETSPY_CAPTURE_OS_PRINT false
#define TELNETSPY_WELCOME_MSG "Connection established via Telnet.\n"
#define TELNETSPY_REJECT_MSG "Telnet: Only one connection possible.\n"
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else // ESP32
#include <WiFi.h>
#endif
#include <WiFiClient.h>
class TelnetSpy : public Stream {
public:
TelnetSpy();
~TelnetSpy();
void handle(void);
void setPort(uint16_t portToUse);
void setWelcomeMsg(const char * msg);
void setRejectMsg(const char * msg);
void setMinBlockSize(uint16_t minSize);
void setCollectingTime(uint16_t colTime);
void setMaxBlockSize(uint16_t maxSize);
bool setBufferSize(uint16_t newSize);
uint16_t getBufferSize();
void setStoreOffline(bool store);
bool getStoreOffline();
void setPingTime(uint16_t pngTime);
void setSerial(HardwareSerial * usedSerial);
bool isClientConnected();
void serialPrint(char c);
void disconnectClient(); // added by Proddy
typedef std::function<void()> telnetSpyCallback; // added by Proddy
void setCallbackOnConnect(telnetSpyCallback callback); // changed by proddy
void setCallbackOnDisconnect(telnetSpyCallback callback); // changed by proddy
// Functions offered by HardwareSerial class:
#ifdef ESP8266
void begin(unsigned long baud) {
begin(baud, SERIAL_8N1, SERIAL_FULL, 1);
}
void begin(unsigned long baud, SerialConfig config) {
begin(baud, config, SERIAL_FULL, 1);
}
void begin(unsigned long baud, SerialConfig config, SerialMode mode) {
begin(baud, config, mode, 1);
}
void begin(unsigned long baud, SerialConfig config, SerialMode mode, uint8_t tx_pin);
#else // ESP32
void begin(unsigned long baud, uint32_t config = SERIAL_8N1, int8_t rxPin = -1, int8_t txPin = -1, bool invert = false);
#endif
void end();
#ifdef ESP8266
void swap() {
swap(1);
}
void swap(uint8_t tx_pin);
void set_tx(uint8_t tx_pin);
void pins(uint8_t tx, uint8_t rx);
bool isTxEnabled(void);
bool isRxEnabled(void);
#endif
int available(void) override;
int peek(void) override;
int read(void) override;
int availableForWrite(void);
void flush(void) override;
size_t write(uint8_t) override;
inline size_t write(unsigned long n) {
return write((uint8_t)n);
}
inline size_t write(long n) {
return write((uint8_t)n);
}
inline size_t write(unsigned int n) {
return write((uint8_t)n);
}
inline size_t write(int n) {
return write((uint8_t)n);
}
using Print::write;
operator bool() const;
void setDebugOutput(bool);
uint32_t baudRate(void);
bool isSerialAvailable(void);
protected:
void sendBlock(void);
void addTelnetBuf(char c);
char pullTelnetBuf();
char peekTelnetBuf();
int telnetAvailable();
WiFiServer * telnetServer;
WiFiClient client;
uint16_t port;
HardwareSerial * usedSer;
bool storeOffline;
bool started;
bool listening;
bool firstMainLoop;
unsigned long waitRef;
unsigned long pingRef;
uint16_t pingTime;
char * welcomeMsg;
char * rejectMsg;
uint16_t minBlockSize;
uint16_t collectingTime;
uint16_t maxBlockSize;
bool debugOutput;
char * telnetBuf;
uint16_t bufLen;
uint16_t bufUsed;
uint16_t bufRdIdx;
uint16_t bufWrIdx;
bool connected;
telnetSpyCallback callbackConnect; // added by proddy
telnetSpyCallback callbackDisconnect; // added by proddy
};
#endif

View File

@@ -1,179 +0,0 @@
/*
* customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014
* modified by Paul S https://github.com/PaulStoffregen/Time
*/
#include "TimeLib.h"
static tmElements_t tm; // a cache of time elements
static time_t cacheTime; // the time the cache was updated
static uint32_t syncInterval = 300; // time sync will be attempted after this many seconds
static uint32_t sysTime = 0;
static uint32_t prevMillis = 0;
static uint32_t nextSyncTime = 0;
static timeStatus_t Status = timeNotSet;
getExternalTime getTimePtr; // pointer to external sync function
#define LEAP_YEAR(Y) (((1970 + (Y)) > 0) && !((1970 + (Y)) % 4) && (((1970 + (Y)) % 100) || !((1970 + (Y)) % 400)))
static const uint8_t monthDays[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; // API starts months from 1, this array starts from 0
time_t now() {
// calculate number of seconds passed since last call to now()
while (millis() - prevMillis >= 1000) {
// millis() and prevMillis are both unsigned ints thus the subtraction will always be the absolute value of the difference
sysTime++;
prevMillis += 1000;
}
if (nextSyncTime <= sysTime) {
if (getTimePtr != 0) {
time_t t = getTimePtr();
if (t != 0) {
setTime(t);
} else {
nextSyncTime = sysTime + syncInterval;
Status = (Status == timeNotSet) ? timeNotSet : timeNeedsSync;
}
}
}
return (time_t)sysTime;
}
void setSyncProvider(getExternalTime getTimeFunction) {
getTimePtr = getTimeFunction;
nextSyncTime = sysTime;
now(); // this will sync the clock
}
void setSyncInterval(time_t interval) { // set the number of seconds between re-sync
syncInterval = (uint32_t)interval;
nextSyncTime = sysTime + syncInterval;
}
void breakTime(time_t timeInput, tmElements_t & tm) {
// break the given time_t into time components
// this is a more compact version of the C library localtime function
// note that year is offset from 1970 !!!
uint8_t year;
uint8_t month, monthLength;
uint32_t time;
unsigned long days;
time = (uint32_t)timeInput;
tm.Second = time % 60;
time /= 60; // now it is minutes
tm.Minute = time % 60;
time /= 60; // now it is hours
tm.Hour = time % 24;
time /= 24; // now it is days
tm.Wday = ((time + 4) % 7) + 1; // Sunday is day 1
year = 0;
days = 0;
while ((unsigned)(days += (LEAP_YEAR(year) ? 366 : 365)) <= time) {
year++;
}
tm.Year = year; // year is offset from 1970
days -= LEAP_YEAR(year) ? 366 : 365;
time -= days; // now it is days in this year, starting at 0
month = 0;
monthLength = 0;
for (month = 0; month < 12; month++) {
if (month == 1) { // february
if (LEAP_YEAR(year)) {
monthLength = 29;
} else {
monthLength = 28;
}
} else {
monthLength = monthDays[month];
}
if (time >= monthLength) {
time -= monthLength;
} else {
break;
}
}
tm.Month = month + 1; // jan is month 1
tm.Day = time + 1; // day of month
}
// assemble time elements into time_t
time_t makeTime(const tmElements_t & tm) {
int i;
uint32_t seconds;
// seconds from 1970 till 1 jan 00:00:00 of the given year
seconds = tm.Year * (SECS_PER_DAY * 365);
for (i = 0; i < tm.Year; i++) {
if (LEAP_YEAR(i)) {
seconds += SECS_PER_DAY; // add extra days for leap years
}
}
// add days for this year, months start from 1
for (i = 1; i < tm.Month; i++) {
if ((i == 2) && LEAP_YEAR(tm.Year)) {
seconds += SECS_PER_DAY * 29;
} else {
seconds += SECS_PER_DAY * monthDays[i - 1]; // monthDay array starts from 0
}
}
seconds += (tm.Day - 1) * SECS_PER_DAY;
seconds += tm.Hour * SECS_PER_HOUR;
seconds += tm.Minute * SECS_PER_MIN;
seconds += tm.Second;
return (time_t)seconds;
}
void refreshCache(time_t t) {
if (t != cacheTime) {
breakTime(t, tm);
cacheTime = t;
}
}
uint8_t to_second(time_t t) { // the second for the given time
refreshCache(t);
return tm.Second;
}
uint8_t to_minute(time_t t) { // the minute for the given time
refreshCache(t);
return tm.Minute;
}
uint8_t to_hour(time_t t) { // the hour for the given time
refreshCache(t);
return tm.Hour;
}
uint8_t to_day(time_t t) { // the day for the given time (0-6)
refreshCache(t);
return tm.Day;
}
uint8_t to_month(time_t t) { // the month for the given time
refreshCache(t);
return tm.Month;
}
uint8_t to_weekday(time_t t) {
refreshCache(t);
return tm.Wday;
}
uint16_t to_year(time_t t) { // the year for the given time
refreshCache(t);
return tm.Year + 1970;
}
void setTime(time_t t) {
sysTime = (uint32_t)t;
nextSyncTime = (uint32_t)t + syncInterval;
Status = timeSet;
prevMillis = millis(); // restart counting from now (thanks to Korman for this fix)
}

View File

@@ -1,55 +0,0 @@
/*
* customized version of Time library, originally Copyright (c) Michael Margolis 2009-2014
* modified by Paul S https://github.com/PaulStoffregen/Time
*/
#ifndef _TimeLib_h
#define _TimeLib_h
#include <Arduino.h>
#define SECS_PER_MIN ((time_t)(60UL))
#define SECS_PER_HOUR ((time_t)(3600UL))
#define SECS_PER_DAY ((time_t)(SECS_PER_HOUR * 24UL))
#define tmYearToCalendar(Y) ((Y) + 1970) // full four digit year
// This ugly hack allows us to define C++ overloaded functions, when included
// from within an extern "C", as newlib's sys/stat.h does. Actually it is
// intended to include "time.h" from the C library (on ARM, but AVR does not
// have that file at all). On Mac and Windows, the compiler will find this
// "Time.h" instead of the C library "time.h", so we may cause other weird
// and unpredictable effects by conflicting with the C library header "time.h",
// but at least this hack lets us define C++ functions as intended. Hopefully
// nothing too terrible will result from overriding the C library header?!
extern "C++" {
typedef enum { timeNotSet, timeNeedsSync, timeSet } timeStatus_t;
typedef struct {
uint8_t Second;
uint8_t Minute;
uint8_t Hour;
uint8_t Wday; // day of week, sunday is day 1
uint8_t Day;
uint8_t Month;
uint8_t Year; // offset from 1970;
} tmElements_t;
typedef time_t (*getExternalTime)();
time_t now(); // return the current time as seconds since Jan 1 1970
void setTime(time_t t);
timeStatus_t timeStatus(); // indicates if time has been set and recently synchronized
void setSyncProvider(getExternalTime getTimeFunction); // identify the external time provider
void setSyncInterval(time_t interval); // set the number of seconds between re-sync
time_t makeTime(const tmElements_t & tm); // convert time elements into time_t
uint8_t to_hour(time_t t); // the hour for the given time
uint8_t to_minute(time_t t); // the minute for the given time
uint8_t to_second(time_t t); // the second for the given time
uint8_t to_day(time_t t); // the day for the given time (0-6)
uint8_t to_month(time_t t); // the month for the given time
uint8_t to_weekday(time_t t); // weekday, sunday is day 1
uint16_t to_year(time_t t); // the year for the given time
}
#endif

View File

@@ -1,200 +0,0 @@
/*----------------------------------------------------------------------*
* Arduino Timezone Library *
* Jack Christensen Mar 2012 *
* *
* Stripped down for myESP by Paul Derbyshire *
* *
* Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and *
* licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html *
*----------------------------------------------------------------------*/
#include "Timezone.h"
/*----------------------------------------------------------------------*
* Create a Timezone object from the given time change rules. *
*----------------------------------------------------------------------*/
Timezone::Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart)
: m_dst(dstStart)
, m_std(stdStart) {
initTimeChanges();
}
/*----------------------------------------------------------------------*
* Create a Timezone object for a zone that does not observe *
* daylight time. *
*----------------------------------------------------------------------*/
Timezone::Timezone(TimeChangeRule stdTime)
: m_dst(stdTime)
, m_std(stdTime) {
initTimeChanges();
}
/*----------------------------------------------------------------------*
* Convert the given UTC time to local time, standard or *
* daylight time, as appropriate. *
*----------------------------------------------------------------------*/
time_t Timezone::toLocal(time_t utc) {
// recalculate the time change points if needed
if (to_year(utc) != to_year(m_dstUTC))
calcTimeChanges(to_year(utc));
if (utcIsDST(utc))
return utc + m_dst.offset * SECS_PER_MIN;
else
return utc + m_std.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Convert the given UTC time to local time, standard or *
* daylight time, as appropriate, and return a pointer to the time *
* change rule used to do the conversion. The caller must take care *
* not to alter this rule. *
*----------------------------------------------------------------------*/
time_t Timezone::toLocal(time_t utc, TimeChangeRule ** tcr) {
// recalculate the time change points if needed
if (to_year(utc) != to_year(m_dstUTC))
calcTimeChanges(to_year(utc));
if (utcIsDST(utc)) {
*tcr = &m_dst;
return utc + m_dst.offset * SECS_PER_MIN;
} else {
*tcr = &m_std;
return utc + m_std.offset * SECS_PER_MIN;
}
}
/*----------------------------------------------------------------------*
* Convert the given local time to UTC time. *
* *
* WARNING: *
* This function is provided for completeness, but should seldom be *
* needed and should be used sparingly and carefully. *
* *
* Ambiguous situations occur after the Standard-to-DST and the *
* DST-to-Standard time transitions. When changing to DST, there is *
* one hour of local time that does not exist, since the clock moves *
* forward one hour. Similarly, when changing to standard time, there *
* is one hour of local times that occur twice since the clock moves *
* back one hour. *
* *
* This function does not test whether it is passed an erroneous time *
* value during the Local -> DST transition that does not exist. *
* If passed such a time, an incorrect UTC time value will be returned. *
* *
* If passed a local time value during the DST -> Local transition *
* that occurs twice, it will be treated as the earlier time, i.e. *
* the time that occurs before the transistion. *
* *
* Calling this function with local times during a transition interval *
* should be avoided! *
*----------------------------------------------------------------------*/
time_t Timezone::toUTC(time_t local) {
// recalculate the time change points if needed
if (to_year(local) != to_year(m_dstLoc))
calcTimeChanges(to_year(local));
if (locIsDST(local))
return local - m_dst.offset * SECS_PER_MIN;
else
return local - m_std.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Determine whether the given UTC time_t is within the DST interval *
* or the Standard time interval. *
*----------------------------------------------------------------------*/
bool Timezone::utcIsDST(time_t utc) {
// recalculate the time change points if needed
if (to_year(utc) != to_year(m_dstUTC))
calcTimeChanges(to_year(utc));
if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz
return false;
else if (m_stdUTC > m_dstUTC) // northern hemisphere
return (utc >= m_dstUTC && utc < m_stdUTC);
else // southern hemisphere
return !(utc >= m_stdUTC && utc < m_dstUTC);
}
/*----------------------------------------------------------------------*
* Determine whether the given Local time_t is within the DST interval *
* or the Standard time interval. *
*----------------------------------------------------------------------*/
bool Timezone::locIsDST(time_t local) {
// recalculate the time change points if needed
if (to_year(local) != to_year(m_dstLoc))
calcTimeChanges(to_year(local));
if (m_stdUTC == m_dstUTC) // daylight time not observed in this tz
return false;
else if (m_stdLoc > m_dstLoc) // northern hemisphere
return (local >= m_dstLoc && local < m_stdLoc);
else // southern hemisphere
return !(local >= m_stdLoc && local < m_dstLoc);
}
/*----------------------------------------------------------------------*
* Calculate the DST and standard time change points for the given *
* given year as local and UTC time_t values. *
*----------------------------------------------------------------------*/
void Timezone::calcTimeChanges(int yr) {
m_dstLoc = toTime_t(m_dst, yr);
m_stdLoc = toTime_t(m_std, yr);
m_dstUTC = m_dstLoc - m_std.offset * SECS_PER_MIN;
m_stdUTC = m_stdLoc - m_dst.offset * SECS_PER_MIN;
}
/*----------------------------------------------------------------------*
* Initialize the DST and standard time change points. *
*----------------------------------------------------------------------*/
void Timezone::initTimeChanges() {
m_dstLoc = 0;
m_stdLoc = 0;
m_dstUTC = 0;
m_stdUTC = 0;
}
/*----------------------------------------------------------------------*
* Convert the given time change rule to a time_t value *
* for the given year. *
*----------------------------------------------------------------------*/
time_t Timezone::toTime_t(TimeChangeRule r, int yr) {
uint8_t m = r.month; // temp copies of r.month and r.week
uint8_t w = r.week;
if (w == 0) // is this a "Last week" rule?
{
if (++m > 12) // yes, for "Last", go to the next month
{
m = 1;
++yr;
}
w = 1; // and treat as first week of next month, subtract 7 days later
}
// calculate first day of the month, or for "Last" rules, first day of the next month
tmElements_t tm;
tm.Hour = r.hour;
tm.Minute = 0;
tm.Second = 0;
tm.Day = 1;
tm.Month = m;
tm.Year = yr - 1970;
time_t t = makeTime(tm);
// add offset from the first of the month to r.dow, and offset for the given week
t += ((r.dow - to_weekday(t) + 7) % 7 + (w - 1) * 7) * SECS_PER_DAY;
// back up a week if this is a "Last" rule
if (r.week == 0)
t -= 7 * SECS_PER_DAY;
return t;
}
/*----------------------------------------------------------------------*
* Read or update the daylight and standard time rules from RAM. *
*----------------------------------------------------------------------*/
void Timezone::setRules(TimeChangeRule dstStart, TimeChangeRule stdStart) {
m_dst = dstStart;
m_std = stdStart;
initTimeChanges(); // force calcTimeChanges() at next conversion call
}

View File

@@ -1,53 +0,0 @@
/*----------------------------------------------------------------------*
* Arduino Timezone Library *
* Jack Christensen Mar 2012 *
* *
* Arduino Timezone Library Copyright (C) 2018 by Jack Christensen and *
* licensed under GNU GPL v3.0, https://www.gnu.org/licenses/gpl.html *
*----------------------------------------------------------------------*/
#ifndef TIMEZONE_H_INCLUDED
#define TIMEZONE_H_INCLUDED
#include <Arduino.h>
#include <TimeLib.h>
// convenient constants for TimeChangeRules
enum week_t { Last, First, Second, Third, Fourth };
enum dow_t { Sun = 1, Mon, Tue, Wed, Thu, Fri, Sat };
enum month_t { Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec };
// structure to describe rules for when daylight/summer time begins,
// or when standard time begins.
struct TimeChangeRule {
char abbrev[6]; // five chars max
uint8_t week; // First, Second, Third, Fourth, or Last week of the month
uint8_t dow; // day of week, 1=Sun, 2=Mon, ... 7=Sat
uint8_t month; // 1=Jan, 2=Feb, ... 12=Dec
uint8_t hour; // 0-23
int offset; // offset from UTC in minutes
};
class Timezone {
public:
Timezone(TimeChangeRule dstStart, TimeChangeRule stdStart);
Timezone(TimeChangeRule stdTime);
time_t toLocal(time_t utc);
time_t toLocal(time_t utc, TimeChangeRule ** tcr);
time_t toUTC(time_t local);
bool utcIsDST(time_t utc);
bool locIsDST(time_t local);
void setRules(TimeChangeRule dstStart, TimeChangeRule stdStart);
private:
void calcTimeChanges(int yr);
void initTimeChanges();
time_t toTime_t(TimeChangeRule r, int yr);
TimeChangeRule m_dst; // rule for start of dst or summer time for any year
TimeChangeRule m_std; // rule for start of standard time for any year
time_t m_dstUTC; // dst start for given/current year, given in UTC
time_t m_stdUTC; // std time start for given/current year, given in UTC
time_t m_dstLoc; // dst start for given/current year, given in local time
time_t m_stdLoc; // std time start for given/current year, given in local time
};
#endif

553
src/console.cpp Normal file
View File

@@ -0,0 +1,553 @@
/*
* 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 "console.h"
#include "emsesp.h"
#include "version.h"
#ifdef EMSESP_DEBUG
#include "test/test.h"
#endif
namespace emsesp {
std::shared_ptr<Commands> EMSESPShell::commands = [] {
std::shared_ptr<Commands> commands = std::make_shared<Commands>();
return commands;
}();
static std::shared_ptr<EMSESPShell> shell;
std::vector<bool> EMSESPStreamConsole::ptys_;
#ifndef EMSESP_STANDALONE
uuid::telnet::TelnetService telnet_([](Stream & stream, const IPAddress & addr, uint16_t port) -> std::shared_ptr<uuid::console::Shell> {
return std::make_shared<EMSESPStreamConsole>(stream, addr, port);
});
#endif
EMSESPShell::EMSESPShell()
: Shell() {
}
void EMSESPShell::started() {
logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session opened on console %s"), console_name().c_str());
// set console name
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { console_hostname_ = wifiSettings.hostname.c_str(); });
if (console_hostname_.empty()) {
console_hostname_.resize(16, '\0');
#if defined(ESP8266)
snprintf_P(&console_hostname_[0], console_hostname_.capacity() + 1, PSTR("esp8266"));
#else
snprintf_P(&console_hostname_[0], console_hostname_.capacity() + 1, PSTR("esp32"));
#endif
}
}
void EMSESPShell::stopped() {
if (has_flags(CommandFlags::ADMIN)) {
logger().log(LogLevel::INFO, LogFacility::AUTH, F("Admin session closed on console %s"), console_name().c_str());
}
logger().log(LogLevel::INFO, LogFacility::CONSOLE, F("User session closed on console %s"), console_name().c_str());
// remove all custom contexts
commands->remove_all_commands();
console_commands_loaded_ = false; // make sure they get reloaded next time a console is opened
}
// show welcome banner
void EMSESPShell::display_banner() {
println();
printfln(F("┌──────────────────────────────────────────┐"));
printfln(F("│ %sEMS-ESP version %-10s%s │"), COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_BOLD_OFF);
printfln(F("│ %s%shttps://github.com/proddy/EMS-ESP%s │"), COLOR_BRIGHT_GREEN, COLOR_UNDERLINE, COLOR_RESET);
printfln(F("│ │"));
printfln(F("│ type %shelp%s to show available commands │"), COLOR_UNDERLINE, COLOR_RESET);
printfln(F("└──────────────────────────────────────────┘"));
println();
// load the list of commands
add_console_commands();
// turn off watch
emsesp::EMSESP::watch_id(WATCH_ID_NONE);
}
// pre-loads all the console commands into the MAIN context
// This is only done after a connection is established, to save on Heap memory
void EMSESPShell::add_console_commands() {
// if we already have these commands loaded, stop adding duplicates
// for example when opening multiple serial/telnet sessions
if (console_commands_loaded_) {
return;
}
// just in case, remove everything
// commands->remove_context_commands(ShellContext::MAIN);
commands->remove_all_commands();
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(refresh)},
[&](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();
});
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(show)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.printfln(F("%s%sEMS-ESP version %s%s"), COLOR_BRIGHT_GREEN, COLOR_BOLD_ON, EMSESP_APP_VERSION, COLOR_RESET);
shell.println();
EMSESP::show_device_values(shell);
EMSESP::show_sensor_values(shell);
});
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(show), F_(devices)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { EMSESP::show_devices(shell); });
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(show), F_(ems)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { EMSESP::show_ems(shell); });
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(show), F_(values)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { EMSESP::show_device_values(shell); });
commands->add_command(
ShellContext::MAIN,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(bus_id)},
flash_string_vector{F_(deviceid_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
uint8_t device_id = Helpers::hextoint(arguments.front().c_str());
if ((device_id == 0x0B) || (device_id == 0x0D) || (device_id == 0x0A) || (device_id == 0x0F) || (device_id == 0x12)) {
EMSESP::emsespSettingsService.update(
[&](EMSESPSettings & settings) {
settings.ems_bus_id = device_id;
shell.printfln(F_(bus_id_fmt), settings.ems_bus_id);
return StateUpdateResult::CHANGED;
},
"local");
} else {
shell.println(F("Must be 0B, 0D, 0A, 0F or 12"));
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{
read_flash_string(F("0B")),
read_flash_string(F("0D")),
read_flash_string(F("0A")),
read_flash_string(F("0F")),
read_flash_string(F("12")),
};
});
commands->add_command(ShellContext::MAIN,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(tx_mode)},
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);
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
});
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(scan), F_(devices)},
flash_string_vector{F_(deep_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments.size() == 0) {
EMSESP::send_read_request(EMSdevice::EMS_TYPE_UBADevices, EMSdevice::EMS_DEVICE_ID_BOILER);
} else {
shell.printfln(F("Performing a deep scan by pinging our device library..."));
std::vector<uint8_t> Device_Ids;
Device_Ids.push_back(0x08); // Boilers - 0x08
Device_Ids.push_back(0x38); // HeatPump - 0x38
Device_Ids.push_back(0x30); // Solar Module - 0x30
Device_Ids.push_back(0x09); // Controllers - 0x09
Device_Ids.push_back(0x02); // Connect - 0x02
Device_Ids.push_back(0x48); // Gateway - 0x48
Device_Ids.push_back(0x20); // Mixing Devices - 0x20
Device_Ids.push_back(0x21); // Mixing Devices - 0x21
Device_Ids.push_back(0x22); // Mixing Devices - 0x22
Device_Ids.push_back(0x23); // Mixing Devices - 0x23
Device_Ids.push_back(0x28); // Mixing Devices WW- 0x28
Device_Ids.push_back(0x29); // Mixing Devices WW- 0x29
Device_Ids.push_back(0x10); // Thermostats - 0x10
Device_Ids.push_back(0x17); // Thermostats - 0x17
Device_Ids.push_back(0x18); // Thermostat remote - 0x18
Device_Ids.push_back(0x19); // Thermostat remote - 0x19
Device_Ids.push_back(0x1A); // Thermostat remote - 0x1A
Device_Ids.push_back(0x1B); // Thermostat remote - 0x1B
Device_Ids.push_back(0x11); // Switches - 0x11
// send the read command with Version command
for (const uint8_t device_id : Device_Ids) {
EMSESP::send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
}
}
});
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(set)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
shell.printfln(F_(tx_mode_fmt), settings.tx_mode);
shell.printfln(F_(bus_id_fmt), settings.ems_bus_id);
});
});
/*
* add all the submenu contexts...
*/
// System
commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(system)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
System::console_commands(shell, ShellContext::SYSTEM);
});
// add all the context menus for the connected devices
// this assumes they devices have been detected and pre-registered
EMSESP::add_context_menus();
Console::load_standard_commands(ShellContext::MAIN);
console_commands_loaded_ = true;
}
std::string EMSESPShell::hostname_text() {
return console_hostname_;
}
// remove commands from the current context to save memory before exiting
bool EMSESPShell::exit_context() {
unsigned int current_context = context();
commands->remove_context_commands(current_context);
if (current_context == ShellContext::MAIN) {
Shell::stop();
return true;
}
return Shell::exit_context();
}
// enter a custom context (sub-menu)
void Console::enter_custom_context(Shell & shell, unsigned int context) {
load_standard_commands(context);
// don't go into the new context if it's already the root (to prevent double loading)
if (context != ShellContext::MAIN) {
shell.enter_context(context);
}
}
// each custom context has the common commands like log, help, exit, su etc
void Console::load_standard_commands(unsigned int context) {
#ifdef EMSESP_DEBUG
EMSESPShell::commands->add_command(context,
CommandFlags::ADMIN,
flash_string_vector{F_(test)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
Test::run_test(shell, arguments.front());
});
#endif
EMSESPShell::commands->add_command(
context,
CommandFlags::USER,
flash_string_vector{F_(log)},
flash_string_vector{F_(log_level_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
if (!arguments.empty()) {
uuid::log::Level level;
if (uuid::log::parse_level_lowercase(arguments[0], level)) {
shell.log_level(level);
} else {
shell.printfln(F_(invalid_log_level));
return;
}
}
shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(shell.log_level()));
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> std::vector<std::string> {
return uuid::log::levels_lowercase();
});
EMSESPShell::commands->add_command(context,
CommandFlags::USER,
flash_string_vector{F_(help)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.print_all_available_commands();
});
EMSESPShell::commands->add_command(context,
CommandFlags::USER,
flash_string_vector{F_(exit)},
[=](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
// delete MAIN console stuff first to save memory
EMSESPShell::commands->remove_context_commands(context);
shell.exit_context();
});
EMSESPShell::commands->add_command(context,
CommandFlags::USER,
flash_string_vector{F_(su)},
[=](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
auto become_admin = [](Shell & shell) {
shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, F("Admin session opened on console"));
shell.add_flags(CommandFlags::ADMIN);
};
if (shell.has_flags(CommandFlags::LOCAL)) {
become_admin(shell);
} else {
shell.enter_password(F_(password_prompt), [=](Shell & shell, bool completed, const std::string & password) {
if (completed) {
uint64_t now = uuid::get_uptime_ms();
EMSESP::esp8266React.getSecuritySettingsService()->read([&](SecuritySettings & securitySettings) {
if (securitySettings.jwtSecret = password.c_str()) {
become_admin(shell);
} else {
shell.delay_until(now + INVALID_PASSWORD_DELAY_MS, [](Shell & shell) {
shell.logger().log(LogLevel::NOTICE, LogFacility::AUTH, F("Invalid admin password on console"));
shell.println(F("su: incorrect password"));
});
}
});
}
});
}
});
EMSESPShell::commands->add_command(context,
CommandFlags::USER,
flash_string_vector{F_(send), F_(telegram)},
flash_string_vector{F_(data_mandatory)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
EMSESP::send_raw_telegram(arguments.front().c_str());
});
EMSESPShell::commands->add_command(context,
CommandFlags::USER,
flash_string_vector{F_(watch)},
flash_string_vector{F_(watch_format_mandatory), F_(watchid_optional)},
[](Shell & shell, const std::vector<std::string> & arguments) {
// get raw/pretty
if (arguments[0] == read_flash_string(F_(raw))) {
emsesp::EMSESP::watch(EMSESP::WATCH_RAW); // raw
} else if (arguments[0] == read_flash_string(F_(on))) {
emsesp::EMSESP::watch(EMSESP::WATCH_ON); // on
} else if (arguments[0] == read_flash_string(F_(off))) {
emsesp::EMSESP::watch(EMSESP::WATCH_OFF); // off
} else {
shell.printfln(F_(invalid_watch));
return;
}
uint16_t watch_id;
if (arguments.size() == 2) {
// get the watch_id if its set
watch_id = Helpers::hextoint(arguments[1].c_str());
} else {
watch_id = WATCH_ID_NONE;
}
emsesp::EMSESP::watch_id(watch_id);
uint8_t watch = emsesp::EMSESP::watch();
if (watch == EMSESP::WATCH_OFF) {
shell.printfln(F("Watching telegrams is off"));
return;
}
// if logging is off, the watch won't show anything, show force it back to NOTICE
if (!shell.logger().enabled(Level::NOTICE)) {
shell.log_level(Level::NOTICE);
}
if (watch == EMSESP::WATCH_ON) {
shell.printfln(F("Watching incoming telegrams, displayed in decoded format"));
} else {
shell.printfln(F("Watching incoming telegrams, displayed as raw bytes")); // WATCH_RAW
}
watch_id = emsesp::EMSESP::watch_id();
if (watch_id != WATCH_ID_NONE) {
shell.printfln(F("Filtering only telegrams that match a device ID or telegram type of 0x%02X"), watch_id);
}
});
}
// prompt, change per context
std::string EMSESPShell::context_text() {
switch (static_cast<ShellContext>(context())) {
case ShellContext::MAIN:
return std::string{'/'};
case ShellContext::BOILER:
return std::string{"/boiler"};
case ShellContext::SYSTEM:
return std::string{"/system"};
case ShellContext::THERMOSTAT:
return std::string{"/thermostat"};
default:
return std::string{};
}
}
// when in su (admin) show # as the prompt suffix
std::string EMSESPShell::prompt_suffix() {
if (has_flags(CommandFlags::ADMIN)) {
return std::string{'#'};
} else {
return std::string{'$'};
}
}
void EMSESPShell::end_of_transmission() {
invoke_command(uuid::read_flash_string(F_(exit)));
}
EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, bool local)
: uuid::console::Shell(commands, ShellContext::MAIN, local ? (CommandFlags::USER | CommandFlags::LOCAL) : CommandFlags::USER)
, uuid::console::StreamConsole(stream)
, EMSESPShell()
, name_(uuid::read_flash_string(F("Serial")))
, pty_(std::numeric_limits<size_t>::max())
, addr_()
, port_(0) {
}
EMSESPStreamConsole::EMSESPStreamConsole(Stream & stream, const IPAddress & addr, uint16_t port)
: uuid::console::Shell(commands, ShellContext::MAIN, CommandFlags::USER)
, uuid::console::StreamConsole(stream)
, EMSESPShell()
, addr_(addr)
, port_(port) {
std::vector<char> text(16);
pty_ = 0;
while (pty_ < ptys_.size() && ptys_[pty_])
pty_++;
if (pty_ == ptys_.size()) {
ptys_.push_back(true);
} else {
ptys_[pty_] = true;
}
snprintf_P(text.data(), text.size(), PSTR("pty%u"), pty_);
name_ = text.data();
#ifndef EMSESP_STANDALONE
logger().info(F("Allocated console %s for connection from [%s]:%u"), name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_);
#endif
}
EMSESPStreamConsole::~EMSESPStreamConsole() {
if (pty_ != SIZE_MAX) {
#ifndef EMSESP_STANDALONE
logger().info(F("Shutdown console %s for connection from [%s]:%u"), name_.c_str(), uuid::printable_to_string(addr_).c_str(), port_);
#endif
ptys_[pty_] = false;
ptys_.shrink_to_fit();
EMSESPShell::commands->remove_all_commands();
}
}
std::string EMSESPStreamConsole::console_name() {
return name_;
}
// Start up telnet and logging
void Console::start() {
// if we've detected a boot into safe mode on ESP8266, start the Serial console too
// Serial is always on with the ESP32 as it has 2 UARTs
#if defined(ESP32) || defined(EMSESP_STANDALONE) || defined(EMSESP_FORCE_SERIAL)
serial_console_.begin(SERIAL_CONSOLE_BAUD_RATE);
serial_console_.println();
shell = std::make_shared<EMSESPStreamConsole>(serial_console_, true);
shell->maximum_log_messages(100); // default is 50
shell->start();
shell->log_level(uuid::log::Level::DEBUG); // order is: err, warning, notice, info, debug, trace, all
#endif
#if defined(EMSESP_STANDALONE)
shell->add_flags(CommandFlags::ADMIN);
#endif
// always start the telnet service, except on an ESP8266
// 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
#ifndef EMSESP_STANDALONE
telnet_.start();
telnet_.default_write_timeout(1000); // in ms, socket timeout 1 second
#endif
}
// handles telnet sync and logging to console
void Console::loop() {
uuid::loop();
#ifndef EMSESP_STANDALONE
telnet_.loop();
#endif
#if defined(ESP8266)
if (!EMSuart::sending()) {
Shell::loop_all();
}
#else
Shell::loop_all();
#endif
}
} // namespace emsesp

214
src/console.h Normal file
View File

@@ -0,0 +1,214 @@
/*
* 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 EMSESP_CONSOLE_H
#define EMSESP_CONSOLE_H
#include <Arduino.h>
#include <string>
#include <vector>
#include <uuid/console.h>
#include <uuid/log.h>
#include "helpers.h"
#include "system.h"
#include "mqtt.h"
using uuid::flash_string_vector;
using uuid::read_flash_string;
using uuid::console::Commands;
using uuid::console::Shell;
using uuid::log::Level;
// clang-format off
#define LOG_DEBUG(...) logger_.debug(__VA_ARGS__)
#define LOG_INFO(...) logger_.info(__VA_ARGS__)
#define LOG_TRACE(...) logger_.trace(__VA_ARGS__)
#define LOG_NOTICE(...) logger_.notice(__VA_ARGS__)
#define LOG_WARNING(...) logger_.warning(__VA_ARGS__)
#define LOG_ERROR(...) logger_.err(__VA_ARGS__)
#define MAKE_PSTR(string_name, string_literal) static const char __pstr__##string_name[] __attribute__((__aligned__(sizeof(int)))) PROGMEM = string_literal;
#define MAKE_PSTR_WORD(string_name) MAKE_PSTR(string_name, #string_name)
#define F_(string_name) FPSTR(__pstr__##string_name)
// clang-format on
// common words
#ifdef EMSESP_DEBUG
MAKE_PSTR_WORD(test)
#endif
MAKE_PSTR_WORD(exit)
MAKE_PSTR_WORD(help)
MAKE_PSTR_WORD(settings)
MAKE_PSTR_WORD(log)
MAKE_PSTR_WORD(logout)
MAKE_PSTR_WORD(enabled)
MAKE_PSTR_WORD(disabled)
MAKE_PSTR_WORD(yes)
MAKE_PSTR_WORD(no)
MAKE_PSTR_WORD(set)
MAKE_PSTR_WORD(show)
MAKE_PSTR_WORD(on)
MAKE_PSTR_WORD(off)
MAKE_PSTR_WORD(su)
MAKE_PSTR_WORD(name)
MAKE_PSTR_WORD(auto)
MAKE_PSTR_WORD(scan)
MAKE_PSTR_WORD(password)
MAKE_PSTR_WORD(read)
MAKE_PSTR_WORD(version)
MAKE_PSTR_WORD(values)
MAKE_PSTR_WORD(system)
MAKE_PSTR_WORD(refresh)
MAKE_PSTR_WORD(restart)
MAKE_PSTR_WORD(raw)
MAKE_PSTR_WORD(watch)
MAKE_PSTR_WORD(mqtt)
MAKE_PSTR_WORD(send)
MAKE_PSTR_WORD(telegram)
MAKE_PSTR_WORD(bus_id)
MAKE_PSTR_WORD(tx_mode)
MAKE_PSTR_WORD(ems)
MAKE_PSTR_WORD(devices)
MAKE_PSTR(deep_optional, "[deep]")
MAKE_PSTR(tx_mode_fmt, "Tx mode = %d")
MAKE_PSTR(bus_id_fmt, "Bus ID = %02X")
MAKE_PSTR(watchid_optional, "[ID]")
MAKE_PSTR(watch_format_mandatory, "<off | on | raw>")
MAKE_PSTR(invalid_watch, "Invalid watch type")
MAKE_PSTR(data_mandatory, "<\"XX XX ...\">")
MAKE_PSTR(percent, "%")
MAKE_PSTR(degrees, "°C")
MAKE_PSTR(degrees_mandatory, "<degrees>")
MAKE_PSTR(asterisks, "********")
MAKE_PSTR(n_mandatory, "<n>")
MAKE_PSTR(n_optional, "[n]")
MAKE_PSTR(bool_mandatory, "<on | off>")
MAKE_PSTR(typeid_mandatory, "<type ID>")
MAKE_PSTR(deviceid_mandatory, "<device ID>")
MAKE_PSTR(deviceid_optional, "[device ID]")
MAKE_PSTR(invalid_log_level, "Invalid log level")
MAKE_PSTR(port_mandatory, "<port>")
MAKE_PSTR(log_level_fmt, "Log level = %s")
MAKE_PSTR(log_level_optional, "[level]")
MAKE_PSTR(name_mandatory, "<name>")
MAKE_PSTR(name_optional, "[name]")
MAKE_PSTR(new_password_prompt1, "Enter new password: ")
MAKE_PSTR(new_password_prompt2, "Retype new password: ")
MAKE_PSTR(password_prompt, "Password: ")
MAKE_PSTR(unset, "<unset>")
#ifdef LOCAL
#undef LOCAL
#endif
static constexpr uint32_t INVALID_PASSWORD_DELAY_MS = 3000;
namespace emsesp {
using LogLevel = ::uuid::log::Level;
using LogFacility = ::uuid::log::Facility;
enum CommandFlags : uint8_t {
USER = 0,
ADMIN = (1 << 0),
LOCAL = (1 << 1)
};
enum ShellContext : uint8_t {
MAIN = 0,
SYSTEM,
BOILER,
THERMOSTAT,
SOLAR,
MIXING
};
class EMSESPShell : virtual public uuid::console::Shell {
public:
~EMSESPShell() override = default;
virtual std::string console_name() = 0;
static std::shared_ptr<uuid::console::Commands> commands;
static std::shared_ptr<EMSESPShell> shell;
protected:
EMSESPShell();
// our custom functions for Shell
void started() override;
void stopped() override;
void display_banner() override;
std::string hostname_text() override;
std::string context_text() override;
std::string prompt_suffix() override;
void end_of_transmission() override;
bool exit_context() override;
private:
void add_console_commands();
bool console_commands_loaded_ = false; // set to true when the initial commands are loaded
std::string console_hostname_;
};
class EMSESPStreamConsole : public uuid::console::StreamConsole, public EMSESPShell {
public:
EMSESPStreamConsole(Stream & stream, bool local);
EMSESPStreamConsole(Stream & stream, const IPAddress & addr, uint16_t port);
~EMSESPStreamConsole() override;
std::string console_name() override;
private:
static std::vector<bool> ptys_;
std::string name_;
size_t pty_;
IPAddress addr_;
uint16_t port_;
};
class Console {
public:
void loop();
void start();
uuid::log::Level log_level();
static void enter_custom_context(Shell & shell, unsigned int context);
static void load_standard_commands(unsigned int context);
private:
static constexpr unsigned long SERIAL_CONSOLE_BAUD_RATE = 115200;
static constexpr auto & serial_console_ = Serial;
};
} // namespace emsesp
#endif

View File

@@ -1,261 +0,0 @@
<div id="customcontent">
<br>
<legend>Custom Settings</legend>
<h6 class="text-muted">Please refer to the Help for configuration options</h6>
<br>
<div class="row form-group">
<label class="col-xs-3">LED<i style="margin-left: 10px;"
class="glyphicon glyphicon-exclamation-sign text-danger" aria-hidden="true" data-toggle="popover"
data-trigger="hover" data-placement="right"
data-content="Please choose if you want to enable an LED to show status"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="led">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="led" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">LED Pin<i style="margin-left: 10px;"
class="glyphicon glyphicon-exclamation-sign text-danger" aria-hidden="true" data-toggle="popover"
data-trigger="hover" data-placement="right"
data-content="Select with GPIO pin the LED is on"></i></label>
<span class="col-xs-9 col-md-5">
<select class="form-control input-sm" id="led_gpio">
<option value="0">GPIO-0</option>
<option selected="selected" value="2">GPIO-2 (onboard LED)</option>
<option value="4">GPIO-4</option>
<option value="5">GPIO-5</option>
<option value="12">GPIO-12</option>
<option value="14">GPIO-14</option>
<option value="16">GPIO-16</option>
</select>
</span>
</div>
<div class="row form-group">
<label class="col-xs-3">Dallas Parasite<i style="margin-left: 10px;"
class="glyphicon glyphicon-exclamation-sign text-danger" aria-hidden="true" data-toggle="popover"
data-trigger="hover" data-placement="right"
data-content="Enable if Dallas sensors are powered via parasite"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="dallas_parasite">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="dallas_parasite" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Dallas Pin<i style="margin-left: 10px;"
class="glyphicon glyphicon-exclamation-sign text-danger" aria-hidden="true" data-toggle="popover"
data-trigger="hover" data-placement="right"
data-content="Select GPIO pin to where the Dallas sensor is connected"></i></label>
<span class="col-xs-9 col-md-5">
<select class="form-control input-sm" id="dallas_gpio">
<option value="0">GPIO-0</option>
<option value="4">GPIO-4</option>
<option value="5">GPIO-5</option>
<option value="12">GPIO-12</option>
<option selected="selected" value="14">GPIO-14</option>
<option value="16">GPIO-16</option>
</select>
</span>
</div>
<div class="row form-group">
<label class="col-xs-3">Listen Mode<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Listen mode disables Tx. Used when debugging."></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="listen_mode">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="listen_mode" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Shower Timer<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Monitors and sends MQTT message on shower duration"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="shower_timer">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="shower_timer" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Shower Alert<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Tells boiler to blast 3 shots of cold water after a specific duration has exceeded (7 mins)"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="shower_alert">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="shower_alert" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Publish Time<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="How often to send the MQTT topics in seconds. Must be at least 1"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="0" value="" style="display:inline;max-width:185px"
id="publish_time" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Tx mode<i style="margin-left: 10px;"
class="glyphicon glyphicon-exclamation-sign text-danger" aria-hidden="true" data-toggle="popover"
data-trigger="hover" data-placement="right"
data-content="Tx mode settings for sending data to the EMS bus"></i></label>
<span class="col-xs-9 col-md-5">
<select class="form-control input-sm" id="tx_mode">
<option selected="selected" value="1">1 (EMS generic)</option>
<option value="2">2 (EMS+)</option>
<option value="3">3 (Junkers Heatronic HT3)</option>
</select>
</span>
</div>
<br>
<div class="row form-group">
<div class="col-xs-9 col-md-8">
<button onclick="savecustom()" class="btn btn-primary btn-sm pull-right">Save</button>
</div>
</div>
<h6 class="text-muted">Note: any setting marked with a <span
class="glyphicon glyphicon-exclamation-sign text-danger"></span> requires a system restart after saving.
</h6>
</div>
<div id="custom_statuscontent">
<br>
<div class="row text-left">
<div class="col-md-8 col-md-offset-2">
<h2>EMS Dashboard</h2>
<h6>Real-time values from the EMS-ESP device are shown here</h6>
</div>
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default table-responsive">
<table class="table table-hover table-striped table-condensed">
<caption>EMS Bus Status</caption>
<tr>
<td colspan="2">
<b>
<div id="msg" role="alert"></div>
</b>
</td>
</tr>
<tr>
<th id="devicesshow">Discovered Devices:</th>
<td>
<ul class="list-group">
<div id="devices"></div>
</ul>
</td>
</tr>
</table>
</div>
<div class="panel panel-success table-responsive" id="boiler_show">
<div class="panel-heading"><b>Boiler</b>:&nbsp;<span id="bm"></span></div>
<table class="table table-hover table-bordered table-condensed">
<tr>
<th>Hot Tap Water:</th>
<td id="b1"></td>
<th>Central Heating:</th>
<td id="b2"></td>
</tr>
<tr>
<th>Selected Flow Temperature:</th>
<td id="b3"></td>
<th>Current Flow Temperature:</th>
<td id="b4"></td>
</tr>
<tr>
<th>DHW Temperature:</th>
<td id="b5"></td>
</tr>
</table>
</div>
<div class="panel panel-info table-responsive" id="thermostat_show">
<div class="panel-heading"><b>Thermostat</b>:&nbsp;<span id="tm"></span></div>
<table class="table table-hover table-bordered table-condensed">
<tr>
<th>Setpoint Temperature:</th>
<td id="ts"></td>
<th>Current Temperature:</th>
<td id="tc"></td>
</tr>
<tr>
<th>Mode:</th>
<td colspan="3" id="tmode"></td>
</tr>
</table>
</div>
<div class="panel panel-warning table-responsive" id="sm_show">
<div class="panel-heading"><b>Solar Module</b>:&nbsp;<span id="sm"></span></div>
<table class="table table-hover table-bordered table-condensed">
<tr>
<th>Colector Temperature:</th>
<td id="sm1"></td>
<th>Bottom Temperature:</th>
<td id="sm2"></td>
</tr>
<tr>
<th>Pump Modulation:</th>
<td id="sm3"></td>
<th>Pump Active:</th>
<td id="sm4"></td>
</tr>
<tr>
<th>Energy Last Hour:</th>
<td id="sm5"></td>
<th>Energy Today:</th>
<td id="sm6"></td>
<th>Energy Total:</th>
<td id="sm7"></td>
</tr>
</table>
</div>
<div class="panel panel-success table-responsive" id="hp_show">
<div class="panel-heading"><b>Heat Pump</b>:&nbsp;<span id="hm"></span></div>
<table class="table table-hover table-bordered table-condensed">
<tr>
<th>Pump Modulation:</th>
<td id="hp1"></td>
<th>Pump Speed:</th>
<td id="hp2"></td>
</tr>
</table>
</div>
</div>
</div>
<div class="row form-group" style="text-align: center;">
<button onclick="refreshCustomStatus()" class="btn btn-info">Refresh</button>
</div>
<br>
</div>

View File

@@ -1,167 +0,0 @@
var custom_config = {
"command": "custom_configfile",
"settings": {
"led": true,
"led_gpio": 2,
"dallas_gpio": 14,
"dallas_parasite": false,
"listen_mode": false,
"shower_timer": false,
"shower_alert": false,
"publish_time": 10,
"tx_mode": 1
}
};
function listcustom() {
document.getElementById("led_gpio").value = custom_config.settings.led_gpio;
document.getElementById("dallas_gpio").value = custom_config.settings.dallas_gpio;
document.getElementById("publish_time").value = custom_config.settings.publish_time;
document.getElementById("tx_mode").value = custom_config.settings.tx_mode;
if (custom_config.settings.led) {
$("input[name=\"led\"][value=\"1\"]").prop("checked", true);
}
if (custom_config.settings.dallas_parasite) {
$("input[name=\"dallas_parasite\"][value=\"1\"]").prop("checked", true);
}
if (custom_config.settings.listen_mode) {
$("input[name=\"listen_mode\"][value=\"1\"]").prop("checked", true);
}
if (custom_config.settings.shower_timer) {
$("input[name=\"shower_timer\"][value=\"1\"]").prop("checked", true);
}
if (custom_config.settings.shower_alert) {
$("input[name=\"shower_alert\"][value=\"1\"]").prop("checked", true);
}
}
function savecustom() {
custom_config.settings.led_gpio = parseInt(document.getElementById("led_gpio").value);
custom_config.settings.dallas_gpio = parseInt(document.getElementById("dallas_gpio").value);
custom_config.settings.dallas_parasite = false;
if (parseInt($("input[name=\"dallas_parasite\"]:checked").val()) === 1) {
custom_config.settings.dallas_parasite = true;
}
custom_config.settings.listen_mode = false;
if (parseInt($("input[name=\"listen_mode\"]:checked").val()) === 1) {
custom_config.settings.listen_mode = true;
}
custom_config.settings.shower_timer = false;
if (parseInt($("input[name=\"shower_timer\"]:checked").val()) === 1) {
custom_config.settings.shower_timer = true;
}
custom_config.settings.shower_alert = false;
if (parseInt($("input[name=\"shower_alert\"]:checked").val()) === 1) {
custom_config.settings.shower_alert = true;
}
custom_config.settings.led = false;
if (parseInt($("input[name=\"led\"]:checked").val()) === 1) {
custom_config.settings.led = true;
}
custom_config.settings.publish_time = parseInt(document.getElementById("publish_time").value);
custom_config.settings.tx_mode = parseInt(document.getElementById("tx_mode").value);
custom_saveconfig();
}
function listCustomStats() {
document.getElementById("msg").innerHTML = ajaxobj.emsbus.msg;
if (ajaxobj.emsbus.ok) {
document.getElementById("msg").className = "alert alert-success";
} else {
document.getElementById("msg").className = "alert alert-danger";
document.getElementById("devicesshow").style.display = "none";
document.getElementById("thermostat_show").style.display = "none";
document.getElementById("boiler_show").style.display = "none";
document.getElementById("sm_show").style.display = "none";
document.getElementById("hp_show").style.display = "none";
return;
}
var list = document.getElementById("devices");
var obj = ajaxobj.emsbus.devices;
document.getElementById("devicesshow").style.display = "block";
for (var i = 0; i < obj.length; i++) {
var l = document.createElement("li");
var type = obj[i].type;
var color = "";
if (type === "Boiler") {
color = "list-group-item-success";
} else if (type === "Thermostat") {
color = "list-group-item-info";
} else if (type === "Solar Module") {
color = "list-group-item-warning";
} else if (type === "Heat Pump") {
color = "list-group-item-success";
}
l.innerHTML = obj[i].model + " (DeviceID: 0x" + obj[i].deviceid + ", ProductID: " + obj[i].productid + ", Version: " + obj[i].version + ")";
l.className = "list-group-item " + color;
list.appendChild(l);
}
if (ajaxobj.boiler.ok) {
document.getElementById("boiler_show").style.display = "block";
document.getElementById("bm").innerHTML = ajaxobj.boiler.bm;
document.getElementById("b1").innerHTML = ajaxobj.boiler.b1;
document.getElementById("b2").innerHTML = ajaxobj.boiler.b2;
document.getElementById("b3").innerHTML = ajaxobj.boiler.b3 + " &#8451;";
document.getElementById("b4").innerHTML = ajaxobj.boiler.b4 + " &#8451;";
document.getElementById("b5").innerHTML = ajaxobj.boiler.b5 + " &#8451;";
} else {
document.getElementById("boiler_show").style.display = "none";
}
if (ajaxobj.thermostat.ok) {
document.getElementById("thermostat_show").style.display = "block";
document.getElementById("tm").innerHTML = ajaxobj.thermostat.tm;
document.getElementById("ts").innerHTML = ajaxobj.thermostat.ts + " &#8451;";
document.getElementById("tc").innerHTML = ajaxobj.thermostat.tc + " &#8451;";
document.getElementById("tmode").innerHTML = ajaxobj.thermostat.tmode;
} else {
document.getElementById("thermostat_show").style.display = "none";
}
if (ajaxobj.sm.ok) {
document.getElementById("sm_show").style.display = "block";
document.getElementById("sm").innerHTML = ajaxobj.sm.sm;
document.getElementById("sm1").innerHTML = ajaxobj.sm.sm1 + " &#8451;";
document.getElementById("sm2").innerHTML = ajaxobj.sm.sm2 + " &#8451;";
document.getElementById("sm3").innerHTML = ajaxobj.sm.sm3 + " &#37;";
document.getElementById("sm4").innerHTML = ajaxobj.sm.sm4;
document.getElementById("sm5").innerHTML = ajaxobj.sm.sm5 + " Wh";
document.getElementById("sm6").innerHTML = ajaxobj.sm.sm6 + " Wh";
document.getElementById("sm7").innerHTML = ajaxobj.sm.sm7 + " KWh";
} else {
document.getElementById("sm_show").style.display = "none";
}
if (ajaxobj.hp.ok) {
document.getElementById("hp_show").style.display = "block";
document.getElementById("hm").innerHTML = ajaxobj.hp.hm;
document.getElementById("hp1").innerHTML = ajaxobj.hp.hp1 + " &#37;";
document.getElementById("hp2").innerHTML = ajaxobj.hp.hp2 + " &#37;";
} else {
document.getElementById("hp_show").style.display = "none";
}
}

119
src/device_library.h Normal file
View File

@@ -0,0 +1,119 @@
/*
* 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/>.
*/
// clang-format off
/*
* These is the EMS devices that we currently recognize
* The types and flags are stored in emsdevice.h
*/
// Boilers - 0x08
{ 64, DeviceType::BOILER, F("BK13,BK15/Smartline/GB1x2"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 72, DeviceType::BOILER, F("GB125/MC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 84, DeviceType::BOILER, F("Logamax Plus GB022"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{ 95, DeviceType::BOILER, F("Condens 2500/Logamax/Logomatic/Cerapur Top/Greenstar/Generic HT3"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{115, DeviceType::BOILER, F("Topline/GB162"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{122, DeviceType::BOILER, F("Proline"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{123, DeviceType::BOILER, F("GBx72/Trendline/Cerapur/Greenstar Si/27i"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{133, DeviceType::BOILER, F("GB125/Logamatic MC110"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{170, DeviceType::BOILER, F("Logano GB212"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{172, DeviceType::BOILER, F("Enviline/Compress 6000AW"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{195, DeviceType::BOILER, F("Condens 5000i"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{203, DeviceType::BOILER, F("Logamax U122/Cerapur"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{208, DeviceType::BOILER, F("Logamax plus/GB192/Condens GC9000"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{234, DeviceType::BOILER, F("Logamax Plus GB122"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Thermostat - not currently supporting write operations, like the Easy/100 types - 0x18
{202, DeviceType::THERMOSTAT, F("Logamatic TC100/Moduline Easy"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{203, DeviceType::THERMOSTAT, F("EasyControl CT200"), DeviceFlags::EMS_DEVICE_FLAG_EASY | DeviceFlags::EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
// Thermostat - Common for Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18
{ 67, DeviceType::THERMOSTAT, F("RC30"), DeviceFlags::EMS_DEVICE_FLAG_RC30_1},// 0x10 - based on RC35
{ 77, DeviceType::THERMOSTAT, F("RC20/Moduline 300"), DeviceFlags::EMS_DEVICE_FLAG_RC20},// 0x17
{ 78, DeviceType::THERMOSTAT, F("Moduline 400"), DeviceFlags::EMS_DEVICE_FLAG_RC30}, // 0x10
{ 79, DeviceType::THERMOSTAT, F("RC10/Moduline 100"), DeviceFlags::EMS_DEVICE_FLAG_RC10},// 0x17
{ 80, DeviceType::THERMOSTAT, F("Moduline 200"), DeviceFlags::EMS_DEVICE_FLAG_RC10}, // 0x17
{ 86, DeviceType::THERMOSTAT, F("RC35"), DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{ 93, DeviceType::THERMOSTAT, F("RC20RF"), DeviceFlags::EMS_DEVICE_FLAG_RC20}, // 0x19
{157, DeviceType::THERMOSTAT, F("RC200/CW100"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18
{158, DeviceType::THERMOSTAT, F("RC300/RC310/Moduline 3000/CW400/Sense II"), DeviceFlags::EMS_DEVICE_FLAG_RC300}, // 0x10
{165, DeviceType::THERMOSTAT, F("RC100/Moduline 1000/1010"), DeviceFlags::EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38
// Thermostat - Sieger - 0x10 / 0x17
{ 76, DeviceType::THERMOSTAT, F("ES73"), DeviceFlags::EMS_DEVICE_FLAG_RC35}, // 0x10
{113, DeviceType::THERMOSTAT, F("ES72/RC20"), DeviceFlags::EMS_DEVICE_FLAG_RC20_2}, // 0x17
// Thermostat - Junkers - 0x10
{105, DeviceType::THERMOSTAT, F("FW100"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{106, DeviceType::THERMOSTAT, F("FW200"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{107, DeviceType::THERMOSTAT, F("FR100"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_2}, // older model
{108, DeviceType::THERMOSTAT, F("FR110"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_2}, // older model
{111, DeviceType::THERMOSTAT, F("FR10"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{147, DeviceType::THERMOSTAT, F("FR50"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{191, DeviceType::THERMOSTAT, F("FR120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
{192, DeviceType::THERMOSTAT, F("FW120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
// Solar Modules - 0x30
{ 73, DeviceType::SOLAR, F("SM10"), DeviceFlags::EMS_DEVICE_FLAG_SM10},
{101, DeviceType::SOLAR, F("ISM1"), DeviceFlags::EMS_DEVICE_FLAG_ISM},
{162, DeviceType::SOLAR, F("SM50"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
{163, DeviceType::SOLAR, F("SM100"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
{164, DeviceType::SOLAR, F("SM200"), DeviceFlags::EMS_DEVICE_FLAG_SM100},
// Mixing Modules - 0x20-0x27 for HC, 0x28-0x29 for WWC
{ 69, DeviceType::MIXING, F("MM10"), DeviceFlags::EMS_DEVICE_FLAG_MM10},
{102, DeviceType::MIXING, F("IPM"), DeviceFlags::EMS_DEVICE_FLAG_IPM},
{159, DeviceType::MIXING, F("MM50"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{160, DeviceType::MIXING, F("MM100"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
{161, DeviceType::MIXING, F("MM200"), DeviceFlags::EMS_DEVICE_FLAG_MMPLUS},
// Heat Pumps - 0x38
{200, DeviceType::HEATPUMP, F("HP Module"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{252, DeviceType::HEATPUMP, F("HP Module"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Controllers - 0x09 / 0x10 / 0x50
{ 68, DeviceType::CONTROLLER, F("BC10/RFM20"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{ 89, DeviceType::CONTROLLER, F("BC10 GB142"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{ 95, DeviceType::CONTROLLER, F("HT3"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{114, DeviceType::CONTROLLER, F("BC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{125, DeviceType::CONTROLLER, F("BC25"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{152, DeviceType::CONTROLLER, F("Controller"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{169, DeviceType::CONTROLLER, F("BC40"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{190, DeviceType::CONTROLLER, F("BC10"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{207, DeviceType::CONTROLLER, F("Sense II/CS200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x10
{209, DeviceType::CONTROLLER, F("ErP"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{218, DeviceType::CONTROLLER, F("M200/RFM200"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x50
{224, DeviceType::CONTROLLER, F("9000i"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{230, DeviceType::CONTROLLER, F("BC Base"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
{241, DeviceType::CONTROLLER, F("Condens 5000i"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x09
// Connect devices - 0x02
{171, DeviceType::CONNECT, F("OpenTherm Converter"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{205, DeviceType::CONNECT, F("Moduline Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
{206, DeviceType::CONNECT, F("Easy Connect"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Switches - 0x11
{ 71, DeviceType::SWITCH, F("WM10"), DeviceFlags::EMS_DEVICE_FLAG_NONE},
// Gateways - 0x48 / 0x18
{ 94, DeviceType::GATEWAY, F("RFM20 Remote Base for RC20RF"), DeviceFlags::EMS_DEVICE_FLAG_NONE}, // 0x18
{189, DeviceType::GATEWAY, F("KM200"), DeviceFlags::EMS_DEVICE_FLAG_NONE} // 0x48
// clang-format on

896
src/devices/boiler.cpp Normal file
View File

@@ -0,0 +1,896 @@
/*
* 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 "boiler.h"
MAKE_PSTR_WORD(boiler)
MAKE_PSTR_WORD(wwtemp)
MAKE_PSTR_WORD(flowtemp)
MAKE_PSTR_WORD(wwactive)
MAKE_PSTR_WORD(wwonetime)
MAKE_PSTR_WORD(wwcirculation)
MAKE_PSTR_WORD(comfort)
MAKE_PSTR_WORD(eco)
MAKE_PSTR_WORD(intelligent)
MAKE_PSTR_WORD(hot)
MAKE_PSTR(comfort_mandatory, "<hot | eco | intelligent>")
// shower
MAKE_PSTR_WORD(shower)
MAKE_PSTR_WORD(timer)
MAKE_PSTR_WORD(alert)
MAKE_PSTR(shower_timer_fmt, "Shower Timer is %s")
MAKE_PSTR(shower_alert_fmt, "Shower Alert is %s")
namespace emsesp {
REGISTER_FACTORY(Boiler, EMSdevice::DeviceType::BOILER)
MAKE_PSTR(logger_name, "boiler")
uuid::log::Logger Boiler::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Boiler::Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Registering new Boiler with device ID 0x%02X"), device_id);
// the telegram handlers...
register_telegram_type(0x10, F("UBAErrorMessage1"), false, std::bind(&Boiler::process_UBAErrorMessage, this, _1));
register_telegram_type(0x11, F("UBAErrorMessage2"), false, std::bind(&Boiler::process_UBAErrorMessage, this, _1));
register_telegram_type(0x18, F("UBAMonitorFast"), false, std::bind(&Boiler::process_UBAMonitorFast, this, _1));
register_telegram_type(0x19, F("UBAMonitorSlow"), true, std::bind(&Boiler::process_UBAMonitorSlow, this, _1));
register_telegram_type(0x34, F("UBAMonitorWW"), false, std::bind(&Boiler::process_UBAMonitorWW, this, _1));
register_telegram_type(0x1C, F("UBAMaintenanceStatus"), false, std::bind(&Boiler::process_UBAMaintenanceStatus, this, _1));
register_telegram_type(0x2A, F("MC10Status"), false, std::bind(&Boiler::process_MC10Status, this, _1));
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(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));
register_telegram_type(0xE3, F("UBAMonitorSlowPlus"), false, std::bind(&Boiler::process_UBAMonitorSlowPlus2, this, _1));
register_telegram_type(0xE4, F("UBAMonitorFastPlus"), false, std::bind(&Boiler::process_UBAMonitorFastPlus, this, _1));
register_telegram_type(0xE5, F("UBAMonitorSlowPlus"), false, std::bind(&Boiler::process_UBAMonitorSlowPlus, this, _1));
register_telegram_type(0xE9, F("UBADHWStatus"), false, std::bind(&Boiler::process_UBADHWStatus, this, _1));
// MQTT callbacks
register_mqtt_topic("boiler_cmd", std::bind(&Boiler::boiler_cmd, this, _1));
register_mqtt_topic("boiler_cmd_wwactivated", std::bind(&Boiler::boiler_cmd_wwactivated, this, _1));
register_mqtt_topic("boiler_cmd_wwonetime", std::bind(&Boiler::boiler_cmd_wwonetime, this, _1));
register_mqtt_topic("boiler_cmd_wwcirculation", std::bind(&Boiler::boiler_cmd_wwcirculation, this, _1));
register_mqtt_topic("boiler_cmd_wwtemp", std::bind(&Boiler::boiler_cmd_wwtemp, this, _1));
}
// add submenu context
void Boiler::add_context_menu() {
EMSESPShell::commands->add_command(ShellContext::MAIN,
CommandFlags::USER,
flash_string_vector{F_(boiler)},
[&](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
Boiler::console_commands(shell, ShellContext::BOILER);
});
}
// boiler_cmd topic
void Boiler::boiler_cmd(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
}
if (nullptr != doc["flowtemp"]) {
uint8_t t = doc["flowtemp"];
set_flow_temp(t);
}
if (nullptr != doc["wwtemp"]) {
uint8_t t = doc["wwtemp"];
set_warmwater_temp(t);
}
const char * command = doc["cmd"];
if (command == 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) {
set_warmwater_mode(2);
} else if (strcmp((char *)data, "intelligent") == 0) {
set_warmwater_mode(3);
}
return;
}
// boiler flowtemp setting
if (strcmp(command, "flowtemp") == 0) {
uint8_t t = doc["data"];
if (t) {
set_flow_temp(t);
}
return;
}
}
void Boiler::boiler_cmd_wwactivated(const char * message) {
if ((message[0] == '1' || strcmp(message, "on") == 0) || (strcmp(message, "auto") == 0)) {
set_warmwater_activated(true);
} else if (message[0] == '0' || strcmp(message, "off") == 0) {
set_warmwater_activated(false);
}
}
void Boiler::boiler_cmd_wwonetime(const char * message) {
if (message[0] == '1' || strcmp(message, "on") == 0) {
set_warmwater_onetime(true);
} else if (message[0] == '0' || strcmp(message, "off") == 0) {
set_warmwater_onetime(false);
}
}
void Boiler::boiler_cmd_wwcirculation(const char * message) {
if (message[0] == '1' || strcmp(message, "on") == 0) {
set_warmwater_circulation(true);
} else if (message[0] == '0' || strcmp(message, "off") == 0) {
set_warmwater_circulation(false);
}
}
void Boiler::boiler_cmd_wwtemp(const char * message) {
uint8_t t = atoi((char *)message);
if (t) {
set_warmwater_temp(t);
}
}
// 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/
DynamicJsonDocument doc(capacity);
char s[10]; // for formatting strings
if (Helpers::hasValue(wWComfort_)) {
if (wWComfort_ == 0x00) {
doc["wWComfort"] = "Hot";
} else if (wWComfort_ == 0xD8) {
doc["wWComfort"] = "Eco";
} else if (wWComfort_ == 0xEC) {
doc["wWComfort"] = "Intelligent";
}
}
if (Helpers::hasValue(wWSelTemp_)) {
doc["wWSelTemp"] = wWSelTemp_;
}
if (Helpers::hasValue(wWSetTmp_)) {
doc["wWSetTemp"] = wWSetTmp_;
}
if (Helpers::hasValue(wWDisinfectTemp_)) {
doc["wWDisinfectionTemp"] = wWDisinfectTemp_;
}
if (Helpers::hasValue(selFlowTemp_)) {
doc["selFlowTemp"] = selFlowTemp_;
}
if (Helpers::hasValue(selBurnPow_)) {
doc["selBurnPow"] = selBurnPow_;
}
if (Helpers::hasValue(curBurnPow_)) {
doc["curBurnPow"] = curBurnPow_;
}
if (Helpers::hasValue(pumpMod_)) {
doc["pumpMod"] = pumpMod_;
}
if (Helpers::hasValue(pumpMod2_)) {
doc["pumpMod2"] = pumpMod2_;
}
if (Helpers::hasValue(wWCircPump_, true)) {
doc["wWCircPump"] = Helpers::render_value(s, wWCircPump_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWCircPumpType_)) {
doc["wWCiPuType"] = wWCircPumpType_ ? "valve" : "pump";
}
if (Helpers::hasValue(wWCircPumpMode_)) {
doc["wWCiPuMode"] = wWCircPumpMode_;
}
if (Helpers::hasValue(wWCirc_)) {
doc["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(extTemp_)) {
doc["outdoorTemp"] = (float)extTemp_ / 10;
}
if (Helpers::hasValue(wWCurTmp_)) {
doc["wWCurTmp"] = (float)wWCurTmp_ / 10;
}
if (Helpers::hasValue(wWCurFlow_)) {
doc["wWCurFlow"] = (float)wWCurFlow_ / 10;
}
if (Helpers::hasValue(curFlowTemp_)) {
doc["curFlowTemp"] = (float)curFlowTemp_ / 10;
}
if (Helpers::hasValue(retTemp_)) {
doc["retTemp"] = (float)retTemp_ / 10;
}
if (Helpers::hasValue(switchTemp_)) {
doc["switchTemp"] = (float)switchTemp_ / 10;
}
if (Helpers::hasValue(sysPress_)) {
doc["sysPress"] = (float)sysPress_ / 10;
}
if (Helpers::hasValue(boilTemp_)) {
doc["boilTemp"] = (float)boilTemp_ / 10;
}
if (Helpers::hasValue(wwStorageTemp1_)) {
doc["wwStorageTemp1"] = (float)wwStorageTemp1_ / 10;
}
if (Helpers::hasValue(wwStorageTemp2_)) {
doc["wwStorageTemp2"] = (float)wwStorageTemp2_ / 10;
}
if (Helpers::hasValue(exhaustTemp_)) {
doc["exhaustTemp"] = (float)exhaustTemp_ / 10;
}
if (Helpers::hasValue(wWActivated_, true)) {
doc["wWActivated"] = Helpers::render_value(s, wWActivated_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWOneTime_, true)) {
doc["wWOnetime"] = Helpers::render_value(s, wWOneTime_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWDesinfecting_, true)) {
doc["wWDesinfecting"] = Helpers::render_value(s, wWDesinfecting_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWReadiness_, true)) {
doc["wWReady"] = Helpers::render_value(s, wWReadiness_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWRecharging_, true)) {
doc["wWRecharge"] = Helpers::render_value(s, wWRecharging_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWTemperatureOK_, true)) {
doc["wWTempOK"] = Helpers::render_value(s, wWTemperatureOK_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWCirc_, true)) {
doc["wWCirc"] = Helpers::render_value(s, wWCirc_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(burnGas_, true)) {
doc["burnGas"] = Helpers::render_value(s, burnGas_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(flameCurr_)) {
doc["flameCurr"] = (float)(int16_t)flameCurr_ / 10;
}
if (Helpers::hasValue(heatPmp_, true)) {
doc["heatPmp"] = Helpers::render_value(s, heatPmp_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(fanWork_, true)) {
doc["fanWork"] = Helpers::render_value(s, fanWork_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(ignWork_, true)) {
doc["ignWork"] = Helpers::render_value(s, ignWork_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(wWHeat_, true)) {
doc["wWHeat"] = Helpers::render_value(s, wWHeat_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(heating_temp_)) {
doc["heating_temp"] = heating_temp_;
}
if (Helpers::hasValue(pump_mod_max_)) {
doc["pump_mod_max"] = pump_mod_max_;
}
if (Helpers::hasValue(pump_mod_min_)) {
doc["pump_mod_min"] = pump_mod_min_;
}
if (Helpers::hasValue(wWStarts_)) {
doc["wWStarts"] = wWStarts_;
}
if (Helpers::hasValue(wWWorkM_)) {
doc["wWWorkM"] = wWWorkM_;
}
if (Helpers::hasValue(UBAuptime_)) {
doc["UBAuptime"] = UBAuptime_;
}
if (Helpers::hasValue(burnStarts_)) {
doc["burnStarts"] = burnStarts_;
}
if (Helpers::hasValue(burnWorkMin_)) {
doc["burnWorkMin"] = burnWorkMin_;
}
if (Helpers::hasValue(heatWorkMin_)) {
doc["heatWorkMin"] = heatWorkMin_;
}
if (Helpers::hasValue(serviceCode_)) {
doc["serviceCode"] = serviceCodeChar_;
doc["serviceCodeNumber"] = serviceCode_;
}
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] Performing a boiler publish"));
#endif
// if we have data, publish it
if (!doc.isNull()) {
Mqtt::publish("boiler_data", doc);
}
}
// called after a process command is called, to check values and see if we need to force an MQTT publish
bool Boiler::updated_values() {
return false;
}
// print values to shell console
void Boiler::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // for showing the header
if (Helpers::hasValue(tap_water_active_, true)) {
print_value(shell, 2, F("Hot tap water"), tap_water_active_ ? F("running") : F("off"));
}
if (Helpers::hasValue(heating_active_, true)) {
print_value(shell, 2, F("Central heating"), heating_active_ ? F("active") : F("off"));
}
print_value(shell, 2, F("Warm Water activated"), wWActivated_, nullptr, EMS_VALUE_BOOL);
if (Helpers::hasValue(wWCircPumpType_, true)) {
print_value(shell, 2, F("Warm Water charging type"), wWCircPumpType_ ? F("3-way valve") : F("charge pump"));
}
print_value(shell, 2, F("Warm Water circulation pump available"), wWCircPump_, nullptr, EMS_VALUE_BOOL);
if (Helpers::hasValue(wWCircPumpMode_)) {
if (wWCircPumpMode_ == 7) {
print_value(shell, 2, F("Warm Water circulation pump freq"), F("continuous"));
} else {
char s[7];
char buffer[2];
buffer[0] = (wWCircPumpMode_ % 10) + '0';
buffer[1] = '\0';
strlcpy(s, buffer, 7);
strlcat(s, "x3min", 7);
print_value(shell, 2, F("Warm Water circulation pump freq"), s);
}
}
print_value(shell, 2, F("Warm Water circulation active"), wWCirc_, nullptr, EMS_VALUE_BOOL);
if (wWComfort_ == 0x00) {
print_value(shell, 2, F("Warm Water comfort setting"), F("Hot"));
} else if (wWComfort_ == 0xD8) {
print_value(shell, 2, F("Warm Water comfort setting"), F("Eco"));
} else if (wWComfort_ == 0xEC) {
print_value(shell, 2, F("Warm Water comfort setting"), F("Intelligent"));
}
print_value(shell, 2, F("Warm water mix temperature"), wwMixTemperature_, F_(degrees), 10);
print_value(shell, 2, F("Warm water buffer boiler temperature"), wwBufferBoilerTemperature_, F_(degrees), 10);
print_value(shell, 2, F("Warm Water disinfection temperature"), wWDisinfectTemp_, F_(degrees));
print_value(shell, 2, F("Warm Water selected temperature"), wWSelTemp_, F_(degrees));
print_value(shell, 2, F("Warm Water set temperature"), wWSetTmp_, F_(degrees));
print_value(shell, 2, F("Warm Water current temperature (intern)"), wWCurTmp_, F_(degrees), 10);
print_value(shell, 2, F("Warm water storage temperature (intern)"), wwStorageTemp1_, F_(degrees), 10);
print_value(shell, 2, F("Warm Water current temperature (extern)"), wWCurTmp2_, F_(degrees), 10);
print_value(shell, 2, F("Warm water storage temperature (extern)"), wwStorageTemp2_, F_(degrees), 10);
print_value(shell, 2, F("Warm Water current tap water flow"), wWCurFlow_, F("l/min"), 10);
print_value(shell, 2, F("Warm Water # starts"), wWStarts_, nullptr);
if (Helpers::hasValue(wWWorkM_)) {
shell.printfln(F(" Warm Water active time: %d days %d hours %d minutes"), wWWorkM_ / 1440, (wWWorkM_ % 1440) / 60, wWWorkM_ % 60);
}
print_value(shell, 2, F("Warm Water charging"), wWHeat_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Warm Water disinfecting"), wWDesinfecting_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Selected flow temperature"), selFlowTemp_, F_(degrees));
print_value(shell, 2, F("Current flow temperature"), curFlowTemp_, F_(degrees), 10);
print_value(shell, 2, F("Max boiler temperature"), boilTemp_, F_(degrees), 10);
print_value(shell, 2, F("Return temperature"), retTemp_, F_(degrees), 10);
print_value(shell, 2, F("Gas"), burnGas_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Boiler pump"), heatPmp_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Fan"), fanWork_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Ignition"), ignWork_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Burner selected max power"), selBurnPow_, F_(percent));
print_value(shell, 2, F("Burner current power"), curBurnPow_, F_(percent));
print_value(shell, 2, F("Flame current"), flameCurr_, F("uA"), 10);
print_value(shell, 2, F("System pressure"), sysPress_, F("bar"), 10);
if (Helpers::hasValue(serviceCode_)) {
shell.printfln(F(" System service code: %s (%d)"), serviceCodeChar_, serviceCode_);
} else if (serviceCodeChar_[0] != '\0') {
print_value(shell, 2, F("System service code"), serviceCodeChar_);
}
// UBAParameters
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));
// UBAMonitorSlow
if (Helpers::hasValue(extTemp_)) {
print_value(shell, 2, F("Outside temperature"), extTemp_, F_(degrees), 10);
}
print_value(shell, 2, F("Exhaust temperature"), exhaustTemp_, F_(degrees), 10);
print_value(shell, 2, F("Pump modulation"), pumpMod_, F_(percent));
print_value(shell, 2, F("Pump modulation2"), pumpMod2_, F_(percent));
print_value(shell, 2, F("Burner # starts"), burnStarts_, nullptr);
if (Helpers::hasValue(burnWorkMin_)) {
shell.printfln(F(" Total burner operating time: %d days %d hours %d minutes"), burnWorkMin_ / 1440, (burnWorkMin_ % 1440) / 60, burnWorkMin_ % 60);
}
if (Helpers::hasValue(heatWorkMin_)) {
shell.printfln(F(" Total heat operating time: %d days %d hours %d minutes"), heatWorkMin_ / 1440, (heatWorkMin_ % 1440) / 60, heatWorkMin_ % 60);
}
if (Helpers::hasValue(UBAuptime_)) {
shell.printfln(F(" Total UBA working time: %d days %d hours %d minutes"), UBAuptime_ / 1440, (UBAuptime_ % 1440) / 60, UBAuptime_ % 60);
}
}
/*
* Check if hot tap water or heating is active
* If a value has changed, post it immediately to MQTT so we get real time data
*/
void Boiler::check_active() {
// hot tap water, using flow to check instead of the burner power
// send these values back to the main EMSESP, so other classes (e.g. Shower) can use it
if (Helpers::hasValue(wWCurFlow_) && Helpers::hasValue(burnGas_)) {
tap_water_active_ = ((wWCurFlow_ != 0) && (burnGas_ != EMS_VALUE_BOOL_OFF));
EMSESP::tap_water_active(tap_water_active_);
}
// heating
// using a quick hack for checking the heating by looking at the Selected Flow Temp, but doesn't work for all boilers apparently
if (Helpers::hasValue(selFlowTemp_) && Helpers::hasValue(burnGas_)) {
heating_active_ = (!tap_water_active_ && ((selFlowTemp_ >= EMS_BOILER_SELFLOWTEMP_HEATING) && (burnGas_ != EMS_VALUE_BOOL_OFF)));
}
// see if the heating or hot tap water has changed, if so send
// last_boilerActive stores heating in bit 1 and tap water in bit 2
if (Helpers::hasValue(tap_water_active_, true) && Helpers::hasValue(heating_active_, true)) {
uint8_t latest_boilerState = (tap_water_active_ << 1) + heating_active_;
if (latest_boilerState != last_boilerState) {
last_boilerState = latest_boilerState;
Mqtt::publish("tapwater_active", tap_water_active_);
Mqtt::publish("heating_active", heating_active_);
}
}
}
// 0x33
void Boiler::process_UBAParameterWW(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(wWActivated_, 1); // 0xFF means on
telegram->read_value(wWCircPump_, 6); // 0xFF means on
telegram->read_value(wWCircPumpMode_, 7); // 1=1x3min... 6=6x3min, 7=continuous
telegram->read_value(wWCircPumpType_, 10); // 0 = charge pump, 0xff = 3-way valve
telegram->read_value(wWSelTemp_, 2);
telegram->read_value(wWDisinfectTemp_, 8);
telegram->read_value(wWComfort_, 9);
}
// 0x18
void Boiler::process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(selFlowTemp_, 0);
telegram->read_value(curFlowTemp_, 1);
telegram->read_value(selBurnPow_, 3); // burn power max setting
telegram->read_value(curBurnPow_, 4);
telegram->read_bitvalue(burnGas_, 7, 0);
telegram->read_bitvalue(fanWork_, 7, 2);
telegram->read_bitvalue(ignWork_, 7, 3);
telegram->read_bitvalue(heatPmp_, 7, 5);
telegram->read_bitvalue(wWHeat_, 7, 6);
telegram->read_bitvalue(wWCirc_, 7, 7);
// warm water storage sensors (if present)
// wwStorageTemp2 is also used by some brands as the boiler temperature - see https://github.com/proddy/EMS-ESP/issues/206
telegram->read_value(wwStorageTemp1_, 9); // 0x8300 if not available
telegram->read_value(wwStorageTemp2_, 11); // 0x8000 if not available - this is boiler temp
telegram->read_value(retTemp_, 13);
telegram->read_value(flameCurr_, 15);
telegram->read_value(serviceCode_, 20);
// system pressure. FF means missing
telegram->read_value(sysPress_, 17); // is *10
// read the service code / installation status as appears on the display
if ((telegram->message_length > 18) && (telegram->offset == 0)) {
serviceCodeChar_[0] = char(telegram->message_data[18]); // ascii character 1
serviceCodeChar_[1] = char(telegram->message_data[19]); // ascii character 2
serviceCodeChar_[2] = '\0'; // null terminate string
}
// at this point do a quick check to see if the hot water or heating is active
check_active();
}
/*
* UBATotalUptime - type 0x14 - total uptime
* received only after requested (not broadcasted)
*/
void Boiler::process_UBATotalUptime(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(UBAuptime_, 0, 3); // force to 3 bytes
}
/*
* UBAParameters - type 0x16
*/
void Boiler::process_UBAParameters(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(heating_temp_, 1);
telegram->read_value(pump_mod_max_, 9);
telegram->read_value(pump_mod_min_, 10);
}
/*
* UBAMonitorWW - type 0x34 - warm water monitor. 19 bytes long
* received every 10 seconds
*/
void Boiler::process_UBAMonitorWW(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(wWSetTmp_, 0);
telegram->read_value(wWCurTmp_, 1);
telegram->read_value(wWCurTmp2_, 3);
telegram->read_value(wWCurFlow_, 9);
telegram->read_value(wWWorkM_, 10, 3); // force to 3 bytes
telegram->read_value(wWStarts_, 13, 3); // force to 3 bytes
telegram->read_bitvalue(wWOneTime_, 5, 1);
telegram->read_bitvalue(wWDesinfecting_, 5, 2);
telegram->read_bitvalue(wWReadiness_, 5, 3);
telegram->read_bitvalue(wWRecharging_, 5, 4);
telegram->read_bitvalue(wWTemperatureOK_, 5, 5);
}
/*
* UBAMonitorFastPlus - type 0xE4 - central heating monitor EMS+
* Still to figure out are: serviceCode, retTemp, sysPress
*/
void Boiler::process_UBAMonitorFastPlus(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(selFlowTemp_, 6);
telegram->read_bitvalue(burnGas_, 11, 0);
telegram->read_bitvalue(wWHeat_, 11, 2);
telegram->read_value(curBurnPow_, 10);
telegram->read_value(selBurnPow_, 9);
telegram->read_value(curFlowTemp_, 7);
telegram->read_value(flameCurr_, 19);
// read the service code / installation status as appears on the display
if ((telegram->message_length > 4) && (telegram->offset == 0)) {
serviceCodeChar_[0] = char(telegram->message_data[4]); // ascii character 1
serviceCodeChar_[1] = char(telegram->message_data[5]); // ascii character 2
serviceCodeChar_[2] = '\0';
}
// at this point do a quick check to see if the hot water or heating is active
check_active();
}
/*
* UBAMonitorSlow - type 0x19 - central heating monitor part 2 (27 bytes long)
* received every 60 seconds
* e.g. 08 00 19 00 80 00 02 41 80 00 00 00 00 00 03 91 7B 05 B8 40 00 00 00 04 92 AD 00 5E EE 80 00
* 08 0B 19 00 FF EA 02 47 80 00 00 00 00 62 03 CA 24 2C D6 23 00 00 00 27 4A B6 03 6E 43
* 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 17 19 20 21 22 23 24
*/
void Boiler::process_UBAMonitorSlow(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(extTemp_, 0);
telegram->read_value(boilTemp_, 2);
telegram->read_value(exhaustTemp_, 4);
telegram->read_value(switchTemp_, 25); // only if there is a mixing module present
telegram->read_value(pumpMod_, 9);
telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes
telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes
telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes
}
/*
* UBAMonitorSlowPlus2 - type 0xE3
*/
void Boiler::process_UBAMonitorSlowPlus2(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(pumpMod2_, 13);
}
/*
* UBAMonitorSlowPlus - type 0xE5 - central heating monitor EMS+
*/
void Boiler::process_UBAMonitorSlowPlus(std::shared_ptr<const Telegram> telegram) {
telegram->read_bitvalue(fanWork_, 2, 2);
telegram->read_bitvalue(ignWork_, 2, 3);
telegram->read_bitvalue(heatPmp_, 2, 5);
telegram->read_bitvalue(wWCirc_, 2, 7);
telegram->read_value(burnStarts_, 10, 3); // force to 3 bytes
telegram->read_value(burnWorkMin_, 13, 3); // force to 3 bytes
telegram->read_value(heatWorkMin_, 19, 3); // force to 3 bytes
telegram->read_value(pumpMod_, 25);
}
// 0xE9 - DHW Status
// e.g. 08 00 E9 00 37 01 F6 01 ED 00 00 00 00 41 3C 00 00 00 00 00 00 00 00 00 00 00 00 37 00 00 00 (CRC=77) #data=27
void Boiler::process_UBADHWStatus(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(wWSetTmp_, 0);
telegram->read_value(wWCurTmp_, 1);
telegram->read_value(wWCurTmp2_, 3);
telegram->read_value(wWWorkM_, 17, 3); // force to 3 bytes
telegram->read_value(wWStarts_, 14, 3); // force to 3 bytes
telegram->read_bitvalue(wWOneTime_, 12, 2);
telegram->read_bitvalue(wWDesinfecting_, 12, 3);
telegram->read_bitvalue(wWReadiness_, 12, 4);
telegram->read_bitvalue(wWRecharging_, 13, 4);
telegram->read_bitvalue(wWTemperatureOK_, 13, 5);
telegram->read_bitvalue(wWCircPump_, 13, 2);
telegram->read_value(wWActivated_, 20);
telegram->read_value(wWSelTemp_, 10);
telegram->read_value(wWDisinfectTemp_, 9);
}
// 0x2A - MC10Status
// e.g. 88 00 2A 00 00 00 00 00 00 00 00 00 D2 00 00 80 00 00 01 08 80 00 02 47 00
// see https://github.com/proddy/EMS-ESP/issues/397
void Boiler::process_MC10Status(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(wwMixTemperature_, 14);
telegram->read_value(wwBufferBoilerTemperature_, 18);
}
/*
* UBAOutdoorTemp - type 0xD1 - external temperature EMS+
*/
void Boiler::process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(extTemp_, 0);
}
#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) {
}
// 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) {
}
// 0x10, 0x11, 0x12
// not yet implemented
void Boiler::process_UBAErrorMessage(std::shared_ptr<const Telegram> telegram) {
// data: displaycode(2), errornumner(2), year, month, hour, day, minute, duration(2), src-addr
}
#pragma GCC diagnostic pop
// 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);
}
// flow temp
void Boiler::set_flow_temp(const uint8_t temperature) {
LOG_INFO(F("Setting boiler flow temperature to %d C"), temperature);
write_command(EMS_TYPE_UBASetPoints, 0, temperature);
}
// 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)
void Boiler::set_warmwater_mode(const uint8_t comfort) {
uint8_t set;
if (comfort == 1) {
LOG_INFO(F("Setting boiler warm water to Hot"));
set = 0x00;
} else if (comfort == 2) {
LOG_INFO(F("Setting boiler warm water to Eco"));
set = 0xD8;
} else if (comfort == 3) {
LOG_INFO(F("Setting boiler warm water to Intelligent"));
set = 0xEC;
} else {
return; // do nothing
}
write_command(EMS_TYPE_UBAParameterWW, 9, set);
}
// turn on/off warm water
void Boiler::set_warmwater_activated(const bool activated) {
LOG_INFO(F("Setting boiler warm water %s"), activated ? "on" : "off");
uint8_t value;
// https://github.com/proddy/EMS-ESP/issues/268
if (EMSbus::is_ht3()) {
value = (activated ? 0x08 : 0x00); // 0x08 is on, 0x00 is off
} else {
value = (activated ? 0xFF : 0x00); // 0xFF is on, 0x00 is off
}
write_command(EMS_TYPE_UBAParameterWW, 1, value);
}
// Activate / De-activate the Warm Tap Water
// true = on, false = off
// Note: Using the type 0x1D to put the boiler into Test mode. This may be shown on the boiler with a flashing 'T'
void Boiler::set_tapwarmwater_activated(const bool activated) {
LOG_INFO(F("Setting boiler warm tap water %s"), activated ? "on" : "off");
uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH];
for (uint8_t i = 0; i < sizeof(message_data); i++) {
message_data[i] = 0x00;
}
// we use the special test mode 0x1D for this. Setting the first data to 5A puts the system into test mode and
// a setting of 0x00 puts it back into normal operating mode
// when in test mode we're able to mess around with the 3-way valve settings
if (!activated) {
// on
message_data[0] = 0x5A; // test mode on
message_data[1] = 0x00; // burner output 0%
message_data[3] = 0x64; // boiler pump capacity 100%
message_data[4] = 0xFF; // 3-way valve hot water only
} else {
// get out of test mode. Send all zeros.
// telegram: 0B 08 1D 00 00
message_data[4] = 0x00; // test mode off
}
write_command(EMS_TYPE_UBAFunctionTest, 0, message_data, sizeof(message_data), 0);
}
// Activate / De-activate One Time warm water 0x35
// true = on, false = off
// See also https://github.com/proddy/EMS-ESP/issues/341#issuecomment-596245458 for Junkers
void Boiler::set_warmwater_onetime(const bool activated) {
LOG_INFO(F("Setting boiler warm water OneTime loading %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 0, (activated ? 0x22 : 0x02));
}
// Activate / De-activate circulation of warm water 0x35
// true = on, false = off
void Boiler::set_warmwater_circulation(const bool activated) {
LOG_INFO(F("Setting boiler warm water circulation %s"), activated ? "on" : "off");
write_command(EMS_TYPE_UBAFlags, 1, (activated ? 0x22 : 0x02));
}
// add console commands
void Boiler::console_commands(Shell & shell, unsigned int context) {
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(read)},
flash_string_vector{F_(typeid_mandatory)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
uint16_t type_id = Helpers::hextoint(arguments.front().c_str());
EMSESP::send_read_request(type_id, device_id());
});
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(wwtemp)},
flash_string_vector{F_(degrees_mandatory)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
set_warmwater_temp(Helpers::atoint(arguments.front().c_str()));
});
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(flowtemp)},
flash_string_vector{F_(degrees_mandatory)},
[=](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments) {
set_flow_temp(Helpers::atoint(arguments.front().c_str()));
});
EMSESPShell::commands->add_command(
ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(wwactive)},
flash_string_vector{F_(bool_mandatory)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments[0] == read_flash_string(F_(on))) {
set_warmwater_activated(true);
} else if (arguments[0] == read_flash_string(F_(off))) {
set_warmwater_activated(false);
} else {
shell.println(F("Must be on or off"));
return;
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(on)), read_flash_string(F_(off))};
});
EMSESPShell::commands->add_command(
ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(wwonetime)},
flash_string_vector{F_(bool_mandatory)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments[0] == read_flash_string(F_(on))) {
set_warmwater_onetime(true);
} else if (arguments[0] == read_flash_string(F_(off))) {
set_warmwater_onetime(false);
} else {
shell.println(F("Must be on or off"));
return;
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(on)), read_flash_string(F_(off))};
});
EMSESPShell::commands->add_command(
ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(wwcirculation)},
flash_string_vector{F_(bool_mandatory)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments[0] == read_flash_string(F_(on))) {
set_warmwater_circulation(true);
} else if (arguments[0] == read_flash_string(F_(off))) {
set_warmwater_circulation(false);
} else {
shell.println(F("Must be on or off"));
return;
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(on)), read_flash_string(F_(off))};
});
EMSESPShell::commands->add_command(
ShellContext::BOILER,
CommandFlags::ADMIN,
flash_string_vector{F_(comfort)},
flash_string_vector{F_(comfort_mandatory)},
[=](Shell & shell, const std::vector<std::string> & arguments) {
if (arguments[0] == read_flash_string(F_(hot))) {
set_warmwater_mode(1);
} else if (arguments[0] == read_flash_string(F_(eco))) {
set_warmwater_mode(2);
} else if (arguments[0] == read_flash_string(F_(intelligent))) {
set_warmwater_mode(3);
} else {
shell.println(F("Invalid value. Must be hot, eco or intelligent"));
}
},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) -> const std::vector<std::string> {
return std::vector<std::string>{read_flash_string(F_(hot)), read_flash_string(F_(eco)), read_flash_string(F_(intelligent))};
});
EMSESPShell::commands->add_command(ShellContext::BOILER,
CommandFlags::USER,
flash_string_vector{F_(show)},
[&](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { show_values(shell); });
// enter the context
Console::enter_custom_context(shell, context);
}
} // namespace emsesp

168
src/devices/boiler.h Normal file
View File

@@ -0,0 +1,168 @@
/*
* 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 EMSESP_BOILER_H
#define EMSESP_BOILER_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include <string>
#include "emsdevice.h"
#include "telegram.h"
#include "emsesp.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Boiler : public EMSdevice {
public:
Boiler(uint8_t device_type, int8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands(Shell & shell, unsigned int context);
uint8_t last_boilerState = 0xFF; // remember last state of heating and warm water on/off
static constexpr uint8_t EMS_TYPE_UBAParameterWW = 0x33;
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_BOILER_SELFLOWTEMP_HEATING = 20; // was originally 70, changed to 30 for issue #193, then to 20 with issue #344
// UBAParameterWW
uint8_t wWActivated_ = EMS_VALUE_BOOL_NOTSET; // Warm Water activated
uint8_t wWSelTemp_ = EMS_VALUE_UINT_NOTSET; // Warm Water selected temperature
uint8_t wWCircPump_ = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump available
uint8_t wWCircPumpMode_ = EMS_VALUE_UINT_NOTSET; // Warm Water circulation pump mode
uint8_t wWCircPumpType_ = EMS_VALUE_BOOL_NOTSET; // Warm Water circulation pump type
uint8_t wWDisinfectTemp_ = EMS_VALUE_UINT_NOTSET; // Warm Water disinfection temperature to prevent infection
uint8_t wWComfort_ = EMS_VALUE_UINT_NOTSET; // WW comfort mode
// MC10Status
uint16_t wwMixTemperature_ = EMS_VALUE_USHORT_NOTSET; // mengertemperatuur
uint16_t wwBufferBoilerTemperature_ = EMS_VALUE_USHORT_NOTSET; // bufferboilertemperatuur
// UBAMonitorFast - 0x18 on EMS1
uint8_t selFlowTemp_ = EMS_VALUE_UINT_NOTSET; // Selected flow temperature
uint16_t curFlowTemp_ = EMS_VALUE_USHORT_NOTSET; // Current flow temperature
uint16_t wwStorageTemp1_ = EMS_VALUE_USHORT_NOTSET; // warm water storage temp 1
uint16_t wwStorageTemp2_ = EMS_VALUE_USHORT_NOTSET; // warm water storage temp 2
uint16_t retTemp_ = EMS_VALUE_USHORT_NOTSET; // Return temperature
uint8_t burnGas_ = EMS_VALUE_BOOL_NOTSET; // Gas on/off
uint8_t fanWork_ = EMS_VALUE_BOOL_NOTSET; // Fan on/off
uint8_t ignWork_ = EMS_VALUE_BOOL_NOTSET; // Ignition on/off
uint8_t heatPmp_ = EMS_VALUE_BOOL_NOTSET; // Boiler pump on/off
uint8_t wWHeat_ = EMS_VALUE_BOOL_NOTSET; // 3-way valve on WW
uint8_t wWCirc_ = EMS_VALUE_BOOL_NOTSET; // Circulation on/off
uint8_t selBurnPow_ = EMS_VALUE_UINT_NOTSET; // Burner max power %
uint8_t curBurnPow_ = EMS_VALUE_UINT_NOTSET; // Burner current power %
uint16_t flameCurr_ = EMS_VALUE_USHORT_NOTSET; // Flame current in micro amps
uint8_t sysPress_ = EMS_VALUE_UINT_NOTSET; // System pressure
char serviceCodeChar_[3] = {'\0'}; // 2 character status/service code
uint16_t serviceCode_ = EMS_VALUE_USHORT_NOTSET; // error/service code
// UBAMonitorSlow - 0x19 on EMS1
int16_t extTemp_ = EMS_VALUE_SHORT_NOTSET; // Outside temperature
uint16_t boilTemp_ = EMS_VALUE_USHORT_NOTSET; // Boiler temperature
uint16_t exhaustTemp_ = EMS_VALUE_USHORT_NOTSET; // Exhaust temperature
uint8_t pumpMod_ = EMS_VALUE_UINT_NOTSET; // Pump modulation %
uint32_t burnStarts_ = EMS_VALUE_ULONG_NOTSET; // # burner restarts
uint32_t burnWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total burner operating time
uint32_t heatWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total heat operating time
uint16_t switchTemp_ = EMS_VALUE_USHORT_NOTSET; // Switch temperature
// UBAMonitorWW
uint8_t wWSetTmp_ = EMS_VALUE_UINT_NOTSET; // Warm Water set temperature
uint16_t wWCurTmp_ = EMS_VALUE_USHORT_NOTSET; // Warm Water current temperature
uint16_t wWCurTmp2_ = EMS_VALUE_USHORT_NOTSET; // Warm Water current temperature storage
uint32_t wWStarts_ = EMS_VALUE_ULONG_NOTSET; // Warm Water # starts
uint32_t wWWorkM_ = EMS_VALUE_ULONG_NOTSET; // Warm Water # minutes
uint8_t wWOneTime_ = EMS_VALUE_BOOL_NOTSET; // Warm Water one time function on/off
uint8_t wWDesinfecting_ = EMS_VALUE_BOOL_NOTSET; // Warm Water disinfection on/off
uint8_t wWReadiness_ = EMS_VALUE_BOOL_NOTSET; // Warm Water readiness on/off
uint8_t wWRecharging_ = EMS_VALUE_BOOL_NOTSET; // Warm Water recharge on/off
uint8_t wWTemperatureOK_ = EMS_VALUE_BOOL_NOTSET; // Warm Water temperature ok on/off
uint8_t wWCurFlow_ = EMS_VALUE_UINT_NOTSET; // Warm Water current flow temp in l/min
// UBATotalUptime
uint32_t UBAuptime_ = EMS_VALUE_ULONG_NOTSET; // Total UBA working hours
// UBAParameters
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 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)
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);
void process_UBATotalUptime(std::shared_ptr<const Telegram> telegram);
void process_UBAParameters(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFastPlus(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorSlow(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorSlowPlus(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorSlowPlus2(std::shared_ptr<const Telegram> telegram);
void process_UBAOutdoorTemp(std::shared_ptr<const Telegram> telegram);
void process_UBASetPoints(std::shared_ptr<const Telegram> telegram);
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_UBAErrorMessage(std::shared_ptr<const Telegram> telegram);
void process_UBADHWStatus(std::shared_ptr<const Telegram> telegram);
void check_active();
void set_warmwater_temp(const uint8_t temperature);
void set_flow_temp(const uint8_t temperature);
void set_warmwater_mode(const uint8_t comfort);
void set_warmwater_activated(const bool activated);
void set_tapwarmwater_activated(const bool activated);
void set_warmwater_onetime(const bool activated);
void set_warmwater_circulation(const bool activated);
// mqtt callbacks
void boiler_cmd(const char * message);
void boiler_cmd_wwactivated(const char * message);
void boiler_cmd_wwonetime(const char * message);
void boiler_cmd_wwcirculation(const char * message);
void boiler_cmd_wwtemp(const char * message);
};
} // namespace emsesp
#endif

58
src/devices/connect.cpp Normal file
View File

@@ -0,0 +1,58 @@
/*
* 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 "connect.h"
namespace emsesp {
REGISTER_FACTORY(Connect, EMSdevice::DeviceType::CONNECT);
MAKE_PSTR(logger_name, "connect")
uuid::log::Logger Connect::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Controller::process_XX, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Controller::cmd, this, _1));
}
void Connect::add_context_menu() {
}
// display all values into the shell console
void Connect::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
}
// publish values via MQTT
void Connect::publish_values() {
}
// check to see if values have been updated
bool Connect::updated_values() {
return false;
}
// add console commands
void Connect::console_commands() {
}
} // namespace emsesp

51
src/devices/connect.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* 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 EMSESP_CONNECT_H
#define EMSESP_CONNECT_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Connect : public EMSdevice {
public:
Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
};
} // namespace emsesp
#endif

View File

@@ -0,0 +1,60 @@
/*
* 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 "controller.h"
// MAKE_PSTR_WORD(controller)
namespace emsesp {
REGISTER_FACTORY(Controller, EMSdevice::DeviceType::CONTROLLER);
MAKE_PSTR(logger_name, "controller")
uuid::log::Logger Controller::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Controller::process_XX, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Controller::cmd, this, _1));
}
void Controller::add_context_menu() {
}
// 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
}
// publish values via MQTT
void Controller::publish_values() {
}
// check to see if values have been updated
bool Controller::updated_values() {
return false;
}
// add console commands
void Controller::console_commands() {
}
} // namespace emsesp

51
src/devices/controller.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* 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 EMSESP_CONTROLLER_H
#define EMSESP_CONTROLLER_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Controller : public EMSdevice {
public:
Controller(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
};
} // namespace emsesp
#endif

60
src/devices/gateway.cpp Normal file
View File

@@ -0,0 +1,60 @@
/*
* 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 "gateway.h"
// MAKE_PSTR_WORD(gateway)
namespace emsesp {
REGISTER_FACTORY(Gateway, EMSdevice::DeviceType::GATEWAY);
MAKE_PSTR(logger_name, "gateway")
uuid::log::Logger Gateway::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Controller::process_XX, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Controller::cmd, this, _1));
}
void Gateway::add_context_menu() {
}
// 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
}
// publish values via MQTT
void Gateway::publish_values() {
}
// check to see if values have been updated
bool Gateway::updated_values() {
return false;
}
// add console commands
void Gateway::console_commands() {
}
} // namespace emsesp

51
src/devices/gateway.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* 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 EMSESP_GATEWAY_H
#define EMSESP_GATEWAY_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Gateway : public EMSdevice {
public:
Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
};
} // namespace emsesp
#endif

92
src/devices/heatpump.cpp Normal file
View File

@@ -0,0 +1,92 @@
/*
* 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 "heatpump.h"
// MAKE_PSTR_WORD(heatpump)
/*
example telegrams 0x32B, 0x37B
"38 10 FF 00 03 7B 08 24 00 4B",
"38 10 FF 00 03 2B 00 C7 07 C3 01",
"38 10 FF 00 03 2B 00 D1 08 2A 01",
*/
namespace emsesp {
REGISTER_FACTORY(Heatpump, EMSdevice::DeviceType::HEATPUMP);
MAKE_PSTR(logger_name, "heatpump")
uuid::log::Logger Heatpump::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Registering new Heat Pump module with device ID 0x%02X"), device_id);
// telegram handlers
register_telegram_type(0x047B, F("HP1"), true, std::bind(&Heatpump::process_HPMonitor1, this, _1));
register_telegram_type(0x042B, F("HP2"), true, std::bind(&Heatpump::process_HPMonitor2, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Heatpump::cmd, this, _1));
}
// context submenu
void Heatpump::add_context_menu() {
}
// 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
}
// publish values via MQTT
void Heatpump::publish_values() {
}
// check to see if values have been updated
bool Heatpump::updated_values() {
return false;
}
// add console commands
void Heatpump::console_commands() {
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
/*
* Type 0x42B- HeatPump Monitor 1
* e.g. "38 10 FF 00 03 2B 00 D1 08 2A 01"
*/
void Heatpump::process_HPMonitor1(std::shared_ptr<const Telegram> telegram) {
// still to implement
}
/*
* Type 0x47B - HeatPump Monitor 2
* e.g. "38 10 FF 00 03 7B 08 24 00 4B"
*/
void Heatpump::process_HPMonitor2(std::shared_ptr<const Telegram> telegram) {
// still to implement
}
#pragma GCC diagnostic pop
} // namespace emsesp

54
src/devices/heatpump.h Normal file
View File

@@ -0,0 +1,54 @@
/*
* 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 EMSESP_HEATPUMP_H
#define EMSESP_HEATPUMP_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Heatpump : public EMSdevice {
public:
Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
void process_HPMonitor1(std::shared_ptr<const Telegram> telegram);
void process_HPMonitor2(std::shared_ptr<const Telegram> telegram);
};
} // namespace emsesp
#endif

213
src/devices/mixing.cpp Normal file
View File

@@ -0,0 +1,213 @@
/*
* 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 "mixing.h"
namespace emsesp {
REGISTER_FACTORY(Mixing, EMSdevice::DeviceType::MIXING);
MAKE_PSTR(logger_name, "mixing")
uuid::log::Logger Mixing::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Mixing::Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Registering new Mixing module with device ID 0x%02X"), device_id);
if (flags == EMSdevice::EMS_DEVICE_FLAG_MMPLUS) {
if (device_id <= 0x27) {
// telegram handlers 0x20 - 0x27 for HC
register_telegram_type(device_id - 0x20 + 0x02D7, F("MMPLUSStatusMessage_HC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_HC, this, _1));
} else {
// telegram handlers for warm water/DHW 0x28, 0x29
register_telegram_type(device_id - 0x28 + 0x0331, F("MMPLUSStatusMessage_WWC"), true, std::bind(&Mixing::process_MMPLUSStatusMessage_WWC, this, _1));
}
}
// EMS 1.0
if (flags == EMSdevice::EMS_DEVICE_FLAG_MM10) {
register_telegram_type(0x00AA, F("MMConfigMessage"), false, std::bind(&Mixing::process_MMConfigMessage, this, _1));
register_telegram_type(0x00AB, F("MMStatusMessage"), true, std::bind(&Mixing::process_MMStatusMessage, this, _1));
register_telegram_type(0x00AC, F("MMSetMessage"), false, std::bind(&Mixing::process_MMSetMessage, this, _1));
}
// HT3
if (flags == EMSdevice::EMS_DEVICE_FLAG_IPM) {
register_telegram_type(0x010C, F("IPMSetMessage"), false, std::bind(&Mixing::process_IPMStatusMessage, this, _1));
}
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Mixing::cmd, this, _1));
}
// add context submenu
void Mixing::add_context_menu() {
}
// check to see if values have been updated
bool Mixing::updated_values() {
return false;
}
// add console commands
void Mixing::console_commands() {
}
// display all values into the shell console
void Mixing::show_values(uuid::console::Shell & shell) {
EMSdevice::show_values(shell); // always call this to show header
if (type_ == Type::NONE) {
return; // don't have any values yet
}
if (type_ == Type::WWC) {
print_value(shell, 2, F("Warm Water Circuit"), hc_, nullptr);
} else {
print_value(shell, 2, F("Heating Circuit"), hc_, nullptr);
}
print_value(shell, 4, F("Current flow temperature"), flowTemp_, F_(degrees), 10);
print_value(shell, 4, F("Setpoint flow temperature"), flowSetTemp_, F_(degrees));
print_value(shell, 4, F("Current pump modulation"), pumpMod_, F_(percent));
print_value(shell, 4, F("Current valve status"), status_, nullptr);
}
// publish values via MQTT
// ideally we should group up all the mixing units together into a nested JSON but for now we'll send them individually
void Mixing::publish_values() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_SMALL);
switch (type_) {
case Type::HC:
doc["type"] = "hc";
break;
case Type::WWC:
doc["type"] = "wwc";
break;
case Type::NONE:
default:
return;
}
if (Helpers::hasValue(flowTemp_)) {
doc["flowTemp"] = (float)flowTemp_ / 10;
}
if (Helpers::hasValue(pumpMod_)) {
doc["pumpMod"] = pumpMod_;
}
if (Helpers::hasValue(status_)) {
doc["status"] = status_;
}
if (Helpers::hasValue(flowSetTemp_)) {
doc["flowSetTemp"] = flowSetTemp_;
}
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] Performing a mixing module publish"));
#endif
char topic[30];
char s[3]; // for formatting strings
strlcpy(topic, "mixing_data", 30);
strlcat(topic, Helpers::itoa(s, device_id() - 0x20 + 1), 30); // append hc to topic
Mqtt::publish(topic, doc);
}
// heating circuits 0x02D7, 0x02D8 etc...
// e.g. A0 00 FF 00 01 D7 00 00 00 80 00 00 00 00 03 C5
// A0 0B FF 00 01 D7 00 00 00 80 00 00 00 00 03 80
void Mixing::process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is
telegram->read_value(flowTemp_, 3); // is * 10
telegram->read_value(flowSetTemp_, 5);
telegram->read_value(pumpMod_, 2);
telegram->read_value(status_, 1); // valve status
}
// Mixing module warm water loading/DHW - 0x0331, 0x0332
// e.g. A9 00 FF 00 02 32 02 6C 00 3C 00 3C 3C 46 02 03 03 00 3C // on 0x28
// A8 00 FF 00 02 31 02 35 00 3C 00 3C 3C 46 02 03 03 00 3C // in 0x29
void Mixing::process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> telegram) {
type_ = Type::WWC;
hc_ = telegram->type_id - 0x0331 + 1; // determine which circuit this is. There are max 2.
telegram->read_value(flowTemp_, 0); // is * 10
telegram->read_value(pumpMod_, 2);
telegram->read_value(status_, 11); // temp status
}
// Mixing IMP - 0x010C
// e.g. A0 00 FF 00 00 0C 01 00 00 00 00 00 54
// A1 00 FF 00 00 0C 02 04 00 01 1D 00 82
void Mixing::process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
hc_ = device_id() - 0x20 + 1;
uint8_t ismixed = 0;
telegram->read_value(ismixed, 0); // check if circuit is active, 0-off, 1-unmixed, 2-mixed
if (ismixed == 0) {
return;
}
if (ismixed == 2) { // we have a mixed circuit
telegram->read_value(flowTemp_, 3); // is * 10
telegram->read_value(flowSetTemp_, 5);
telegram->read_value(status_, 2); // valve status
}
uint8_t pump = 0xFF;
telegram->read_bitvalue(pump, 1, 0); // pump is also in unmixed circuits
if (pump != 0xFF) {
pumpMod_ = 100 * pump;
}
}
// Mixing on a MM10 - 0xAB
// e.g. Mixing Module -> All, type 0xAB, telegram: 21 00 AB 00 2D 01 BE 64 04 01 00 (CRC=15) #data=7
// see also https://github.com/proddy/EMS-ESP/issues/386
void Mixing::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type_ = Type::HC;
// the heating circuit is determine by which device_id it is, 0x20 - 0x23
// 0x21 is position 2. 0x20 is typically reserved for the WM10 switch module
// see https://github.com/proddy/EMS-ESP/issues/270 and https://github.com/proddy/EMS-ESP/issues/386#issuecomment-629610918
hc_ = device_id() - 0x20 + 1;
telegram->read_value(flowTemp_, 1); // is * 10
telegram->read_value(pumpMod_, 3);
telegram->read_value(flowSetTemp_, 0);
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// Mixing on a MM10 - 0xAA
// e.g. Thermostat -> Mixing Module, type 0xAA, telegram: 10 21 AA 00 FF 0C 0A 11 0A 32 xx
void Mixing::process_MMConfigMessage(std::shared_ptr<const Telegram> telegram) {
hc_ = device_id() - 0x20 + 1;
// pos 0: active FF = on
// pos 1: valve runtime 0C = 120 sec in units of 10 sec
}
// Mixing on a MM10 - 0xAC
// e.g. Thermostat -> Mixing Module, type 0xAC, telegram: 10 21 AC 00 1E 64 01 AB
void Mixing::process_MMSetMessage(std::shared_ptr<const Telegram> telegram) {
hc_ = device_id() - 0x20 + 1;
// pos 0: flowtemp setpoint 1E = 30°C
// pos 1: position in %
}
#pragma GCC diagnostic pop
} // namespace emsesp

72
src/devices/mixing.h Normal file
View File

@@ -0,0 +1,72 @@
/*
* 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 EMSESP_MIXING_H
#define EMSESP_MIXING_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Mixing : public EMSdevice {
public:
Mixing(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
void process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram);
void process_MMPLUSStatusMessage_WWC(std::shared_ptr<const Telegram> telegram);
void process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram);
void process_MMStatusMessage(std::shared_ptr<const Telegram> telegram);
void process_MMConfigMessage(std::shared_ptr<const Telegram> telegram);
void process_MMSetMessage(std::shared_ptr<const Telegram> telegram);
enum class Type {
NONE,
HC, // heating circuit
WWC // warm water circuit
};
private:
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET;
uint8_t pumpMod_ = EMS_VALUE_UINT_NOTSET;
uint8_t status_ = EMS_VALUE_UINT_NOTSET;
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
Type type_ = Type::NONE;
};
} // namespace emsesp
#endif

236
src/devices/solar.cpp Normal file
View File

@@ -0,0 +1,236 @@
/*
* 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 "solar.h"
MAKE_PSTR(kwh, "kWh")
MAKE_PSTR(wh, "Wh")
namespace emsesp {
REGISTER_FACTORY(Solar, EMSdevice::DeviceType::SOLAR);
MAKE_PSTR(logger_name, "solar")
uuid::log::Logger Solar::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
LOG_DEBUG(F("Registering new Solar module with device ID 0x%02X"), device_id);
// telegram handlers
if (flags == EMSdevice::EMS_DEVICE_FLAG_SM10) {
register_telegram_type(0x0097, F("SM10Monitor"), true, std::bind(&Solar::process_SM10Monitor, this, _1));
}
if (flags == EMSdevice::EMS_DEVICE_FLAG_SM100) {
register_telegram_type(0x0362, F("SM100Monitor"), true, std::bind(&Solar::process_SM100Monitor, this, _1));
register_telegram_type(0x0363, F("SM100Monitor2"), true, std::bind(&Solar::process_SM100Monitor2, this, _1));
register_telegram_type(0x0366, F("SM100Config"), true, std::bind(&Solar::process_SM100Config, this, _1));
register_telegram_type(0x0364, F("SM100Status"), false, std::bind(&Solar::process_SM100Status, this, _1));
register_telegram_type(0x036A, F("SM100Status2"), false, std::bind(&Solar::process_SM100Status2, this, _1));
register_telegram_type(0x038E, F("SM100Energy"), true, std::bind(&Solar::process_SM100Energy, this, _1));
}
if (flags == EMSdevice::EMS_DEVICE_FLAG_ISM) {
register_telegram_type(0x0103, F("ISM1StatusMessage"), true, std::bind(&Solar::process_ISM1StatusMessage, this, _1));
register_telegram_type(0x0101, F("ISM1Set"), false, std::bind(&Solar::process_ISM1Set, this, _1));
}
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Solar::cmd, this, _1));
}
// context submenu
void Solar::add_context_menu() {
}
// 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
print_value(shell, 2, F("Collector temperature (TS1)"), collectorTemp_, F_(degrees), 10);
print_value(shell, 2, F("Bottom temperature (TS2)"), bottomTemp_, F_(degrees), 10);
print_value(shell, 2, F("Bottom temperature (TS5)"), bottomTemp2_, F_(degrees), 10);
print_value(shell, 2, F("Pump modulation"), pumpModulation_, F_(percent));
print_value(shell, 2, F("Valve (VS2) status"), valveStatus_, nullptr, EMS_VALUE_BOOL);
print_value(shell, 2, F("Pump (PS1) active"), pump_, nullptr, EMS_VALUE_BOOL);
if (Helpers::hasValue(pumpWorkMin_)) {
shell.printfln(F(" Pump working time: %d days %d hours %d minutes"), pumpWorkMin_ / 1440, (pumpWorkMin_ % 1440) / 60, pumpWorkMin_ % 60);
}
print_value(shell, 2, F("Energy last hour"), energyLastHour_, F_(wh), 10);
print_value(shell, 2, F("Energy today"), energyToday_, F_(wh));
print_value(shell, 2, F("Energy total"), energyTotal_, F_(kwh), 10);
}
// publish values via MQTT
void Solar::publish_values() {
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_MEDIUM);
char s[10]; // for formatting strings
if (Helpers::hasValue(collectorTemp_)) {
doc["collectortemp"] = (float)collectorTemp_ / 10;
}
if (Helpers::hasValue(bottomTemp_)) {
doc["bottomtemp"] = (float)bottomTemp_ / 10;
}
if (Helpers::hasValue(bottomTemp2_)) {
doc["bottomtemp2"] = (float)bottomTemp2_ / 10;
}
if (Helpers::hasValue(pumpModulation_)) {
doc["pumpmodulation"] = pumpModulation_;
}
if (Helpers::hasValue(pump_, true)) {
doc["pump"] = Helpers::render_value(s, pump_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(valveStatus_, true)) {
doc["valvestatus"] = Helpers::render_value(s, valveStatus_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(pumpWorkMin_)) {
doc["pumpWorkMin"] = (float)pumpWorkMin_;
}
if (Helpers::hasValue(energyLastHour_)) {
doc["energylasthour"] = (float)energyLastHour_ / 10;
}
if (Helpers::hasValue(energyToday_)) {
doc["energytoday"] = energyToday_;
}
if (Helpers::hasValue(energyTotal_)) {
doc["energytotal"] = (float)energyTotal_ / 10;
}
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] Performing a solar module publish"));
#endif
Mqtt::publish("sm_data", doc);
}
// check to see if values have been updated
bool Solar::updated_values() {
return false;
}
// add console commands
void Solar::console_commands() {
}
// SM10Monitor - type 0x97
void Solar::process_SM10Monitor(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 2); // collector temp from SM10, is *10
telegram->read_value(bottomTemp_, 5); // bottom temp from SM10, is *10
telegram->read_value(pumpModulation_, 4); // modulation solar pump
telegram->read_bitvalue(pump_, 7, 1);
telegram->read_value(pumpWorkMin_, 8);
}
/*
* SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200
* e.g. B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80
* e.g, 30 00 FF 00 02 62 01 AC
* 30 00 FF 18 02 62 80 00
* 30 00 FF 00 02 62 01 A1 - for bottom temps
* bytes 0+1 = TS1 Temperature sensor for collector
* bytes 2+3 = TS2 Temperature sensor bottom cylinder 1
* bytes 16+17 = TS5 Temperature sensor bottom cylinder 2
*/
void Solar::process_SM100Monitor(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 0); // is *10
telegram->read_value(bottomTemp_, 2); // is *10
telegram->read_value(bottomTemp2_, 16); // is *10
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
// SM100Monitor2 - 0x0363
// e.g. B0 00 FF 00 02 63 80 00 80 00 00 00 80 00 80 00 80 00 00 80 00 5A
void Solar::process_SM100Monitor2(std::shared_ptr<const Telegram> telegram) {
// not implemented yet
}
#pragma GCC diagnostic pop
// SM100Config - 0x0366
// e.g. B0 00 FF 00 02 66 01 62 00 13 40 14
void Solar::process_SM100Config(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(availabilityFlag_, 0);
telegram->read_value(configFlag_, 1);
telegram->read_value(userFlag_, 2);
}
/*
* SM100Status - type 0x0364 EMS+ for pump modulation - for SM100 and SM200
* e.g. 30 00 FF 09 02 64 64 = 100%
* 30 00 FF 09 02 64 1E = 30%
*/
void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
uint8_t pumpmod = pumpModulation_;
telegram->read_value(pumpModulation_, 9);
if (pumpmod == 0 && pumpModulation_ == 100) { // mask out boosts
pumpModulation_ = 15; // set to minimum
}
}
/*
* SM100Status2 - type 0x036A EMS+ for pump on/off at offset 0x0A - for SM100 and SM200
* e.g. B0 00 FF 00 02 6A 03 03 03 03 01 03 03 03 03 03 01 03
* byte 4 = VS2 3-way valve for cylinder 2 : test=01, on=04 and off=03
* byte 10 = PS1 Solar circuit pump for collector array 1: test=b0001(1), on=b0100(4) and off=b0011(3)
*/
void Solar::process_SM100Status2(std::shared_ptr<const Telegram> telegram) {
telegram->read_bitvalue(valveStatus_, 4, 2); // on if bit 2 set
telegram->read_bitvalue(pump_, 10, 2); // on if bit 2 set
}
/*
* SM100Energy - type 0x038E EMS+ for energy readings
* e.g. 30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35
*/
void Solar::process_SM100Energy(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(energyLastHour_, 0); // last hour / 10 in Wh
telegram->read_value(energyToday_, 4); // todays in Wh
telegram->read_value(energyTotal_, 8); // total / 10 in kWh
}
/*
* Junkers ISM1 Solar Module - type 0x0103 EMS+ for energy readings
* e.g. B0 00 FF 00 00 03 32 00 00 00 00 13 00 D6 00 00 00 FB D0 F0
*/
void Solar::process_ISM1StatusMessage(std::shared_ptr<const Telegram> telegram) {
telegram->read_value(collectorTemp_, 4); // Collector Temperature
telegram->read_value(bottomTemp_, 6); // Temperature Bottom of Solar Boiler
uint16_t Wh = 0xFFFF;
telegram->read_value(Wh, 2); // Solar Energy produced in last hour only ushort, is not * 10
if (Wh != 0xFFFF) {
energyLastHour_ = Wh * 10; // set to *10
}
telegram->read_bitvalue(pump_, 8, 0); // Solar pump on (1) or off (0)
telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes
}
/*
* 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) {
telegram->read_value(setpoint_maxBottomTemp_, 6);
}
} // namespace emsesp

80
src/devices/solar.h Normal file
View File

@@ -0,0 +1,80 @@
/*
* 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 EMSESP_SOLAR_H
#define EMSESP_SOLAR_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Solar : public EMSdevice {
public:
Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
int16_t collectorTemp_ = EMS_VALUE_SHORT_NOTSET; // collector temp (TS1)
int16_t bottomTemp_ = EMS_VALUE_SHORT_NOTSET; // bottom temp (TS2)
int16_t bottomTemp2_ = EMS_VALUE_SHORT_NOTSET; // bottom temp cylinder 2 (TS5)
uint8_t pumpModulation_ = EMS_VALUE_UINT_NOTSET; // modulation solar pump
uint8_t pump_ = EMS_VALUE_BOOL_NOTSET; // pump active
uint8_t valveStatus_ = EMS_VALUE_BOOL_NOTSET; // valve status (VS2)
int16_t setpoint_maxBottomTemp_ = EMS_VALUE_SHORT_NOTSET; // setpoint for maximum collector temp
uint32_t energyLastHour_ = EMS_VALUE_ULONG_NOTSET;
uint32_t energyToday_ = EMS_VALUE_ULONG_NOTSET;
uint32_t energyTotal_ = EMS_VALUE_ULONG_NOTSET;
uint32_t pumpWorkMin_ = EMS_VALUE_ULONG_NOTSET; // Total solar pump operating time
uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t userFlag_ = EMS_VALUE_BOOL_NOTSET;
void process_SM10Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100Monitor2(std::shared_ptr<const Telegram> telegram);
void process_SM100Config(std::shared_ptr<const Telegram> telegram);
void process_SM100Status(std::shared_ptr<const Telegram> telegram);
void process_SM100Status2(std::shared_ptr<const Telegram> telegram);
void process_SM100Energy(std::shared_ptr<const Telegram> telegram);
void process_ISM1StatusMessage(std::shared_ptr<const Telegram> telegram);
void process_ISM1Set(std::shared_ptr<const Telegram> telegram);
};
} // namespace emsesp
#endif

60
src/devices/switch.cpp Normal file
View File

@@ -0,0 +1,60 @@
/*
* 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 "switch.h"
// MAKE_PSTR_WORD(switch)
namespace emsesp {
REGISTER_FACTORY(Switch, EMSdevice::DeviceType::SWITCH);
MAKE_PSTR(logger_name, "switch")
uuid::log::Logger Switch::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
// telegram handlers
// register_telegram_type(EMS_TYPE_XX, "XX", false, std::bind(&Controller::process_XX, this, _1));
// MQTT callbacks
// register_mqtt_topic("cmd", std::bind(&Controller::cmd, this, _1));
}
void Switch::add_context_menu() {
}
// 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
}
// publish values via MQTT
void Switch::publish_values() {
}
// check to see if values have been updated
bool Switch::updated_values() {
return false;
}
// add console commands
void Switch::console_commands() {
}
} // namespace emsesp

51
src/devices/switch.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* 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 EMSESP_SWITCH_H
#define EMSESP_SWITCH_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "helpers.h"
#include "mqtt.h"
namespace emsesp {
class Switch : public EMSdevice {
public:
Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
private:
static uuid::log::Logger logger_;
void console_commands();
};
} // namespace emsesp
#endif

1888
src/devices/thermostat.cpp Normal file

File diff suppressed because it is too large Load Diff

272
src/devices/thermostat.h Normal file
View File

@@ -0,0 +1,272 @@
/*
* 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 EMSESP_THERMOSTAT_H
#define EMSESP_THERMOSTAT_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "telegram.h"
#include "emsesp.h"
#include "helpers.h"
#include "mqtt.h"
#include <vector>
namespace emsesp {
class Thermostat : public EMSdevice {
public:
Thermostat(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand);
class HeatingCircuit {
public:
HeatingCircuit(const uint8_t hc_num, const uint16_t monitor_typeid, const uint16_t set_typeid)
: hc_num_(hc_num)
, monitor_typeid_(monitor_typeid)
, set_typeid_(set_typeid) {
}
~HeatingCircuit() = default;
int16_t setpoint_roomTemp = EMS_VALUE_SHORT_NOTSET;
int16_t curr_roomTemp = EMS_VALUE_SHORT_NOTSET;
uint8_t mode = EMS_VALUE_UINT_NOTSET;
uint8_t mode_type = EMS_VALUE_UINT_NOTSET;
uint8_t summer_mode = EMS_VALUE_UINT_NOTSET;
uint8_t holiday_mode = EMS_VALUE_UINT_NOTSET;
uint8_t daytemp = EMS_VALUE_UINT_NOTSET;
uint8_t nighttemp = EMS_VALUE_UINT_NOTSET;
uint8_t holidaytemp = EMS_VALUE_UINT_NOTSET;
uint8_t heatingtype = EMS_VALUE_UINT_NOTSET; // type of heating: 1 radiator, 2 convectors, 3 floors, 4 room supply
uint8_t targetflowtemp = EMS_VALUE_UINT_NOTSET;
uint8_t summertemp = EMS_VALUE_UINT_NOTSET;
uint8_t nofrosttemp = EMS_VALUE_UINT_NOTSET;
uint8_t designtemp = EMS_VALUE_UINT_NOTSET; // heatingcurve design temp at MinExtTemp
int8_t offsettemp = EMS_VALUE_INT_NOTSET; // heatingcurve offest temp at roomtemp signed!
uint8_t hc_num() const {
return hc_num_; // 1..10
}
uint8_t get_mode(uint8_t flags) const;
uint8_t get_mode_type(uint8_t flags) const;
uint16_t monitor_typeid() const {
return monitor_typeid_;
}
uint16_t set_typeid() const {
return set_typeid_;
}
enum Mode : uint8_t { UNKNOWN, OFF, MANUAL, AUTO, DAY, NIGHT, HEAT, NOFROST, ECO, HOLIDAY, COMFORT, OFFSET, DESIGN, SUMMER };
// for sorting
friend inline bool operator<(const std::shared_ptr<HeatingCircuit> & lhs, const std::shared_ptr<HeatingCircuit> & rhs) {
return (lhs->hc_num_ < rhs->hc_num_);
}
private:
uint8_t hc_num_; // 1..10
uint16_t monitor_typeid_;
uint16_t set_typeid_;
};
std::string mode_tostring(uint8_t mode) const;
virtual void show_values(uuid::console::Shell & shell);
virtual void publish_values();
virtual bool updated_values();
virtual void add_context_menu();
bool can_write() const {
return ((flags() & EMSdevice::EMS_DEVICE_FLAG_NO_WRITE) == EMSdevice::EMS_DEVICE_FLAG_NO_WRITE);
}
// each thermostat has a list of heating controller type IDs for reading and writing
std::vector<uint16_t> monitor_typeids;
std::vector<uint16_t> set_typeids;
std::vector<uint16_t> timer_typeids;
private:
static uuid::log::Logger logger_;
void console_commands(Shell & shell, unsigned int context);
void init_mqtt();
std::string datetime_; // date and time stamp
uint8_t mqtt_format_; // single, nested or ha
// Installation parameters
uint8_t ibaMainDisplay_ =
EMS_VALUE_UINT_NOTSET; // 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
uint8_t ibaLanguage_ = EMS_VALUE_UINT_NOTSET; // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian
int8_t ibaCalIntTemperature_ = EMS_VALUE_INT_NOTSET; // offset int. temperature sensor, by * 0.1 Kelvin (-5.0 to 5.0K)
int8_t ibaMinExtTemperature_ = EMS_VALUE_INT_NOTSET; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1
uint8_t ibaBuildingType_ = EMS_VALUE_UINT_NOTSET; // building type: 0 = light, 1 = medium, 2 = heavy
uint8_t ibaClockOffset_ = EMS_VALUE_UINT_NOTSET; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s
int8_t dampedoutdoortemp_ = EMS_VALUE_INT_NOTSET;
uint16_t tempsensor1_ = EMS_VALUE_USHORT_NOTSET;
uint16_t tempsensor2_ = EMS_VALUE_USHORT_NOTSET;
uint8_t wwSystem_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwExtra_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwMode_ = EMS_VALUE_UINT_NOTSET;
std::vector<std::shared_ptr<HeatingCircuit>> heating_circuits_; // each thermostat can have multiple heating circuits
// Generic Types
static constexpr uint16_t EMS_TYPE_RCTime = 0x06; // time
static constexpr uint16_t EMS_TYPE_RCOutdoorTemp = 0xA3; // is an automatic thermostat broadcast, outdoor external temp
// Type offsets
static constexpr uint8_t EMS_OFFSET_RC10StatusMessage_setpoint = 1; // setpoint temp
static constexpr uint8_t EMS_OFFSET_RC10StatusMessage_curr = 2; // current temp
static constexpr uint8_t EMS_OFFSET_RC10Set_temp = 4; // position of thermostat setpoint temperature
static constexpr uint8_t EMS_OFFSET_RC20StatusMessage_setpoint = 1; // setpoint temp
static constexpr uint8_t EMS_OFFSET_RC20StatusMessage_curr = 2; // current temp
static constexpr uint8_t EMS_OFFSET_RC20Set_mode = 23; // position of thermostat mode
static constexpr uint8_t EMS_OFFSET_RC20Set_temp = 28; // position of thermostat setpoint temperature
static constexpr uint8_t EMS_OFFSET_RC20_2_Set_mode = 3; // ES72 - see https://github.com/proddy/EMS-ESP/issues/334
static constexpr uint8_t EMS_OFFSET_RC20_2_Set_temp_night = 1; // ES72
static constexpr uint8_t EMS_OFFSET_RC20_2_Set_temp_day = 2; // ES72
static constexpr uint8_t EMS_OFFSET_RC30StatusMessage_setpoint = 1; // setpoint temp
static constexpr uint8_t EMS_OFFSET_RC30StatusMessage_curr = 2; // current temp
static constexpr uint8_t EMS_OFFSET_RC30Set_mode = 23; // position of thermostat mode
static constexpr uint8_t EMS_OFFSET_RC30Set_temp = 28; // position of thermostat setpoint temperature
static constexpr uint8_t EMS_OFFSET_RC35StatusMessage_setpoint = 2; // desired temp
static constexpr uint8_t EMS_OFFSET_RC35StatusMessage_curr = 3; // current temp
static constexpr uint8_t EMS_OFFSET_RC35StatusMessage_mode = 1; // day mode, also summer on RC3's
static constexpr uint8_t EMS_OFFSET_RC35StatusMessage_mode1 = 0; // for holiday mode
static constexpr uint8_t EMS_OFFSET_RC35Set_mode = 7; // position of thermostat mode
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_day = 2; // position of thermostat setpoint temperature for day time
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_night = 1; // position of thermostat setpoint temperature for night time
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_holiday = 3; // temp during holiday mode
static constexpr uint8_t EMS_OFFSET_RC35Set_heatingtype = 0; // e.g. floor heating = 3
static constexpr uint8_t EMS_OFFSET_RC35Set_targetflowtemp = 14; // target flow temperature
static constexpr uint8_t EMS_OFFSET_RC35Set_seltemp = 37; // selected temp
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_offset = 6;
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_design = 17;
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_summer = 22;
static constexpr uint8_t EMS_OFFSET_RC35Set_temp_nofrost = 23;
static constexpr uint8_t EMS_OFFSET_EasyStatusMessage_setpoint = 10; // setpoint temp
static constexpr uint8_t EMS_OFFSET_EasyStatusMessage_curr = 8; // current temp
static constexpr uint8_t EMS_OFFSET_RCPLUSStatusMessage_mode = 10; // thermostat mode (auto, manual)
static constexpr uint8_t EMS_OFFSET_RCPLUSStatusMessage_setpoint = 3; // setpoint temp
static constexpr uint8_t EMS_OFFSET_RCPLUSStatusMessage_curr = 0; // current temp
static constexpr uint8_t EMS_OFFSET_RCPLUSStatusMessage_currsetpoint = 6; // target setpoint temp
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_mode = 0; // operation mode(Auto=0xFF, Manual=0x00)
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_temp_comfort3 = 1; // comfort3 level
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_temp_comfort2 = 2; // comfort2 level
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_temp_comfort1 = 3; // comfort1 level
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_temp_eco = 4; // eco level
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_temp_setpoint = 8; // temp setpoint, when changing of templevel (in auto) value is reset to FF
static constexpr uint8_t EMS_OFFSET_RCPLUSSet_manual_setpoint = 10; // manual setpoint
static constexpr uint8_t EMS_OFFSET_JunkersStatusMessage_daymode = 0; // 3 = day, 2 = night, 1 = nofrost
static constexpr uint8_t EMS_OFFSET_JunkersStatusMessage_mode = 1; // current mode, 1 = manual, 2 = auto
static constexpr uint8_t EMS_OFFSET_JunkersStatusMessage_setpoint = 2; // setpoint temp
static constexpr uint8_t EMS_OFFSET_JunkersStatusMessage_curr = 4; // current temp
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage_day_temp = 17; // EMS offset to set temperature on thermostat for day mode
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage_night_temp = 16; // EMS offset to set temperature on thermostat for night mode
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage_no_frost_temp = 15; // EMS offset to set temperature on thermostat for no frost mode
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage_set_mode = 14; // EMS offset to set mode on thermostat
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_set_mode = 4; // EMS offset to set mode on thermostat
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_no_frost_temp = 5;
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage2_eco_temp = 6;
static constexpr uint8_t EMS_OFFSET_JunkersSetMessage3_heat = 7;
#define AUTO_HEATING_CIRCUIT 0
#define DEFAULT_HEATING_CIRCUIT 1
// Installation settings
static constexpr uint8_t EMS_TYPE_IBASettings = 0xA5; // installation settings
static constexpr uint8_t EMS_TYPE_wwSettings = 0x37; // ww settings
std::shared_ptr<Thermostat::HeatingCircuit> heating_circuit(std::shared_ptr<const Telegram> telegram);
std::shared_ptr<Thermostat::HeatingCircuit> heating_circuit(const uint8_t hc_num);
void process_RCOutdoorTemp(std::shared_ptr<const Telegram> telegram);
void process_IBASettings(std::shared_ptr<const Telegram> telegram);
void process_RCTime(std::shared_ptr<const Telegram> telegram);
void process_RC35wwSettings(std::shared_ptr<const Telegram> telegram);
void process_RC35Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC35Set(std::shared_ptr<const Telegram> telegram);
void process_RC30Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC30Set(std::shared_ptr<const Telegram> telegram);
void process_RC20Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC20Set(std::shared_ptr<const Telegram> telegram);
void process_RC20Remote(std::shared_ptr<const Telegram> telegram);
void process_RC20Monitor_2(std::shared_ptr<const Telegram> telegram);
void process_RC20Set_2(std::shared_ptr<const Telegram> telegram);
void process_RC10Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC10Set(std::shared_ptr<const Telegram> telegram);
void process_RC300Monitor(std::shared_ptr<const Telegram> telegram);
void process_RC300Set(std::shared_ptr<const Telegram> telegram);
void process_JunkersMonitor(std::shared_ptr<const Telegram> telegram);
void process_JunkersSet(std::shared_ptr<const Telegram> telegram);
void process_EasyMonitor(std::shared_ptr<const Telegram> telegram);
void process_RC300WWmode(std::shared_ptr<const Telegram> telegram);
// set functions
void set_settings_minexttemp(const int8_t mt);
void set_settings_calinttemp(const int8_t ct);
void set_settings_clockoffset(const int8_t co);
void set_settings_display(const uint8_t ds);
void set_settings_building(const uint8_t bg);
void set_settings_language(const uint8_t lg);
void set_control(const uint8_t ctrl, const uint8_t hc_num);
void set_ww_mode(const std::string & mode);
void set_holiday(const char * hd, const uint8_t hc_num);
void set_datetime(const char * dt);
void set_pause(const uint8_t hrs, const uint8_t hc_num);
void set_party(const uint8_t hrs, const uint8_t hc_num);
void set_mode(const uint8_t mode, const uint8_t hc_num);
void set_mode(const std::string & mode, const uint8_t hc_num);
void set_temperature(const float temperature, const std::string & mode, const uint8_t hc_num);
void set_temperature(const float temperature, const uint8_t mode, const uint8_t hc_num);
// MQTT functions
void thermostat_cmd(const char * message);
void thermostat_cmd_temp(const char * message);
void thermostat_cmd_mode(const char * message);
};
} // namespace emsesp
#endif

View File

@@ -1,228 +0,0 @@
/*
* Dallas support for external settings
* Copied from Espurna - Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
*/
#include "ds18.h"
std::vector<ds_device_t> _devices;
DS18::DS18() {
_wire = nullptr;
_count = 0;
_gpio = GPIO_NONE;
_parasite = 0;
}
DS18::~DS18() {
if (_wire) {
delete _wire;
}
}
// init
void DS18::setup(uint8_t gpio, bool parasite) {
_gpio = gpio;
_parasite = (parasite ? 1 : 0);
// OneWire
if (_wire) {
delete _wire;
}
_wire = new OneWire(_gpio);
}
// clear list and scan for devices
uint8_t DS18::scan() {
_devices.clear();
uint8_t count = loadDevices(); // start the search
// If no devices found check again pulling up the line
if (count == 0) {
pinMode(_gpio, INPUT_PULLUP);
count = loadDevices();
}
_count = count;
return count;
}
void DS18::loop() {
// we either start a conversion or read the scratchpad
static bool conversion = true;
if (conversion) {
_wire->reset();
_wire->skip();
_wire->write(DS18_CMD_START_CONVERSION, _parasite);
} else {
// Read scratchpads
for (unsigned char index = 0; index < _devices.size(); index++) {
if (_wire->reset() == 0) {
_devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error
return;
}
// Read each scratchpad
_wire->select(_devices[index].address);
_wire->write(DS18_CMD_READ_SCRATCHPAD);
uint8_t data[DS18_DATA_SIZE];
for (unsigned char i = 0; i < DS18_DATA_SIZE; i++) {
data[i] = _wire->read();
}
if (_wire->reset() != 1) {
_devices[index].data[0] = _devices[index].data[0] + 1; // Force a CRC check error
return;
}
memcpy(_devices[index].data, data, DS18_DATA_SIZE);
}
}
conversion = !conversion;
}
// return string of the device, with name and address
char * DS18::getDeviceType(char * buffer, unsigned char index) {
uint8_t size = 128;
if (index < _count) {
unsigned char chip_id = chip(index);
if (chip_id == DS18_CHIP_DS18S20) {
strlcpy(buffer, "DS18S20", size);
} else if (chip_id == DS18_CHIP_DS18B20) {
strlcpy(buffer, "DS18B20", size);
} else if (chip_id == DS18_CHIP_DS1822) {
strlcpy(buffer, "DS1822", size);
} else if (chip_id == DS18_CHIP_DS1825) {
strlcpy(buffer, "DS1825", size);
} else {
strlcpy(buffer, "Unknown", size);
}
} else {
strlcpy(buffer, "invalid", size);
}
return buffer;
}
// return string of the device, with name and address
char * DS18::getDeviceID(char * buffer, unsigned char index) {
uint8_t size = 128;
if (index < _count) {
uint8_t * address = _devices[index].address;
char a[30] = {0};
snprintf(a, sizeof(a), "%02X%02X%02X%02X%02X%02X%02X%02X", address[0], address[1], address[2], address[3], address[4], address[5], address[6], address[7]);
strlcpy(buffer, a, size);
} else {
strlcpy(buffer, "invalid", size);
}
return buffer;
}
/*
* Read sensor values
*
* Registers:
byte 0: temperature LSB
byte 1: temperature MSB
byte 2: high alarm temp
byte 3: low alarm temp
byte 4: DS18S20: store for crc
DS18B20 & DS1822: configuration register
byte 5: internal use & crc
byte 6: DS18S20: COUNT_REMAIN
DS18B20 & DS1822: store for crc
byte 7: DS18S20: COUNT_PER_C
DS18B20 & DS1822: store for crc
byte 8: SCRATCHPAD_CRC
*/
int16_t DS18::getRawValue(unsigned char index) {
if (index >= _count) {
return 0;
}
uint8_t * data = _devices[index].data;
if (OneWire::crc8(data, DS18_DATA_SIZE - 1) != data[DS18_DATA_SIZE - 1]) {
return DS18_CRC_ERROR;
}
int16_t raw = (data[1] << 8) | data[0];
if (chip(index) == DS18_CHIP_DS18S20) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
raw = (raw & 0xFFF0) + 12 - data[6]; // "count remain" gives full 12 bit resolution
}
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) {
raw = raw & ~7; // 9 bit res, 93.75 ms
} else if (cfg == 0x20) {
raw = raw & ~3; // 10 bit res, 187.5 ms
} else if (cfg == 0x40) {
raw = raw & ~1; // 11 bit res, 375 ms
// 12 bit res, 750 ms
}
}
return raw;
}
// return real value as a float, rounded to 2 decimal places
float DS18::getValue(unsigned char index) {
int16_t raw_value = getRawValue(index);
// check if valid
if ((raw_value == DS18_CRC_ERROR) || (raw_value == DS18_DISCONNECTED)) {
return (float)DS18_DISCONNECTED;
}
// The raw temperature data is in units of sixteenths of a degree,
// so the value must first be divided by 16 in order to convert it to degrees.
float new_value = (float)(raw_value / 16.0);
// round to 2 decimal places
// https://arduinojson.org/v6/faq/how-to-configure-the-serialization-of-floats/
return (int)(new_value * 100 + 0.5) / 100.0;
}
// check for a supported DS chip version
bool DS18::validateID(unsigned char id) {
return (id == DS18_CHIP_DS18S20) || (id == DS18_CHIP_DS18B20) || (id == DS18_CHIP_DS1822) || (id == DS18_CHIP_DS1825);
}
// return the type
unsigned char DS18::chip(unsigned char index) {
if (index < _count) {
return _devices[index].address[0];
}
return 0;
}
// scan for DS sensors and load into the vector
uint8_t DS18::loadDevices() {
uint8_t address[8];
_wire->reset();
_wire->reset_search();
while (_wire->search(address)) {
// Check CRC
if (_wire->crc8(address, 7) == address[7]) {
// Check ID
if (validateID(address[0])) {
ds_device_t device;
memcpy(device.address, address, 8);
_devices.push_back(device);
}
}
}
return (_devices.size());
}

View File

@@ -1,56 +0,0 @@
/*
* Dallas support for external temperature sensors
* Copyright (C) 2017-2018 by Xose Pérez <xose dot perez at gmail dot com>
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
*/
#pragma once
#include <OneWire.h>
#include <vector>
#define DS18_CHIP_DS18S20 0x10
#define DS18_CHIP_DS1822 0x22
#define DS18_CHIP_DS18B20 0x28
#define DS18_CHIP_DS1825 0x3B
#define DS18_DATA_SIZE 9
#define DS18_DISCONNECTED -127
#define DS18_CRC_ERROR -126
#define GPIO_NONE 0x99
#define DS18_READ_INTERVAL 2000 // Force sensor read & cache every 2 seconds
#define DS18_CMD_START_CONVERSION 0x44
#define DS18_CMD_READ_SCRATCHPAD 0xBE
typedef struct {
uint8_t address[8];
uint8_t data[DS18_DATA_SIZE];
} ds_device_t;
class DS18 {
public:
DS18();
~DS18();
void setup(uint8_t gpio, bool parasite);
uint8_t scan();
void loop();
char * getDeviceType(char * s, unsigned char index);
char * getDeviceID(char * buffer, unsigned char index);
float getValue(unsigned char index);
int16_t getRawValue(unsigned char index); // raw values, needs / 16
protected:
bool validateID(unsigned char id);
unsigned char chip(unsigned char index);
uint8_t loadDevices();
OneWire * _wire;
uint8_t _count; // # devices
uint8_t _gpio; // the sensor pin
uint8_t _parasite; // parasite mode
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

574
src/ems.h
View File

@@ -1,574 +0,0 @@
/*
* Header file for ems.cpp
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
* See ChangeLog.md for history
*
*/
#pragma once
#include <Arduino.h>
#include <list> // std::list
// EMS bus IDs
#define EMS_BUSID_DEFAULT 0x0B // Default 0x0B (Service Key)
// EMS tx_mode types
#define EMS_TXMODE_DEFAULT 1 // Default (was previously known as tx_mode 2 in v1.8.x)
#define EMS_TXMODE_EMSPLUS 2 // EMS+
#define EMS_TXMODE_HT3 3 // Junkers HT3
#define EMS_ID_NONE 0x00 // used as a dest in broadcast messages and empty device IDs
#define EMS_MIN_TELEGRAM_LENGTH 6 // minimal length for a validation telegram, including CRC
#define EMS_MAX_TELEGRAM_LENGTH 32 // max length of a telegram, including CRC, for Rx and Tx.
// default values for null values
#define EMS_VALUE_BOOL_ON 0x01 // boolean true
#define EMS_VALUE_BOOL_ON2 0xFF // boolean true, EMS sometimes uses 0xFF for TRUE
#define EMS_VALUE_BOOL_OFF 0x00 // boolean false
#define EMS_VALUE_BOOL_NOTSET 0xFE // random number that's not 0, 1 or FF
#define EMS_VALUE_INT_NOTSET 0xFF // for 8-bit unsigned ints/bytes
#define EMS_VALUE_SHORT_NOTSET -32000 // 0x8300 for 2-byte signed shorts
#define EMS_VALUE_USHORT_NOTSET 32000 // 0x7D00 (was 0x8000) for 2-byte unsigned shorts
#define EMS_VALUE_USHORT_NOTVALID 0x8000 // 0x8000 for 2-byte unsigned shorts
#define EMS_VALUE_LONG_NOTSET 0xFFFFFF // for 3-byte longs
// thermostat specific
#define EMS_THERMOSTAT_MAXHC 4 // max number of heating circuits
#define EMS_THERMOSTAT_DEFAULTHC 1 // default heating circuit is 1
#define EMS_THERMOSTAT_WRITE_YES true
#define EMS_THERMOSTAT_WRITE_NO false
// mixing specific
#define EMS_MIXING_MAXHC 4 // max number of heating circuits
#define EMS_MIXING_MAXWWC 2 // max number of warm water circuits
// Device Flags
// They are unique to the model type (mixing, solar, thermostat etc)
enum EMS_DEVICE_FLAG_TYPES : uint8_t {
EMS_DEVICE_FLAG_NONE = 0,
EMS_DEVICE_FLAG_MMPLUS = 20, // mixing EMS+
EMS_DEVICE_FLAG_MM10 = 21, // mixing MM10
EMS_DEVICE_FLAG_SM10 = 10,
EMS_DEVICE_FLAG_SM100 = 11, // for SM100 and SM200
EMS_DEVICE_FLAG_EASY = 1,
EMS_DEVICE_FLAG_RC10 = 2,
EMS_DEVICE_FLAG_RC20 = 3,
EMS_DEVICE_FLAG_RC30 = 4,
EMS_DEVICE_FLAG_RC30N = 5, // newer type of RC30 with RC35 circuit
EMS_DEVICE_FLAG_RC35 = 6,
EMS_DEVICE_FLAG_RC100 = 7,
EMS_DEVICE_FLAG_RC300 = 8,
EMS_DEVICE_FLAG_RC20N = 9,
EMS_DEVICE_FLAG_JUNKERS1 = 31, // use 0x65 for HC
EMS_DEVICE_FLAG_JUNKERS2 = 32, // use 0x79 for HC, older models
EMS_DEVICE_FLAG_JUNKERS = (1 << 6), // 6th bit set if its junkers HT3
EMS_DEVICE_FLAG_NO_WRITE = (1 << 7) // top bit set if thermostat write not supported
};
// trigger settings to determine if hot tap water or the heating is active
#define EMS_BOILER_BURNPOWER_TAPWATER 100
#define EMS_BOILER_SELFLOWTEMP_HEATING 20 // was originally 70, changed to 30 for issue #193, then to 20 with issue #344
// define min & maximum setable tapwater temperatures
#define EMS_BOILER_TAPWATER_TEMPERATURE_MAX 60
#define EMS_BOILER_TAPWATER_TEMPERATURE_MIN 30
#define EMS_TX_TELEGRAM_QUEUE_MAX 50 // max size of Tx FIFO queue. Number of Tx records to send.
//#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_VERBOSE // turn on for debugging
#define EMS_SYS_LOGGING_DEFAULT EMS_SYS_LOGGING_NONE
#define EMS_SYS_DEVICEMAP_LENGTH 15 // size of the 0x07 telegram data part which stores all active EMS devices
#define EMS_MODELTYPE_UNKNOWN_STRING "unknown?" // model type text to use when discovering an unknown device
/* EMS UART transfer status */
typedef enum {
EMS_RX_STATUS_IDLE,
EMS_RX_STATUS_BUSY // Rx package is being received
} _EMS_RX_STATUS;
typedef enum {
EMS_TX_STATUS_OK,
EMS_TX_STATUS_IDLE, // ready
EMS_TX_STATUS_WAIT, // waiting for response from last Tx
EMS_TX_WTD_TIMEOUT, // watchdog timeout during send
EMS_TX_BRK_DETECT, // incoming BRK during Tx
EMS_TX_REV_DETECT // waiting to detect reverse bit
} _EMS_TX_STATUS;
#define EMS_TX_SUCCESS 0x01 // EMS single byte after a Tx Write indicating a success
#define EMS_TX_ERROR 0x04 // EMS single byte after a Tx Write indicating an error
typedef enum {
EMS_TX_TELEGRAM_INIT, // just initialized
EMS_TX_TELEGRAM_READ, // doing a read request
EMS_TX_TELEGRAM_WRITE, // doing a write request
EMS_TX_TELEGRAM_VALIDATE, // do a read but only to validate the last write
EMS_TX_TELEGRAM_RAW // sending in raw mode
} _EMS_TX_TELEGRAM_ACTION;
/* EMS logging */
typedef enum {
EMS_SYS_LOGGING_NONE, // no messages
EMS_SYS_LOGGING_RAW, // raw data mode
EMS_SYS_LOGGING_WATCH, // watch a specific type ID
EMS_SYS_LOGGING_BASIC, // only basic read/write messages
EMS_SYS_LOGGING_THERMOSTAT, // only telegrams sent from thermostat
EMS_SYS_LOGGING_SOLARMODULE, // only telegrams sent from solar module
EMS_SYS_LOGGING_MIXINGMODULE, // only telegrams sent from mixing module
EMS_SYS_LOGGING_VERBOSE, // everything
EMS_SYS_LOGGING_JABBER, // lots of debug output...
EMS_SYS_LOGGING_DEVICE // watch the device ID
} _EMS_SYS_LOGGING;
// status/counters since last power on
typedef struct {
_EMS_RX_STATUS emsRxStatus;
_EMS_TX_STATUS emsTxStatus;
uint16_t emsRxPgks; // # successfull received
uint16_t emsTxPkgs; // # successfull sent
uint16_t emxCrcErr; // CRC errors
bool emsPollEnabled; // flag enable the response to poll messages
_EMS_SYS_LOGGING emsLogging; // logging
uint16_t emsLogging_ID; // the type or device ID to watch
uint8_t emsRefreshedFlags; // fresh data, needs to be pushed out to MQTT
bool emsBusConnected; // is there an active bus
uint32_t emsRxTimestamp; // timestamp of last EMS message received
uint32_t emsPollFrequency; // time between EMS polls
bool emsTxCapable; // able to send via Tx
bool emsTxDisabled; // true to prevent all Tx
uint8_t txRetryCount; // # times the last Tx was re-sent
uint8_t emsIDMask; // Buderus: 0x00, Junkers: 0x80
uint8_t emsPollAck[1]; // acknowledge buffer for Poll
uint8_t emsTxMode; // Tx mode 1, 2 or 3
uint8_t emsbusid; // EMS bus ID, default 0x0B for Service Key
uint8_t emsMasterThermostat; // product ID for the default thermostat to use
} _EMS_Sys_Status;
// The Rx receive package
typedef struct {
unsigned long timestamp; // timestamp from millis()
uint8_t * telegram; // the full data package
uint8_t data_length; // length in bytes of the data
uint8_t length; // full length of the complete telegram
uint8_t src; // source ID
uint8_t dest; // destination ID
uint16_t type; // type ID as a 2-byte to support EMS+
uint8_t offset; // offset
uint8_t * data; // pointer to where telegram data starts
bool emsplus; // true if ems+/ems 2.0
uint8_t emsplus_type; // FF, F7 or F9
} _EMS_RxTelegram;
// The Tx send package
typedef struct {
_EMS_TX_TELEGRAM_ACTION action; // read, write, validate, init
uint8_t dest;
uint16_t type;
uint8_t offset;
uint8_t length; // full length of complete telegram, including CRC
bool emsplus; // true if ems+/ems 2.0
uint8_t dataValue; // value to validate against
uint16_t type_validate; // type to call after a successful Write command
uint8_t comparisonValue; // value to compare against during a validate command
uint8_t comparisonOffset; // offset of where the byte is we want to compare too during validation
uint16_t comparisonPostRead; // after a successful write, do a read from this type ID
unsigned long timestamp; // when created
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
} _EMS_TxTelegram;
// default empty Tx, must match struct
const _EMS_TxTelegram EMS_TX_TELEGRAM_NEW = {
EMS_TX_TELEGRAM_INIT, // action
EMS_ID_NONE, // dest
EMS_ID_NONE, // type
0, // offset
0, // length
false, // emsplus (no)
0, // dataValue
EMS_ID_NONE, // type_validate
0, // comparisonValue
0, // comparisonOffset
EMS_ID_NONE, // comparisonPostRead
0, // timestamp
{0x00} // data
};
// flags for triggering changes when EMS data is received
typedef enum : uint8_t {
EMS_DEVICE_UPDATE_FLAG_NONE = 0,
EMS_DEVICE_UPDATE_FLAG_BOILER = (1 << 0),
EMS_DEVICE_UPDATE_FLAG_THERMOSTAT = (1 << 1),
EMS_DEVICE_UPDATE_FLAG_MIXING = (1 << 2),
EMS_DEVICE_UPDATE_FLAG_SOLAR = (1 << 3),
EMS_DEVICE_UPDATE_FLAG_HEATPUMP = (1 << 4),
EMS_DEVICE_UPDATE_FLAG_SETTINGS = (1 << 5)
} _EMS_DEVICE_UPDATE_FLAG;
typedef enum : uint8_t {
EMS_DEVICE_TYPE_NONE = 0,
EMS_DEVICE_TYPE_SERVICEKEY,
EMS_DEVICE_TYPE_BOILER,
EMS_DEVICE_TYPE_THERMOSTAT,
EMS_DEVICE_TYPE_MIXING,
EMS_DEVICE_TYPE_SOLAR,
EMS_DEVICE_TYPE_HEATPUMP,
EMS_DEVICE_TYPE_GATEWAY,
EMS_DEVICE_TYPE_SWITCH,
EMS_DEVICE_TYPE_CONTROLLER,
EMS_DEVICE_TYPE_CONNECT,
EMS_DEVICE_TYPE_UNKNOWN
} _EMS_DEVICE_TYPE;
// to store mapping of device_ids to their string name
typedef struct {
_EMS_DEVICE_TYPE device_type;
char device_type_string[30];
} _EMS_Device_Types;
// mapping for EMS_Devices_Type
const _EMS_Device_Types EMS_Devices_Types[] = {
{EMS_DEVICE_TYPE_UNKNOWN, EMS_MODELTYPE_UNKNOWN_STRING}, // the first, at index 0 is reserved for "unknown"
{EMS_DEVICE_TYPE_NONE, "All"},
{EMS_DEVICE_TYPE_SERVICEKEY, "Me"},
{EMS_DEVICE_TYPE_BOILER, "Boiler"},
{EMS_DEVICE_TYPE_THERMOSTAT, "Thermostat"},
{EMS_DEVICE_TYPE_MIXING, "Mixing Module"},
{EMS_DEVICE_TYPE_SOLAR, "Solar Module"},
{EMS_DEVICE_TYPE_HEATPUMP, "Heat Pump"},
{EMS_DEVICE_TYPE_GATEWAY, "Gateway"},
{EMS_DEVICE_TYPE_SWITCH, "Switching Module"},
{EMS_DEVICE_TYPE_CONTROLLER, "Controller"},
{EMS_DEVICE_TYPE_CONNECT, "Connect Module"}
};
// to store all known EMS devices to date
typedef struct {
uint8_t product_id;
_EMS_DEVICE_TYPE type;
char device_desc[100];
uint8_t flags;
} _EMS_Device;
// for storing all recognised EMS devices
typedef struct {
_EMS_DEVICE_TYPE device_type; // type (see above)
uint8_t product_id; // product id
uint8_t device_id; // device_id
const char * device_desc_p; // pointer to description string in EMS_Devices table
char version[10]; // the version number XX.XX
bool known; // is this a known device?
} _Detected_Device;
/*
* Telegram package defintions
*/
typedef struct {
// settings
uint8_t device_id; // this is typically always 0x08
uint8_t device_flags;
const char * device_desc_p;
uint8_t product_id;
char version[10];
uint8_t brand; // 0=unknown, 1=bosch, 2=junkers, 3=buderus, 4=nefit, 5=sieger, 11=worcester
// UBAParameterWW
uint8_t wWActivated; // Warm Water activated
uint8_t wWSelTemp; // Warm Water selected temperature
uint8_t wWCircPump; // Warm Water circulation pump Available
uint8_t wWCircPumpMode; // Warm Water circulation pump mode (1 = 1x3min, ..., 6=6x3min, 7 continuous)
uint8_t wWCircPumpType; // Warm Water circulation pump type (0 = charge pump, 0xff = 3-way pump)
uint8_t wWDesinfectTemp; // Warm Water desinfection temperature
uint8_t wWComfort; // Warm water comfort or ECO mode
// UBAMonitorFast
uint8_t selFlowTemp; // Selected flow temperature
uint16_t curFlowTemp; // Current flow temperature
uint16_t wwStorageTemp1; // warm water storage temp 1
uint16_t wwStorageTemp2; // warm water storage temp 2
uint16_t retTemp; // Return temperature
uint8_t burnGas; // Gas on/off
uint8_t wWMode; // warm water mode
uint8_t fanWork; // Fan on/off
uint8_t ignWork; // Ignition on/off
uint8_t heatPmp; // Circulating pump on/off
uint8_t wWHeat; // 3-way valve on WW
uint8_t wWCirc; // Circulation on/off
uint8_t selBurnPow; // Burner max power
uint8_t curBurnPow; // Burner current power
uint16_t flameCurr; // Flame current in micro amps
uint8_t sysPress; // System pressure
char serviceCodeChar[3]; // 2 character status/service code
uint16_t serviceCode; // error/service code
// UBAMonitorSlow
int16_t extTemp; // Outside temperature
uint16_t boilTemp; // Boiler temperature
uint16_t exhaustTemp; // Exhaust temperature
uint8_t pumpMod; // Pump modulation
uint32_t burnStarts; // # burner starts
uint32_t burnWorkMin; // Total burner operating time
uint32_t heatWorkMin; // Total heat operating time
uint16_t switchTemp; // Switch temperature
// UBAMonitorWWMessage
uint8_t wWSetTmp; // set temp WW (DHW)
uint16_t wWCurTmp; // Warm Water current temperature
uint16_t wWCurTmp2; // Warm Water current temperature storage
uint32_t wWStarts; // Warm Water # starts
uint32_t wWWorkM; // Warm Water # minutes
uint8_t wWOneTime; // Warm Water one time function on/off
uint8_t wWDesinfecting; // Warm Water desinfection on/off
uint8_t wWReadiness; // Warm Water readiness on/off
uint8_t wWRecharging; // Warm Water recharge on/off
uint8_t wWTemperatureOK; // Warm Water temperature ok on/off
uint8_t wWCurFlow; // Warm Water current flow in l/min
// UBATotalUptimeMessage
uint32_t UBAuptime; // Total UBA working hours
// UBAParametersMessage
uint8_t heating_temp; // Heating temperature setting on the boiler
uint8_t pump_mod_max; // Boiler circuit pump modulation max. power
uint8_t pump_mod_min; // Boiler circuit pump modulation min. power
// calculated values
uint8_t tapwaterActive; // Hot tap water is on/off
uint8_t heatingActive; // Central heating is on/off
} _EMS_Boiler;
/*
* Telegram package defintions for Other EMS devices
*/
typedef struct {
uint8_t device_id; // the device ID of the Heat Pump (e.g. 0x30)
uint8_t device_flags;
const char * device_desc_p;
uint8_t product_id;
char version[10];
uint8_t HPModulation; // heatpump modulation in %
uint8_t HPSpeed; // speed 0-100 %
} _EMS_HeatPump;
// Mixing Module per HC
typedef struct {
uint8_t hc; // heating circuit 1, 2, 3 or 4
bool active; // true if there is data for this HC
uint16_t flowTemp;
uint8_t pumpMod;
uint8_t valveStatus;
uint8_t flowSetTemp;
} _EMS_MixingModule_HC;
// Mixing Module per WWC
typedef struct {
uint8_t wwc; // warm water circuit 1, 2
bool active; // true if there is data for this WWC
uint16_t flowTemp;
uint8_t pumpMod;
uint8_t tempStatus;
} _EMS_MixingModule_WWC;
// Mixing data
typedef struct {
uint8_t device_id;
uint8_t device_flags;
const char * device_desc_p;
uint8_t product_id;
char version[10];
_EMS_MixingModule_HC hc[EMS_MIXING_MAXHC]; // array for the 4 heating circuits
_EMS_MixingModule_WWC wwc[EMS_MIXING_MAXWWC]; // array for the 2 ww circuits
} _EMS_MixingModule;
// Solar Module - SM10/SM100/SM200/ISM1
typedef struct {
uint8_t device_id; // the device ID of the Solar Module
uint8_t device_flags; // Solar Module flags
const char * device_desc_p;
uint8_t product_id;
char version[10];
int16_t collectorTemp; // collector temp (TS1)
int16_t bottomTemp; // bottom temp (TS2)
int16_t bottomTemp2; // bottom temp cylinder 2 (TS5)
uint8_t pumpModulation; // modulation solar pump
uint8_t pump; // pump active
uint8_t valveStatus; // valve status (VS2)
int16_t setpoint_maxBottomTemp; // setpoint for maximum collector temp
uint32_t EnergyLastHour;
uint32_t EnergyToday;
uint32_t EnergyTotal;
uint32_t pumpWorkMin; // Total solar pump operating time
} _EMS_SolarModule;
// heating circuit
typedef struct {
uint8_t hc; // heating circuit 1,2, 3 or 4
bool active; // true if there is data for this HC
int16_t setpoint_roomTemp; // current set temp
int16_t curr_roomTemp; // current room temp
uint8_t mode; // 0=low, 1=manual, 2=auto (or night, day on RC35s)
uint8_t mode_type; // 0=night/eco, 1=day/comfort
uint8_t summer_mode;
uint8_t holiday_mode;
uint8_t daytemp;
uint8_t nighttemp;
uint8_t holidaytemp;
uint8_t heatingtype; // type of heating: 1 radiator, 2 convectors, 3 floors, 4 room supply
uint8_t circuitcalctemp; // calculated setpoint flow temperature
} _EMS_Thermostat_HC;
// Thermostat data
typedef struct {
uint8_t device_id; // the device ID of the thermostat
uint8_t device_flags; // thermostat model flags
const char * device_desc_p;
uint8_t product_id;
char version[10];
char datetime[25]; // HH:MM:SS DD/MM/YYYY
bool write_supported;
// Installation parameters (tested on RC30)
uint8_t ibaMainDisplay; // 00, 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
uint8_t ibaLanguage; // 01, language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian
uint8_t ibaCalIntTemperature; // 02, offset int. temperature sensor, by * 0.1 Kelvin
int16_t ibaMinExtTemperature; // 05, min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1 (actually a int8_t, coded as int16_t to benefit from negative value rendering)
uint8_t ibaBuildingType; // 06, building type: 0 = light, 1 = medium, 2 = heavy
uint8_t ibaClockOffset; // 12, offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s
_EMS_Thermostat_HC hc[EMS_THERMOSTAT_MAXHC]; // array for the 4 heating circuits
} _EMS_Thermostat;
// call back function signature for processing telegram types
typedef void (*EMS_processType_cb)(_EMS_RxTelegram * EMS_RxTelegram);
// Definition for each EMS type, including the relative callback function
typedef struct {
_EMS_DEVICE_UPDATE_FLAG device_flag;
uint16_t type;
const char typeString[30];
EMS_processType_cb processType_cb;
} _EMS_Type;
typedef enum : uint8_t {
EMS_THERMOSTAT_MODE_UNKNOWN = 0,
EMS_THERMOSTAT_MODE_OFF,
EMS_THERMOSTAT_MODE_MANUAL,
EMS_THERMOSTAT_MODE_AUTO,
EMS_THERMOSTAT_MODE_HEAT, // 'heizen'
EMS_THERMOSTAT_MODE_NIGHT,
EMS_THERMOSTAT_MODE_DAY,
EMS_THERMOSTAT_MODE_ECO, // 'sparen'
EMS_THERMOSTAT_MODE_COMFORT,
EMS_THERMOSTAT_MODE_HOLIDAY,
EMS_THERMOSTAT_MODE_NOFROST
} _EMS_THERMOSTAT_MODE;
#define EMS_THERMOSTAT_MODE_UNKNOWN_STR "unknown"
#define EMS_THERMOSTAT_MODE_OFF_STR "off"
#define EMS_THERMOSTAT_MODE_MANUAL_STR "manual"
#define EMS_THERMOSTAT_MODE_AUTO_STR "auto"
#define EMS_THERMOSTAT_MODE_HEAT_STR "heat"
#define EMS_THERMOSTAT_MODE_NIGHT_STR "night"
#define EMS_THERMOSTAT_MODE_DAY_STR "day"
#define EMS_THERMOSTAT_MODE_ECO_STR "eco"
#define EMS_THERMOSTAT_MODE_COMFORT_STR "comfort"
#define EMS_THERMOSTAT_MODE_HOLIDAY_STR "holiday"
#define EMS_THERMOSTAT_MODE_NOFROST_STR "nofrost"
// function definitions
void ems_dumpBuffer(const char * prefix, uint8_t * telegram, uint8_t length);
void ems_parseTelegram(uint8_t * telegram, uint8_t len);
void ems_init();
void ems_doReadCommand(uint16_t type, uint8_t dest, uint8_t offset = 0);
void ems_sendRawTelegram(char * telegram);
void ems_printDevices();
uint8_t ems_printDevices_s(char * buffer, uint16_t len);
void ems_printTxQueue();
void ems_testTelegram(uint8_t test_num);
void ems_startupTelegrams();
bool ems_checkEMSBUSAlive();
void ems_setSettingsLanguage(uint8_t lg);
void ems_setSettingsBuilding(uint8_t bg);
void ems_setSettingsDisplay(uint8_t ds);
void ems_setThermostatTemp(float temperature, uint8_t hc, _EMS_THERMOSTAT_MODE temptype);
void ems_setThermostatTemp(float temperature, uint8_t hc, const char * mode_s);
void ems_setThermostatMode(_EMS_THERMOSTAT_MODE mode, uint8_t hc);
void ems_setThermostatMode(const char * mode_s, uint8_t hc);
void ems_setWarmWaterTemp(uint8_t temperature);
void ems_setFlowTemp(uint8_t temperature);
void ems_setWarmWaterActivated(bool activated);
void ems_setWarmWaterOnetime(bool activated);
void ems_setWarmWaterCirculation(bool activated);
void ems_setWarmTapWaterActivated(bool activated);
void ems_setPoll(bool b);
void ems_setLogging(_EMS_SYS_LOGGING loglevel, uint16_t type_id);
void ems_setLogging(_EMS_SYS_LOGGING loglevel, bool quiet = false);
void ems_setWarmWaterModeComfort(uint8_t comfort);
void ems_setModels();
void ems_setTxDisabled(bool b);
void ems_setTxMode(uint8_t mode);
void ems_setEMSbusid(uint8_t id);
void ems_setMasterThermostat(uint8_t product_id);
char * ems_getDeviceDescription(_EMS_DEVICE_TYPE device_type, char * buffer, bool name_only = false);
bool ems_getDeviceTypeDescription(uint8_t device_id, char * buffer);
char * ems_getDeviceTypeName(_EMS_DEVICE_TYPE device_type, char * buffer);
_EMS_THERMOSTAT_MODE ems_getThermostatMode(const char * mode_s);
void ems_getThermostatValues();
void ems_getBoilerValues();
void ems_getSettingsValues();
void ems_getSolarModuleValues();
void ems_getMixingModuleValues();
char * ems_getThermostatModeString(_EMS_THERMOSTAT_MODE mode, char * mode_str);
bool ems_getPoll();
bool ems_getTxEnabled();
bool ems_getThermostatEnabled();
bool ems_getMixingModuleEnabled();
bool ems_getBoilerEnabled();
bool ems_getSolarModuleEnabled();
bool ems_getHeatPumpEnabled();
bool ems_getBusConnected();
_EMS_SYS_LOGGING ems_getLogging();
uint8_t ems_getThermostatFlags();
uint8_t ems_getSolarModuleFlags();
void ems_discoverModels();
bool ems_getTxCapable();
uint32_t ems_getPollFrequency();
bool ems_getTxDisabled();
void ems_Device_add_flags(unsigned int flags);
bool ems_Device_has_flags(unsigned int flags);
void ems_Device_remove_flags(unsigned int flags);
bool ems_isHT3();
void ems_scanDevices();
// private functions
uint8_t _crcCalculator(uint8_t * data, uint8_t len);
void _processType(_EMS_RxTelegram * EMS_RxTelegram);
void _debugPrintPackage(const char * prefix, _EMS_RxTelegram * EMS_RxTelegram, const char * color);
void _ems_clearTxData();
void _removeTxQueue();
int8_t _getHeatingCircuit(_EMS_RxTelegram * EMS_RxTelegram);
void _printMessage(_EMS_RxTelegram * EMS_RxTelegram, const int show_type = -1);
// global so can referenced in other classes
extern _EMS_Sys_Status EMS_Sys_Status;
extern _EMS_Boiler EMS_Boiler;
extern _EMS_Thermostat EMS_Thermostat;
extern _EMS_SolarModule EMS_SolarModule;
extern _EMS_HeatPump EMS_HeatPump;
extern _EMS_MixingModule EMS_MixingModule;
extern std::list<_Detected_Device> Devices;

View File

@@ -1,351 +0,0 @@
/*
* General information about known EMS devices
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
* See ChangeLog.md for History
*
*/
#pragma once
#include "ems.h"
// Fixed EMS Device IDs
#define EMS_ID_BOILER 0x08 // all UBA Boilers have 0x08
/*
* Common Type
*/
#define EMS_TYPE_Version 0x02
#define EMS_TYPE_UBADevices 0x07 // EMS connected devices
/*
* Boiler Telegram Types...
*/
#define EMS_TYPE_UBAMonitorFast 0x18 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMonitorSlow 0x19 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMonitorWWMessage 0x34 // is an automatic monitor broadcast
#define EMS_TYPE_UBAMaintenanceStatusMessage 0x1C // is an automatic monitor broadcast
#define EMS_TYPE_MC10Status 0x2A
#define EMS_TYPE_UBAParameterWW 0x33
#define EMS_TYPE_UBATotalUptimeMessage 0x14
#define EMS_TYPE_UBAFlags 0x35
#define EMS_TYPE_UBAMaintenanceSettingsMessage 0x15
#define EMS_TYPE_UBAParametersMessage 0x16
#define EMS_TYPE_UBASetPoints 0x1A
#define EMS_TYPE_UBAFunctionTest 0x1D
// EMS+ specific
#define EMS_TYPE_UBAOutdoorTemp 0xD1 // external temp
#define EMS_TYPE_UBAMonitorFast2 0xE4 // Monitor fast for newer EMS+
#define EMS_TYPE_UBAMonitorSlow2 0xE5 // Monitor slow for newer EMS+
#define EMS_OFFSET_UBAParameterWW_wwtemp 2 // WW Temperature
#define EMS_OFFSET_UBAParameterWW_wwactivated 1 // WW Activated
#define EMS_OFFSET_UBAParameterWW_wwOneTime 0x00 // WW OneTime loading
#define EMS_OFFSET_UBAParameterWW_wwCirulation 1 // WW circulation
#define EMS_OFFSET_UBAParameterWW_wwComfort 9 // WW is in comfort or eco mode
#define EMS_VALUE_UBAParameterWW_wwComfort_Hot 0x00 // the value for hot
#define EMS_VALUE_UBAParameterWW_wwComfort_Eco 0xD8 // the value for eco
#define EMS_VALUE_UBAParameterWW_wwComfort_Intelligent 0xEC // the value for intelligent
#define EMS_OFFSET_UBASetPoints_flowtemp 0 // flow temp
// Installation settings
#define EMS_TYPE_IBASettingsMessage 0xA5 // installation settings
#define EMS_OFFSET_IBASettings_Display 0 // display
#define EMS_OFFSET_IBASettings_Language 1 // language
#define EMS_OFFSET_IBASettings_MinExtTemp 5 // min. ext. temperature
#define EMS_OFFSET_IBASettings_Building 6 // building
#define EMS_OFFSET_IBASettings_CalIntTemp 2 // cal. int. temperature
#define EMS_OFFSET_IBASettings_ClockOffset 12 // clock offset
#define EMS_VALUE_IBASettings_LANG_GERMAN 0
#define EMS_VALUE_IBASettings_LANG_DUTCH 1
#define EMS_VALUE_IBASettings_LANG_FRENCH 2
#define EMS_VALUE_IBASettings_LANG_ITALIAN 3
#define EMS_VALUE_IBASettings_BUILDING_LIGHT 0
#define EMS_VALUE_IBASettings_BUILDING_MEDIUM 1
#define EMS_VALUE_IBASettings_BUILDING_HEAVY 2
#define EMS_VALUE_IBASettings_DISPLAY_INTTEMP 0
#define EMS_VALUE_IBASettings_DISPLAY_INTSETPOINT 1
#define EMS_VALUE_IBASettings_DISPLAY_EXTTEMP 2
#define EMS_VALUE_IBASettings_DISPLAY_BURNERTEMP 3
#define EMS_VALUE_IBASettings_DISPLAY_WWTEMP 4
#define EMS_VALUE_IBASettings_DISPLAY_FUNCMODE 5
#define EMS_VALUE_IBASettings_DISPLAY_TIME 6
#define EMS_VALUE_IBASettings_DISPLAY_DATE 7
#define EMS_VALUE_IBASettings_DISPLAY_SMOKETEMP 9
// Mixing Modules
// MM100/MM200 (EMS Plus)
#define EMS_TYPE_MMPLUSStatusMessage_HC1 0x01D7 // mixing status HC1
#define EMS_TYPE_MMPLUSStatusMessage_HC2 0x01D8 // mixing status HC2
#define EMS_TYPE_MMPLUSStatusMessage_HC3 0x01D9 // mixing status HC3
#define EMS_TYPE_MMPLUSStatusMessage_HC4 0x01DA // mixing status HC4
#define EMS_TYPE_MMPLUSStatusMessage_WWC1 0x0231 // mixing status WWC1
#define EMS_TYPE_MMPLUSStatusMessage_WWC2 0x0232 // mixing status WWC2
#define EMS_OFFSET_MMPLUSStatusMessage_flow_temp 3 // flow temperature
#define EMS_OFFSET_MMPLUSStatusMessage_pump_mod 5 // pump modulation
#define EMS_OFFSET_MMPLUSStatusMessage_valve_status 2 // valve in percent
#define EMS_OFFSET_MMPLUSStatusMessage_WW_flow_temp 0 // flow temperature
#define EMS_OFFSET_MMPLUSStatusMessage_WW_pump_mod 2 // pump on 6, off 0
#define EMS_OFFSET_MMPLUSStatusMessage_WW_temp_status 11 // 0,1,2
// MM10
#define EMS_TYPE_MMStatusMessage 0xAB // mixing status
#define EMS_OFFSET_MMStatusMessage_flow_set 0 // flow setpoint
#define EMS_OFFSET_MMStatusMessage_flow_temp 1 // flow temperature
#define EMS_OFFSET_MMStatusMessage_pump_mod 3 // pump modulation in percent
#define EMS_OFFSET_MMStatusMessage_valve_status 4 // valve 0..255
#define EMS_TYPE_MM10ParameterMessage 0xAC // mixing parameters
// Solar Module
// Assuming here that the SM200 behaves like SM100
#define EMS_TYPE_SM10Monitor 0x97 // SM10Monitor
#define EMS_TYPE_SM100Monitor 0x0262 // SM100Monitor
#define EMS_TYPE_SM100Status 0x0264 // SM100Status
#define EMS_TYPE_SM100Status2 0x026A // SM100Status2
#define EMS_TYPE_SM100Energy 0x028E // SM100Energy
// ISPM solar module
#define EMS_TYPE_ISM1StatusMessage 0x0003 // Solar Module Junkers ISM1 Status
#define EMS_TYPE_ISM1Set 0x0001 // for setting values of the solar module like max boiler temp
#define EMS_OFFSET_ISM1Set_MaxBoilerTemp 6 // position of max boiler temp e.g. 50 in the following example: 90 30 FF 06 00 01 50 (CRC=2C)
// Heat Pump
#define EMS_TYPE_HPMonitor1 0xE3 // HeatPump Monitor 1
#define EMS_TYPE_HPMonitor2 0xE5 // HeatPump Monitor 2
/*
* Thermostat Types
*/
// Common for all thermostats
#define EMS_TYPE_RCTime 0x06 // is an automatic thermostat broadcast
#define EMS_TYPE_RCOutdoorTempMessage 0xA3 // is an automatic thermostat broadcast, outdoor external temp
// RC10 specific
#define EMS_TYPE_RC10StatusMessage 0xB1 // is an automatic thermostat broadcast giving us temps
#define EMS_OFFSET_RC10StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC10StatusMessage_curr 2 // current temp
#define EMS_TYPE_RC10Set 0xB0 // for setting values like temp and mode
#define EMS_OFFSET_RC10Set_temp 4 // position of thermostat setpoint temperature
// RC20 specific
#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps
#define EMS_OFFSET_RC20StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC20StatusMessage_curr 2 // current temp
#define EMS_TYPE_RC20NStatusMessage 0xAE
#define EMS_OFFSET_RC20NStatusMessage_setpoint 2 // setpoint temp in AE
#define EMS_OFFSET_RC20NStatusMessage_curr 3 // current temp in AE
#define EMS_TYPE_RC20NSet 0xAD
#define EMS_OFFSET_RC20NSet_temp_day 2 // position of thermostat setpoint temperature for day time
#define EMS_OFFSET_RC20NSet_temp_night 1 // position of thermostat setpoint temperature for night time
#define EMS_OFFSET_RC20NSet_mode 3 // position mode
#define EMS_OFFSET_RC20NSet_heatingtype 0
#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode
#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode
#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature
// RC30 specific
#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps
#define EMS_OFFSET_RC30StatusMessage_setpoint 1 // setpoint temp
#define EMS_OFFSET_RC30StatusMessage_curr 2 // current temp
#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode
#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
// RC35 specific
#define EMS_TYPE_RC35StatusMessage_HC1 0x3E // is an automatic thermostat broadcast giving us temps on HC1
#define EMS_TYPE_RC35StatusMessage_HC2 0x48 // is an automatic thermostat broadcast giving us temps on HC2
#define EMS_TYPE_RC35StatusMessage_HC3 0x52 // is an automatic thermostat broadcast giving us temps on HC3
#define EMS_TYPE_RC35StatusMessage_HC4 0x5C // is an automatic thermostat broadcast giving us temps on HC4
#define EMS_OFFSET_RC35StatusMessage_setpoint 2 // desired temp
#define EMS_OFFSET_RC35StatusMessage_curr 3 // current temp
#define EMS_OFFSET_RC35StatusMessage_mode 1 // day mode, also summer on RC3's
#define EMS_OFFSET_RC35StatusMessage_mode1 0 // for holiday mode
#define EMS_TYPE_RC35Set_HC1 0x3D // for setting values like temp and mode (Working mode HC1)
#define EMS_TYPE_RC35Set_HC2 0x47 // for setting values like temp and mode (Working mode HC2)
#define EMS_TYPE_RC35Set_HC3 0x51 // for setting values like temp and mode (Working mode HC3)
#define EMS_TYPE_RC35Set_HC4 0x5B // for setting values like temp and mode (Working mode HC4)
#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode
#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time
#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time
#define EMS_OFFSET_RC35Set_temp_holiday 3 // temp during holiday mode
#define EMS_OFFSET_RC35Set_heatingtype 0 // e.g. floor heating = 3
#define EMS_OFFSET_RC35Set_circuitcalctemp 14 // calculated circuit temperature
#define EMS_OFFSET_RC35Set_seltemp 37 // selected temp
// Easy specific
#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat
#define EMS_OFFSET_EasyStatusMessage_setpoint 10 // setpoint temp
#define EMS_OFFSET_EasyStatusMessage_curr 8 // current temp
// RC1010, RC310 and RC300 specific (EMS Plus)
#define EMS_TYPE_RCPLUSStatusMessage_HC1 0x01A5 // is an automatic thermostat broadcast giving us temps for HC1
#define EMS_TYPE_RCPLUSStatusMessage_HC2 0x01A6 // is an automatic thermostat broadcast giving us temps for HC2
#define EMS_TYPE_RCPLUSStatusMessage_HC3 0x01A7 // is an automatic thermostat broadcast giving us temps for HC3
#define EMS_TYPE_RCPLUSStatusMessage_HC4 0x01A8 // is an automatic thermostat broadcast giving us temps for HC4
#define EMS_TYPE_RCPLUSStatusMode 0x1AF // summer/winter mode
#define EMS_OFFSET_RCPLUSStatusMessage_mode 10 // thermostat mode (auto, manual)
#define EMS_OFFSET_RCPLUSStatusMessage_setpoint 3 // setpoint temp
#define EMS_OFFSET_RCPLUSStatusMessage_curr 0 // current temp
#define EMS_OFFSET_RCPLUSStatusMessage_currsetpoint 6 // target setpoint temp
#define EMS_TYPE_RCPLUSSet 0x01B9 // setpoint temp message and mode
#define EMS_OFFSET_RCPLUSSet_mode 0 // operation mode(Auto=0xFF, Manual=0x00)
#define EMS_OFFSET_RCPLUSSet_temp_comfort3 1 // comfort3 level
#define EMS_OFFSET_RCPLUSSet_temp_comfort2 2 // comfort2 level
#define EMS_OFFSET_RCPLUSSet_temp_comfort1 3 // comfort1 level
#define EMS_OFFSET_RCPLUSSet_temp_eco 4 // eco level
#define EMS_OFFSET_RCPLUSSet_temp_setpoint 8 // temp setpoint, when changing of templevel (in auto) value is reset and set to FF
#define EMS_OFFSET_RCPLUSSet_manual_setpoint 10 // manual setpoint
// Junkers FR10, FR50, FW100, FW120 (EMS Plus)
// HC1 = 0x6F-0x72
#define EMS_TYPE_JunkersStatusMessage_HC1 0x6F
#define EMS_TYPE_JunkersStatusMessage_HC2 0x70
#define EMS_TYPE_JunkersStatusMessage_HC3 0x71
#define EMS_TYPE_JunkersStatusMessage_HC4 0x72
#define EMS_OFFSET_JunkersStatusMessage_daymode 0 // 3 = day, 2 = night
#define EMS_OFFSET_JunkersStatusMessage_mode 1 // current mode, 1 = manual, 2 = auto
#define EMS_OFFSET_JunkersStatusMessage_setpoint 2 // setpoint temp
#define EMS_OFFSET_JunkersStatusMessage_curr 4 // current temp
// HC1-4 0x65-0x68 - EMS_DEVICE_FLAG_JUNKERS1
// Junkers FR10, FR50, FW100, FW120
#define EMS_TYPE_JunkersSetMessage1_HC1 0x65
#define EMS_TYPE_JunkersSetMessage1_HC2 0x66
#define EMS_TYPE_JunkersSetMessage1_HC3 0x67
#define EMS_TYPE_JunkersSetMessage1_HC4 0x68
#define EMS_OFFSET_JunkersSetMessage_day_temp 0x11 // EMS offset to set temperature on thermostat for day mode
#define EMS_OFFSET_JunkersSetMessage_night_temp 0x10 // EMS offset to set temperature on thermostat for night mode
#define EMS_OFFSET_JunkersSetMessage_no_frost_temp 0x0F // EMS offset to set temperature on thermostat for no frost mode
#define EMS_OFFSET_JunkersSetMessage_set_mode 0x0E // EMS offset to set mode on thermostat
// HC1-4 0x79-0x7C - EMS_DEVICE_FLAG_JUNKERS2
// Junkers FR100
#define EMS_TYPE_JunkersSetMessage2_HC1 0x79
#define EMS_TYPE_JunkersSetMessage2_HC2 0x7A
#define EMS_TYPE_JunkersSetMessage2_HC3 0x7B
#define EMS_TYPE_JunkersSetMessage2_HC4 0x7C
#define EMS_OFFSET_JunkersSetMessage2_no_frost_temp 0x05
#define EMS_OFFSET_JunkersSetMessage2_eco_temp 0x06
#define EMS_OFFSET_JunkersSetMessage3_heat 0x07
/*
* Table of all known EMS Devices
* ProductID, DeviceType, Description, Flags
*/
static const _EMS_Device EMS_Devices[] = {
//
// UBA Masters - must have device_id of 0x08
//
{72, EMS_DEVICE_TYPE_BOILER, "MC10 Module", EMS_DEVICE_FLAG_NONE},
{123, EMS_DEVICE_TYPE_BOILER, "Buderus GBx72/Nefit Trendline/Junkers Cerapur/Worcester Greenstar Si/27i", EMS_DEVICE_FLAG_NONE},
{133, EMS_DEVICE_TYPE_BOILER, "Buderus GB125/Logamatic MC110", EMS_DEVICE_FLAG_NONE},
{115, EMS_DEVICE_TYPE_BOILER, "Nefit Topline/Buderus GB162", EMS_DEVICE_FLAG_NONE},
{203, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax U122/Junkers Cerapur", EMS_DEVICE_FLAG_NONE},
{208, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax plus/GB192/Bosch Condens GC9000", EMS_DEVICE_FLAG_NONE},
{64, EMS_DEVICE_TYPE_BOILER, "Sieger BK13,BK15/Nefit Smartline/Buderus GB1x2", EMS_DEVICE_FLAG_NONE},
{234, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax Plus GB122", EMS_DEVICE_FLAG_NONE},
{84, EMS_DEVICE_TYPE_BOILER, "Buderus Logamax Plus GB022", EMS_DEVICE_FLAG_NONE},
{95, EMS_DEVICE_TYPE_BOILER, "Bosch Condens 2500/Buderus Logamax,Logomatic/Junkers Cerapur Top/Worcester Greenstar i/Generic HT3", EMS_DEVICE_FLAG_NONE},
{122, EMS_DEVICE_TYPE_BOILER, "Nefit Proline", EMS_DEVICE_FLAG_NONE},
{170, EMS_DEVICE_TYPE_BOILER, "Buderus Logano GB212", EMS_DEVICE_FLAG_NONE},
{172, EMS_DEVICE_TYPE_BOILER, "Nefit Enviline", EMS_DEVICE_FLAG_NONE},
//
// Solar Modules - type 0x30
//
{73, EMS_DEVICE_TYPE_SOLAR, "SM10 Solar Module", EMS_DEVICE_FLAG_SM10},
{163, EMS_DEVICE_TYPE_SOLAR, "SM100 Solar Module", EMS_DEVICE_FLAG_SM100},
{164, EMS_DEVICE_TYPE_SOLAR, "SM200 Solar Module", EMS_DEVICE_FLAG_SM100},
{101, EMS_DEVICE_TYPE_SOLAR, "Junkers ISM1 Solar Module", EMS_DEVICE_FLAG_SM100},
{162, EMS_DEVICE_TYPE_SOLAR, "SM50 Solar Module", EMS_DEVICE_FLAG_SM100},
//
// Mixing Devices - type 0x20 or 0x21
//
{160, EMS_DEVICE_TYPE_MIXING, "MM100 Mixing Module", EMS_DEVICE_FLAG_MMPLUS},
{161, EMS_DEVICE_TYPE_MIXING, "MM200 Mixing Module", EMS_DEVICE_FLAG_MMPLUS},
{69, EMS_DEVICE_TYPE_MIXING, "MM10 Mixing Module", EMS_DEVICE_FLAG_MM10},
{159, EMS_DEVICE_TYPE_MIXING, "MM50 Mixing Module", EMS_DEVICE_FLAG_MMPLUS},
//
// HeatPump - type 0x38
//
{252, EMS_DEVICE_TYPE_HEATPUMP, "HeatPump Module", EMS_DEVICE_FLAG_NONE},
{200, EMS_DEVICE_TYPE_HEATPUMP, "HeatPump Module", EMS_DEVICE_FLAG_NONE},
//
// Other devices like controllers and modems
// such as 0x11 for Switching, 0x09 for controllers, 0x02 for Connect, 0x48 for Gateway
//
{71, EMS_DEVICE_TYPE_SWITCH, "WM10 Switch Module", EMS_DEVICE_FLAG_NONE}, // 0x11
{68, EMS_DEVICE_TYPE_CONTROLLER, "BC10/RFM20 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x09
{218, EMS_DEVICE_TYPE_CONTROLLER, "Junkers M200/Buderus RFM200 Receiver", EMS_DEVICE_FLAG_NONE}, // 0x50
{190, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{114, EMS_DEVICE_TYPE_CONTROLLER, "BC10 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{125, EMS_DEVICE_TYPE_CONTROLLER, "BC25 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{169, EMS_DEVICE_TYPE_CONTROLLER, "BC40 Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{152, EMS_DEVICE_TYPE_CONTROLLER, "Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{95, EMS_DEVICE_TYPE_CONTROLLER, "HT3 Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{209, EMS_DEVICE_TYPE_CONTROLLER, "W-B ErP Boiler Control Panel", EMS_DEVICE_FLAG_NONE}, // 0x09
{230, EMS_DEVICE_TYPE_CONTROLLER, "BC Base Controller", EMS_DEVICE_FLAG_NONE}, // 0x09
{205, EMS_DEVICE_TYPE_CONNECT, "Nefit Moduline Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02
{206, EMS_DEVICE_TYPE_CONNECT, "Bosch Easy Connect", EMS_DEVICE_FLAG_NONE}, // 0x02
{171, EMS_DEVICE_TYPE_CONNECT, "EMS-OT OpenTherm converter", EMS_DEVICE_FLAG_NONE}, // 0x02
{189, EMS_DEVICE_TYPE_GATEWAY, "Web Gateway KM200", EMS_DEVICE_FLAG_NONE}, // 0x48
{94, EMS_DEVICE_TYPE_GATEWAY, "RC Remote Device", EMS_DEVICE_FLAG_NONE}, // 0x18
{207, EMS_DEVICE_TYPE_CONTROLLER, "Worcester Sense II/Bosch CS200 Solar Controller", EMS_DEVICE_FLAG_NONE}, // 0x10
//
// Thermostats, typically device id of 0x10, 0x17, 0x18, 0x38 (RC100), 0x39 (Easy)
//
// Easy devices - not currently supporting write operations
{202, EMS_DEVICE_TYPE_THERMOSTAT, "Logamatic TC100/Nefit Moduline Easy", EMS_DEVICE_FLAG_EASY | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{203, EMS_DEVICE_TYPE_THERMOSTAT, "Bosch EasyControl CT200", EMS_DEVICE_FLAG_EASY | EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
{157, EMS_DEVICE_TYPE_THERMOSTAT, "Buderus RC200/Bosch CW100/Junkers CW100", EMS_DEVICE_FLAG_NO_WRITE}, // 0x18, cannot write
// Buderus/Nefit specific
{79, EMS_DEVICE_TYPE_THERMOSTAT, "RC10/Moduline 100", EMS_DEVICE_FLAG_RC10}, // 0x17
{80, EMS_DEVICE_TYPE_THERMOSTAT, "Moduline 200", EMS_DEVICE_FLAG_RC10}, // 0x17
{77, EMS_DEVICE_TYPE_THERMOSTAT, "RC20/Moduline 300", EMS_DEVICE_FLAG_RC20}, // 0x17
{67, EMS_DEVICE_TYPE_THERMOSTAT, "RC30", EMS_DEVICE_FLAG_RC30N}, // 0x10 - based on RC35
{78, EMS_DEVICE_TYPE_THERMOSTAT, "Moduline 400", EMS_DEVICE_FLAG_RC30}, // 0x10
{86, EMS_DEVICE_TYPE_THERMOSTAT, "RC35", EMS_DEVICE_FLAG_RC35}, // 0x10
{93, EMS_DEVICE_TYPE_THERMOSTAT, "RC20RF", EMS_DEVICE_FLAG_RC20}, // 0x19
{158, EMS_DEVICE_TYPE_THERMOSTAT, "RC300/RC310/Moduline 3000/Bosch CW400/W-B Sense II", EMS_DEVICE_FLAG_RC300}, // 0x10
{165, EMS_DEVICE_TYPE_THERMOSTAT, "RC100/Moduline 1000/1010", EMS_DEVICE_FLAG_RC100}, // 0x18, 0x38
// Sieger
{76, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES73", EMS_DEVICE_FLAG_RC35}, // 0x10
{113, EMS_DEVICE_TYPE_THERMOSTAT, "Sieger ES72/Buderus RC20", EMS_DEVICE_FLAG_RC20N}, // 0x17
// Junkers - all 0x10
{105, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW100", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10
{106, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW200", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10
{107, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR100", EMS_DEVICE_FLAG_JUNKERS2}, // 0x10
{108, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR110", EMS_DEVICE_FLAG_JUNKERS2}, // 0x10
{111, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR10", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10
{147, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR50", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10
{191, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FR120", EMS_DEVICE_FLAG_JUNKERS1}, // 0x10
{192, EMS_DEVICE_TYPE_THERMOSTAT, "Junkers FW120", EMS_DEVICE_FLAG_JUNKERS1} // 0x10
};

View File

@@ -1,304 +0,0 @@
/*
* Generic utils
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
*/
#include "ems_utils.h"
// convert float to char
char * _float_to_char(char * a, float f, uint8_t precision) {
long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
char * ret = a;
long whole = (long)f;
itoa(whole, a, 10);
while (*a != '\0')
a++;
*a++ = '.';
long decimal = abs((long)((f - whole) * p[precision]));
itoa(decimal, a, 10);
return ret;
}
// convert bool to text. bools are stored as bytes
char * _bool_to_char(char * s, uint8_t value) {
if ((value == EMS_VALUE_BOOL_ON) || (value == EMS_VALUE_BOOL_ON2)) {
strlcpy(s, "on", sizeof(s));
} else if (value == EMS_VALUE_BOOL_OFF) {
strlcpy(s, "off", sizeof(s));
} else { // EMS_VALUE_BOOL_NOTSET
strlcpy(s, "?", sizeof(s));
}
return s;
}
// convert short (two bytes) to text string and returns string
// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100
// negative values are assumed stored as 1-compliment (https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c)
char * _short_to_char(char * s, int16_t value, uint8_t decimals) {
// remove errors or invalid values
if (value == EMS_VALUE_SHORT_NOTSET) {
strlcpy(s, "?", 10);
return (s);
}
// just print
if (decimals == 0) {
ltoa(value, s, 10);
return (s);
}
// check for negative values
if (value < 0) {
strlcpy(s, "-", 10);
value *= -1; // convert to positive
} else {
strlcpy(s, "", 10);
}
// do floating point
char s2[10] = {0};
if (decimals == 2) {
// divide by 2
strlcat(s, ltoa(value / 2, s2, 10), 10);
strlcat(s, ".", 10);
strlcat(s, ((value & 0x01) ? "5" : "0"), 10);
} else {
strlcat(s, ltoa(value / (decimals * 10), s2, 10), 10);
strlcat(s, ".", 10);
strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10);
}
return s;
}
// convert unsigned short (two bytes) to text string and prints it
// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100
char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals) {
// remove errors or invalid values
if (value == EMS_VALUE_USHORT_NOTSET) { // 0x7D00
strlcpy(s, "?", 10);
return (s);
}
// just print
if (decimals == 0) {
ltoa(value, s, 10);
return (s);
}
// do floating point
char s2[10] = {0};
if (decimals == 2) {
// divide by 2
strlcpy(s, ltoa(value / 2, s2, 10), 10);
strlcat(s, ".", 10);
strlcat(s, ((value & 0x01) ? "5" : "0"), 10);
} else {
strlcpy(s, ltoa(value / (decimals * 10), s2, 10), 10);
strlcat(s, ".", 10);
strlcat(s, ltoa(value % (decimals * 10), s2, 10), 10);
}
return s;
}
// takes a signed short value (2 bytes), converts to a fraction and prints it
// decimals: 0=no division, 1=divide value by 10 (default), 2=divide by 2, 10=divide value by 100
void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals) {
static char buffer[200] = {0};
static char s[20] = {0};
strlcpy(buffer, " ", sizeof(buffer));
strlcat(buffer, prefix, sizeof(buffer));
strlcat(buffer, ": ", sizeof(buffer));
strlcat(buffer, _short_to_char(s, value, decimals), sizeof(buffer));
if (postfix != nullptr) {
strlcat(buffer, " ", sizeof(buffer));
strlcat(buffer, postfix, sizeof(buffer));
}
myDebug(buffer);
}
// takes a unsigned short value (2 bytes), converts to a fraction and prints it
// decimals: 0 = no division, 1=divide value by 10, 2=divide by 2, 10=divide value by 100
void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals) {
static char buffer[200] = {0};
static char s[20] = {0};
strlcpy(buffer, " ", sizeof(buffer));
strlcat(buffer, prefix, sizeof(buffer));
strlcat(buffer, ": ", sizeof(buffer));
strlcat(buffer, _ushort_to_char(s, value, decimals), sizeof(buffer));
if (postfix != nullptr) {
strlcat(buffer, " ", sizeof(buffer));
strlcat(buffer, postfix, sizeof(buffer));
}
myDebug(buffer);
}
// convert int (single byte) to text value and returns it
char * _int_to_char(char * s, uint8_t value, uint8_t div) {
s[0] = '\0'; // reset
if (value == EMS_VALUE_INT_NOTSET) {
strlcpy(s, "?", sizeof(s));
return (s);
}
static char s2[5] = {0};
switch (div) {
case 1:
itoa(value, s, 10);
break;
case 2:
strlcpy(s, itoa(value >> 1, s2, 10), 5);
strlcat(s, ".", sizeof(s));
strlcat(s, ((value & 0x01) ? "5" : "0"), 5);
break;
case 10:
strlcpy(s, itoa(value / 10, s2, 10), 5);
strlcat(s, ".", sizeof(s));
strlcat(s, itoa(value % 10, s2, 10), 5);
break;
default:
itoa(value, s, 10);
break;
}
return s;
}
// takes an int value (1 byte), converts to a fraction and prints
void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div) {
static char buffer[200] = {0};
static char s[20] = {0};
strlcpy(buffer, " ", sizeof(buffer));
strlcat(buffer, prefix, sizeof(buffer));
strlcat(buffer, ": ", sizeof(buffer));
strlcat(buffer, _int_to_char(s, value, div), sizeof(buffer));
if (postfix != nullptr) {
strlcat(buffer, " ", sizeof(buffer));
strlcat(buffer, postfix, sizeof(buffer));
}
myDebug(buffer);
}
// takes a long value at prints it to debug log and prints
void _renderLongValue(const char * prefix, const char * postfix, uint32_t value, uint8_t div) {
static char buffer[200] = {0};
strlcpy(buffer, " ", sizeof(buffer));
strlcat(buffer, prefix, sizeof(buffer));
strlcat(buffer, ": ", sizeof(buffer));
if (value == EMS_VALUE_LONG_NOTSET) {
strlcat(buffer, "?", sizeof(buffer));
} else {
char s[20] = {0};
if (div == 0) {
strlcat(buffer, ltoa(value, s, 10), sizeof(buffer));
} else {
strlcat(buffer, ltoa(value / 10, s, 10), sizeof(buffer));
strlcat(buffer, ".", sizeof(buffer));
strlcat(buffer, ltoa(value % 10, s, 10), sizeof(buffer));
}
}
if (postfix != nullptr) {
strlcat(buffer, " ", sizeof(buffer));
strlcat(buffer, postfix, sizeof(buffer));
}
myDebug(buffer);
}
// takes a bool value at prints it to debug log and prints
void _renderBoolValue(const char * prefix, uint8_t value) {
static char buffer[200] = {0};
static char s[20] = {0};
strlcpy(buffer, " ", sizeof(buffer));
strlcat(buffer, prefix, sizeof(buffer));
strlcat(buffer, ": ", sizeof(buffer));
strlcat(buffer, _bool_to_char(s, value), sizeof(buffer));
myDebug(buffer);
}
// like itoa but for hex, and quicker
char * _hextoa(uint8_t value, char * buffer) {
char * p = buffer;
byte nib1 = (value >> 4) & 0x0F;
byte nib2 = (value >> 0) & 0x0F;
*p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA;
*p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA;
*p = '\0'; // null terminate just in case
return buffer;
}
// for decimals 0 to 99, printed as a 2 char string
char * _smallitoa(uint8_t value, char * buffer) {
buffer[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0';
buffer[1] = (value % 10) + '0';
buffer[2] = '\0';
return buffer;
}
/* for decimals 0 to 999, printed as a string
*/
char * _smallitoa3(uint16_t value, char * buffer) {
buffer[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0';
buffer[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0';
buffer[2] = (value % 10) + '0';
buffer[3] = '\0';
return buffer;
}
// used to read the next string from an input buffer and convert to an 8 bit int
uint8_t _readIntNumber() {
char * numTextPtr = strtok(nullptr, ", \n");
if (numTextPtr == nullptr) {
return 0;
}
return atoi(numTextPtr);
}
// used to read the next string from an input buffer and convert to a float
float _readFloatNumber() {
char * numTextPtr = strtok(nullptr, ", \n");
if (numTextPtr == nullptr) {
return 0;
}
return atof(numTextPtr);
}
// used to read the next string from an input buffer as a hex value and convert to a 16 bit int
uint16_t _readHexNumber() {
char * numTextPtr = strtok(nullptr, ", \n");
if (numTextPtr == nullptr) {
return 0;
}
return (uint16_t)strtol(numTextPtr, 0, 16);
}
// used to read the next string from an input buffer
char * _readWord() {
char * word = strtok(nullptr, ", \n");
return word;
}

View File

@@ -1,33 +0,0 @@
/*
* Generic utils
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*
*/
#pragma once
#include "MyESP.h"
#include "ems.h"
#define myDebug(...) myESP.myDebug(__VA_ARGS__)
#define myDebug_P(...) myESP.myDebug_P(__VA_ARGS__)
char * _float_to_char(char * a, float f, uint8_t precision = 2);
char * _bool_to_char(char * s, uint8_t value);
char * _short_to_char(char * s, int16_t value, uint8_t decimals = 1);
char * _ushort_to_char(char * s, uint16_t value, uint8_t decimals = 1);
void _renderShortValue(const char * prefix, const char * postfix, int16_t value, uint8_t decimals = 1);
void _renderUShortValue(const char * prefix, const char * postfix, uint16_t value, uint8_t decimals = 1);
char * _int_to_char(char * s, uint8_t value, uint8_t div = 1);
void _renderIntValue(const char * prefix, const char * postfix, uint8_t value, uint8_t div = 1);
void _renderLongValue(const char * prefix, const char * postfix, uint32_t value, uint8_t div = 0);
void _renderBoolValue(const char * prefix, uint8_t value);
char * _hextoa(uint8_t value, char * buffer);
char * _smallitoa(uint8_t value, char * buffer);
char * _smallitoa3(uint16_t value, char * buffer);
uint8_t _readIntNumber();
float _readFloatNumber();
uint16_t _readHexNumber();
char * _readWord();

296
src/emsdevice.cpp Normal file
View File

@@ -0,0 +1,296 @@
/*
* 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 "emsdevice.h"
#include "emsesp.h"
#include "mqtt.h" // for the mqtt_function_p
MAKE_PSTR(logger_name, "emsdevice")
namespace emsesp {
uuid::log::Logger EMSdevice::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
std::string EMSdevice::brand_to_string() const {
switch (brand_) {
case EMSdevice::Brand::BOSCH:
return read_flash_string(F("Bosch"));
break;
case EMSdevice::Brand::JUNKERS:
return read_flash_string(F("Junkers"));
break;
case EMSdevice::Brand::BUDERUS:
return read_flash_string(F("Buderus"));
break;
case EMSdevice::Brand::NEFIT:
return read_flash_string(F("Nefit"));
break;
case EMSdevice::Brand::SIEGER:
return read_flash_string(F("Sieger"));
break;
case EMSdevice::Brand::WORCESTER:
return read_flash_string(F("Worcester"));
break;
case EMSdevice::Brand::NO_BRAND:
default:
return read_flash_string(F("?"));
break;
}
return std::string{};
}
std::string EMSdevice::device_type_name() const {
switch (device_type_) {
case DeviceType::BOILER:
return read_flash_string(F("Boiler"));
break;
case DeviceType::THERMOSTAT:
return read_flash_string(F("Thermostat"));
break;
case DeviceType::HEATPUMP:
return read_flash_string(F("Heat Pump"));
break;
case DeviceType::SOLAR:
return read_flash_string(F("Solar Module"));
break;
case DeviceType::CONNECT:
return read_flash_string(F("Connect Module"));
break;
case DeviceType::CONTROLLER:
return read_flash_string(F("Controller"));
break;
case DeviceType::MIXING:
return read_flash_string(F("Mixing Module"));
break;
case DeviceType::SWITCH:
return read_flash_string(F("Switching Module"));
break;
case DeviceType::GATEWAY:
return read_flash_string(F("Gateway Module"));
break;
default:
return read_flash_string(F("Unknown"));
break;
}
}
// 0=unknown, 1=bosch, 2=junkers, 3=buderus, 4=nefit, 5=sieger, 11=worcester
uint8_t EMSdevice::decode_brand(uint8_t value) {
switch (value) {
case 1:
return EMSdevice::Brand::BOSCH;
break;
case 2:
return EMSdevice::Brand::JUNKERS;
break;
case 3:
return EMSdevice::Brand::BUDERUS;
break;
case 4:
return EMSdevice::Brand::NEFIT;
break;
case 5:
return EMSdevice::Brand::SIEGER;
break;
case 11:
return EMSdevice::Brand::WORCESTER;
break;
case 0:
default:
return EMSdevice::Brand::NO_BRAND;
break;
}
}
// print human friendly description of the EMS device
std::string EMSdevice::to_string() const {
std::string str(160, '\0');
// for devices that haven't been lookup yet, don't show all details
if (product_id_ == 0) {
snprintf_P(&str[0], str.capacity() + 1, PSTR("%s (DeviceID:0x%02X)"), name_.c_str(), device_id_);
return str;
}
if (brand_ == Brand::NO_BRAND) {
snprintf_P(&str[0], str.capacity() + 1, PSTR("%s (DeviceID:0x%02X, ProductID:%d, Version:%s)"), name_.c_str(), device_id_, product_id_, version_.c_str());
} else {
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%s %s (DeviceID:0x%02X ProductID:%d, Version:%s)"),
brand_to_string().c_str(),
name_.c_str(),
device_id_,
product_id_,
version_.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());
}
// for each telegram that has the fetch value set (true) do a read request
void EMSdevice::fetch_values() {
LOG_DEBUG(F("Fetching values for device ID 0x%02X"), device_id());
for (const auto & tf : telegram_functions_) {
if (tf.fetch_) {
read_command(tf.telegram_type_id_);
}
}
}
// toggle on/off automatic fetch for a telegram id
void EMSdevice::toggle_fetch(uint16_t telegram_id, bool toggle) {
LOG_DEBUG(F("Toggling fetch for device ID 0x%02X, telegram ID 0x%02X to %d"), device_id(), telegram_id, toggle);
for (auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram_id) {
tf.fetch_ = toggle;
}
}
}
// list all the telegram type IDs for this device
void EMSdevice::show_telegram_handlers(uuid::console::Shell & shell) {
if (telegram_functions_.size() == 0) {
return;
}
shell.printf(F(" This %s will respond to telegram type IDs: "), device_type_name().c_str());
for (const auto & tf : telegram_functions_) {
shell.printf(F("0x%02X "), tf.telegram_type_id_);
}
shell.println();
}
// list all the mqtt handlers for this device
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) {
LOG_DEBUG(F("Registering MQTT topic %s for device ID %02X"), topic.c_str(), this->device_id_);
Mqtt::subscribe(this->device_id_, topic, f);
}
EMSdevice::TelegramFunction::TelegramFunction(uint16_t telegram_type_id,
const __FlashStringHelper * telegram_type_name,
bool fetch,
process_function_p process_function)
: telegram_type_id_(telegram_type_id)
, telegram_type_name_(telegram_type_name)
, fetch_(fetch)
, process_function_(process_function) {
}
// register a call back function for a specific telegram type
void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, process_function_p f) {
telegram_functions_.emplace_back(telegram_type_id, telegram_type_name, fetch, f);
}
// return the name of the telegram type
std::string EMSdevice::telegram_type_name(std::shared_ptr<const Telegram> telegram) {
// see if it's one of the common ones, like Version
if (telegram->type_id == EMS_TYPE_VERSION) {
return read_flash_string(F("Version"));
} else if (telegram->type_id == EMS_TYPE_UBADevices) {
return read_flash_string(F("UBADevices"));
}
for (const auto & tf : telegram_functions_) {
if ((tf.telegram_type_id_ == telegram->type_id) && ((telegram->type_id & 0x0F0) != 0xF0)) {
return uuid::read_flash_string(tf.telegram_type_name_);
}
}
return std::string{};
}
// take a telegram_type_id and call the matching handler
// return true if match found
bool EMSdevice::handle_telegram(std::shared_ptr<const Telegram> telegram) {
for (const auto & tf : telegram_functions_) {
if (tf.telegram_type_id_ == telegram->type_id) {
// if the data block is empty, assume that this telegram is not recognized by the bus master
// so remove it from the automatic fetch list
if (telegram->message_length == 0) {
LOG_DEBUG(F("This telegram (%s) is not recognized by the EMS bus"), uuid::read_flash_string(tf.telegram_type_name_).c_str());
toggle_fetch(tf.telegram_type_id_, false);
return false;
}
LOG_DEBUG(F("Processing %s..."), uuid::read_flash_string(tf.telegram_type_name_).c_str());
tf.process_function_(telegram);
return true;
}
}
return false; // type not found
}
// send Tx write with a data block
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid) {
EMSESP::send_write_request(type_id, device_id(), offset, message_data, message_length, validate_typeid);
}
// send Tx write with a single value
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid) {
uint8_t message_data[1];
message_data[0] = value;
EMSESP::send_write_request(type_id, device_id(), offset, message_data, 1, validate_typeid);
}
// send Tx write with a single value, with no post validation
void EMSdevice::write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value) {
write_command(type_id, offset, value, 0);
}
// send Tx read command to the device
void EMSdevice::read_command(const uint16_t type_id) {
EMSESP::send_read_request(type_id, device_id());
}
// prints a string value to the console
void EMSdevice::print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const __FlashStringHelper * value) {
print_value(shell, padding, name, uuid::read_flash_string(value).c_str());
}
void EMSdevice::print_value(uuid::console::Shell & shell, uint8_t padding, const __FlashStringHelper * name, const char * value) {
uint8_t i = padding;
while (i-- > 0) {
shell.print(F(" "));
}
shell.printfln(PSTR("%s: %s"), uuid::read_flash_string(name).c_str(), value);
}
} // namespace emsesp

249
src/emsdevice.h Normal file
View File

@@ -0,0 +1,249 @@
/*
* 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 EMSESP_EMSDEVICE_H_
#define EMSESP_EMSDEVICE_H_
#include <string>
#include <vector>
#include <functional>
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "helpers.h"
namespace emsesp {
using namespace std::placeholders; // for `_1`
class EMSdevice {
public:
// device_type defines which derived class to use, e.g. BOILER, THERMOSTAT etc..
EMSdevice(uint8_t device_type, uint8_t device_id, uint8_t product_id, const std::string & version, const std::string & name, uint8_t flags, uint8_t brand)
: device_type_(device_type)
, device_id_(device_id)
, product_id_(product_id)
, version_(version)
, name_(name)
, flags_(flags)
, brand_(brand) {
}
virtual ~EMSdevice() = default; // destructor of base class must always be virtual because it's a polymorphic class
inline uint8_t device_id() const {
return device_id_;
}
std::string device_type_name() const;
inline uint8_t product_id() const {
return product_id_;
}
void product_id(uint8_t product_id) {
product_id_ = product_id;
}
inline bool is_device_id(uint8_t device_id) {
return ((device_id & 0x7F) == (device_id_ & 0x7F));
}
inline uint8_t flags() const {
return flags_;
}
inline void flags(uint8_t flags) {
flags_ = flags;
}
// see enum DeviceType below
inline uint8_t device_type() const {
return device_type_;
}
inline void version(std::string & version) {
version_ = version;
}
inline void brand(uint8_t brand) {
brand_ = brand;
}
inline uint8_t brand() const {
return brand_;
}
inline void name(const std::string & name) {
name_ = name;
}
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);
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);
bool handle_telegram(std::shared_ptr<const Telegram> telegram);
void write_command(const uint16_t type_id, const uint8_t offset, uint8_t * message_data, const uint8_t message_length, const uint16_t validate_typeid);
void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value, const uint16_t validate_typeid);
void write_command(const uint16_t type_id, const uint8_t offset, const uint8_t value);
void read_command(const uint16_t type_id);
void register_mqtt_topic(const std::string & topic, mqtt_function_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;
std::string telegram_type_name(std::shared_ptr<const Telegram> telegram);
void fetch_values();
void toggle_fetch(uint16_t telegram_id, bool toggle);
// prints a ems device value to the console, handling the correct rendering of the type
// padding is # white space
// name is the name of the parameter
// suffix is any string to be appended after the value
// format:
// for ints its 0=no division, 255=handle as boolean, other divide by the value given and render with a decimal point
// for floats its the precision in number of decimal places from 0 to 8
template <typename Value>
static void print_value(uuid::console::Shell & shell,
uint8_t padding,
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;
}
uint8_t i = padding;
while (i-- > 0) {
shell.print(F(" "));
}
shell.printf(PSTR("%s: %s"), uuid::read_flash_string(name).c_str(), buffer);
if (suffix != nullptr) {
shell.println(uuid::read_flash_string(suffix).c_str());
} else {
shell.println();
}
}
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);
enum Brand : uint8_t {
NO_BRAND, // 0
BOSCH, // 1
JUNKERS, // 2
BUDERUS, // 3
NEFIT, // 4
SIEGER, // 5
WORCESTER // 11
};
enum DeviceType : uint8_t {
UNKNOWN = 0,
SERVICEKEY,
BOILER,
THERMOSTAT,
MIXING,
SOLAR,
HEATPUMP,
GATEWAY,
SWITCH,
CONTROLLER,
CONNECT
};
// device IDs
static constexpr uint8_t EMS_DEVICE_ID_BOILER = 0x08; // fixed device_id for Master Boiler/UBA
static constexpr uint8_t EMS_DEVICE_ID_MODEM = 0x48; // gateways like the KM200
// type IDs
static constexpr uint16_t EMS_TYPE_VERSION = 0x02; // type ID for Version information. Generic across all EMS devices.
static constexpr uint16_t EMS_TYPE_UBADevices = 0x07; // EMS connected devices
// device flags: The lower 4 bits hold the unique identifier, the upper 4 bits are used for specific flags
static constexpr uint8_t EMS_DEVICE_FLAG_NONE = 0;
// Solar Module
static constexpr uint8_t EMS_DEVICE_FLAG_SM10 = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_SM100 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_ISM = 3;
// Mixing Module
static constexpr uint8_t EMS_DEVICE_FLAG_MMPLUS = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_MM10 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_IPM = 3;
// Thermostats
static constexpr uint8_t EMS_DEVICE_FLAG_NO_WRITE = (1 << 7); // last bit
static constexpr uint8_t EMS_DEVICE_FLAG_EASY = 1;
static constexpr uint8_t EMS_DEVICE_FLAG_RC10 = 2;
static constexpr uint8_t EMS_DEVICE_FLAG_RC20 = 3;
static constexpr uint8_t EMS_DEVICE_FLAG_RC20_2 = 4; // Variation on RC20, Older, like ES72?
static constexpr uint8_t EMS_DEVICE_FLAG_RC30_1 = 5; // variation on RC30, Newer?
static constexpr uint8_t EMS_DEVICE_FLAG_RC30 = 6;
static constexpr uint8_t EMS_DEVICE_FLAG_RC35 = 7;
static constexpr uint8_t EMS_DEVICE_FLAG_RC300 = 8;
static constexpr uint8_t EMS_DEVICE_FLAG_RC100 = 9;
static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS = 10;
static constexpr uint8_t EMS_DEVICE_FLAG_JUNKERS_2 = (1 << 6); // 6th bit set if older models
private:
uint8_t device_type_ = DeviceType::UNKNOWN;
uint8_t device_id_ = 0;
uint8_t product_id_ = 0;
std::string version_;
std::string name_; // the long name of the EMS model
uint8_t flags_ = 0;
uint8_t brand_ = Brand::NO_BRAND;
static uuid::log::Logger logger_;
class TelegramFunction {
public:
TelegramFunction(uint16_t telegram_type_id, const __FlashStringHelper * telegram_type_name, bool fetch, process_function_p process_function);
~TelegramFunction() = default;
uint16_t telegram_type_id_; // it's type_id
const __FlashStringHelper * telegram_type_name_; // e.g. RC20Message
bool fetch_; // if this type_id be queried automatically
process_function_p process_function_;
};
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
};
} // namespace emsesp
#endif

735
src/emsesp.cpp Normal file
View File

@@ -0,0 +1,735 @@
/*
* 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 "emsesp.h"
MAKE_PSTR(logger_name, "emsesp")
namespace emsesp {
using DeviceFlags = emsesp::EMSdevice;
using DeviceType = emsesp::EMSdevice::DeviceType;
AsyncWebServer webServer(80);
#if defined(ESP32)
ESP8266React EMSESP::esp8266React(&webServer, &SPIFFS);
EMSESPSettingsService EMSESP::emsespSettingsService = EMSESPSettingsService(&webServer, &SPIFFS, EMSESP::esp8266React.getSecurityManager());
#else
ESP8266React EMSESP::esp8266React(&webServer, &LittleFS);
EMSESPSettingsService EMSESP::emsespSettingsService = EMSESPSettingsService(&webServer, &LittleFS, EMSESP::esp8266React.getSecurityManager());
#endif
EMSESPStatusService EMSESP::emsespStatusService =
EMSESPStatusService(&webServer, EMSESP::esp8266React.getSecurityManager(), EMSESP::esp8266React.getMqttClient());
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
uuid::log::Logger EMSESP::logger_{FPSTR(__pstr__logger_name), uuid::log::Facility::KERN};
// The services
RxService EMSESP::rxservice_; // incoming Telegram Rx handler
TxService EMSESP::txservice_; // outgoing Telegram Tx handler
Mqtt EMSESP::mqtt_; // mqtt handler
System EMSESP::system_; // core system services
Console EMSESP::console_; // telnet and serial console
Sensors EMSESP::sensors_; // Dallas sensors
Shower EMSESP::shower_; // Shower logic
// static/common variables
uint8_t EMSESP::actual_master_thermostat_ = EMSESP_DEFAULT_MASTER_THERMOSTAT; // which thermostat leads when multiple found
uint16_t EMSESP::watch_id_ = WATCH_ID_NONE; // for when log is TRACE. 0 means no trace set
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;
// for a specific EMS device go and request data values
// or if device_id is 0 it will fetch from all known devices
void EMSESP::fetch_device_values(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if ((device_id == 0) || emsdevice->is_device_id(device_id)) {
emsdevice->fetch_values();
if (device_id != 0) {
return; // quit, we only want to return the selected device
}
}
}
}
}
// return number of devices of a known type
uint8_t EMSESP::count_devices(const uint8_t device_type) {
uint8_t count = 0;
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
count += (emsdevice->device_type() == device_type);
}
}
return count;
}
void EMSESP::actual_master_thermostat(const uint8_t device_id) {
actual_master_thermostat_ = device_id;
}
uint8_t EMSESP::actual_master_thermostat() {
return actual_master_thermostat_;
}
// to watch both type IDs and device IDs
void EMSESP::watch_id(uint16_t watch_id) {
// if it's a device ID, which is a single byte, remove the MSB so to support both Buderus and HT3 protocols
if (watch_id <= 0xFF) {
watch_id_ = (watch_id & 0x7F);
} else {
watch_id_ = watch_id;
}
}
void EMSESP::reset_tx(uint8_t const tx_mode) {
txservice_.telegram_read_count(0);
txservice_.telegram_write_count(0);
txservice_.telegram_fail_count(0);
EMSuart::stop();
EMSuart::start(tx_mode); // reset the UART
}
// return status of bus: connected, connected but Tx is broken, disconnected
uint8_t EMSESP::bus_status() {
if (!rxservice_.bus_connected()) {
return BUS_STATUS_OFFLINE;
}
// check if we have Tx issues
if (txservice_.telegram_read_count() < 2) {
return BUS_STATUS_TX_ERRORS;
}
return BUS_STATUS_CONNECTED;
}
// 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()) {
uint8_t success_rate = 0;
if (rxservice_.telegram_error_count()) {
success_rate = ((float)rxservice_.telegram_error_count() / (float)rxservice_.telegram_count()) * 100;
}
shell.printfln(F("EMS Bus info:"));
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { shell.printfln(F(" Tx mode: %d"), settings.tx_mode); });
shell.printfln(F(" Bus protocol: %s"), EMSbus::is_ht3() ? F("HT3") : F("Buderus"));
shell.printfln(F(" #telegrams received: %d"), rxservice_.telegram_count());
shell.printfln(F(" #read requests sent: %d"), txservice_.telegram_read_count());
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();
// Rx queue
auto rx_telegrams = rxservice_.queue();
if (rx_telegrams.empty()) {
shell.printfln(F("Rx Queue is empty"));
} else {
shell.printfln(F("Rx Queue (%ld telegram%s):"), rx_telegrams.size(), rx_telegrams.size() == 1 ? "" : "s");
for (const auto & it : rx_telegrams) {
shell.printfln(F(" [%02d] %s"), it.id_, pretty_telegram(it.telegram_).c_str());
}
}
shell.println();
// Tx queue
auto tx_telegrams = txservice_.queue();
if (tx_telegrams.empty()) {
shell.printfln(F("Tx Queue is empty"));
} else {
shell.printfln(F("Tx Queue (%ld telegram%s):"), tx_telegrams.size(), tx_telegrams.size() == 1 ? "" : "s");
std::string op;
for (const auto & it : tx_telegrams) {
if ((it.telegram_->operation) == Telegram::Operation::TX_RAW) {
op = read_flash_string(F("RAW "));
} else if ((it.telegram_->operation) == Telegram::Operation::TX_READ) {
op = read_flash_string(F("READ "));
} else if ((it.telegram_->operation) == Telegram::Operation::TX_WRITE) {
op = read_flash_string(F("WRITE"));
}
shell.printfln(F(" [%02d%c] %s %s (offset %d)"),
it.id_,
((it.retry_) ? '*' : ' '),
op.c_str(),
pretty_telegram(it.telegram_).c_str(),
it.telegram_->offset);
}
}
shell.println();
}
// show EMS device values
void EMSESP::show_device_values(uuid::console::Shell & shell) {
if (emsdevices.empty()) {
shell.printfln(F("No EMS devices detected. Try using 'scan devices' from the ems menu."));
shell.println();
return;
}
// do this in the order of factory classes to keep a consistent order when displaying
for (const auto & device_class : EMSFactory::device_handlers()) {
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == device_class.first)) {
emsdevice->show_values(shell);
shell.println();
}
}
}
shell.println();
}
// show Dallas sensors
void EMSESP::show_sensor_values(uuid::console::Shell & shell) {
if (sensor_devices().empty()) {
return;
}
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.println();
}
// publish all values from each EMS device to MQTT
// plus the heartbeat and sensor if activated
void EMSESP::publish_all_values() {
if (Mqtt::connected()) {
// Dallas sensors first
sensors_.publish_values();
// all the connected EMS devices we known about
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
emsdevice->publish_values();
}
}
}
}
// search for recognized device_ids : Me, All, otherwise print hex value
std::string EMSESP::device_tostring(const uint8_t device_id) {
if ((device_id & 0x7F) == rxservice_.ems_bus_id()) {
return read_flash_string(F("Me"));
} else if (device_id == 0x00) {
return read_flash_string(F("All"));
} else {
char buffer[5];
return Helpers::hextoa(buffer, device_id);
}
}
// created a pretty print telegram as a text string
// e.g. Boiler(0x08) -> Me(0x0B), Version(0x02), data: 7B 06 01 00 00 00 00 00 00 04 (#data=10)
std::string EMSESP::pretty_telegram(std::shared_ptr<const Telegram> telegram) {
uint8_t src = telegram->src & 0x7F;
uint8_t dest = telegram->dest & 0x7F;
uint8_t offset = telegram->offset;
// find name for src and dest by looking up known devices
std::string src_name;
std::string dest_name;
std::string type_name;
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
// get src & dest
if (emsdevice->is_device_id(src)) {
src_name = emsdevice->device_type_name();
} else if (emsdevice->is_device_id(dest)) {
dest_name = emsdevice->device_type_name();
}
// get the type name, any match will do
if (type_name.empty()) {
type_name = emsdevice->telegram_type_name(telegram);
}
}
}
// if we can't find names for the devices, use their hex values
if (src_name.empty()) {
src_name = device_tostring(src);
}
if (dest_name.empty()) {
dest_name = device_tostring(dest);
}
// check for global/common types like Version
if (telegram->type_id == EMSdevice::EMS_TYPE_VERSION) {
type_name = read_flash_string(F("Version"));
}
// if we don't know the type show
if (type_name.empty()) {
type_name = read_flash_string(F("?"));
}
std::string str(200, '\0');
if (offset) {
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%s(0x%02X) -> %s(0x%02X), %s(0x%02X), data: %s @offset %d"),
src_name.c_str(),
src,
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string().c_str(),
offset);
} else {
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%s(0x%02X) -> %s(0x%02X), %s(0x%02X), data: %s"),
src_name.c_str(),
src,
dest_name.c_str(),
dest,
type_name.c_str(),
telegram->type_id,
telegram->to_string().c_str());
}
return str;
}
/*
* Type 0x07 - UBADevices - shows us the connected EMS devices
* e.g. 08 00 07 00 0B 80 00 00 00 00 00 00 00 00 00 00 00
* Junkers has 15 bytes of data
* each byte is a bitmask for which devices are active
* byte 1 = range 0x08 - 0x0F, byte 2=0x10 - 0x17 etc...
*/
void EMSESP::process_UBADevices(std::shared_ptr<const Telegram> telegram) {
// exit it length is incorrect (must be 13 or 15 bytes long)
if (telegram->message_length > 15) {
return;
}
// for each byte, check the bits and determine the device_id
for (uint8_t data_byte = 0; data_byte < telegram->message_length; data_byte++) {
uint8_t next_byte = telegram->message_data[data_byte];
if (next_byte) {
for (uint8_t bit = 0; bit < 8; bit++) {
if (next_byte & 0x01) {
uint8_t device_id = ((data_byte + 1) * 8) + bit;
// if we haven't already detected this device, request it's version details, unless its us (EMS-ESP)
// when the version info is received, it will automagically add the device
if ((device_id != EMSbus::ems_bus_id()) && !(EMSESP::device_exists(device_id))) {
LOG_DEBUG(F("New EMS device detected with ID 0x%02X. Requesting version information."), device_id);
send_read_request(EMSdevice::EMS_TYPE_VERSION, device_id);
}
}
next_byte = next_byte >> 1; // advance 1 bit
}
}
}
}
// process the Version telegram (type 0x02), which is a common type
// e.g. 09 0B 02 00 PP V1 V2
void EMSESP::process_version(std::shared_ptr<const Telegram> telegram) {
// check for valid telegram, just in case
if (telegram->message_length < 3) {
return;
}
// check for 2nd subscriber, e.g. 18 0B 02 00 00 00 00 5E 02 01
uint8_t offset = 0;
if (telegram->message_data[0] == 0x00) {
// see if we have a 2nd subscriber
if (telegram->message_data[3] != 0x00) {
offset = 3;
} else {
return; // ignore whole telegram
}
}
// extra details from the telegram
uint8_t device_id = telegram->src; // device ID
uint8_t product_id = telegram->message_data[offset]; // product ID
// get version as XX.XX
std::string version(5, '\0');
snprintf_P(&version[0], version.capacity() + 1, PSTR("%02d.%02d"), telegram->message_data[offset + 1], telegram->message_data[offset + 2]);
// some devices store the protocol type (HT3, Buderus) in the last byte
uint8_t brand;
if (telegram->message_length >= 10) {
brand = EMSdevice::decode_brand(telegram->message_data[9]);
} else {
brand = EMSdevice::Brand::NO_BRAND; // unknown
}
// add it - will be overwritten if device already exists
(void)add_device(device_id, product_id, version, brand);
}
// find the device object that matches the device ID and see if it has a matching telegram type handler
// but only process if the telegram is sent to us or it's a broadcast (dest=0x00=all)
// We also check for common telgram types, like the Version(0x02)
// returns false if there are none found
bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
// if watching...
if (watch() == WATCH_ON) {
if ((watch_id_ == WATCH_ID_NONE) || (telegram->src == watch_id_) || (telegram->dest == watch_id_) || (telegram->type_id == watch_id_)) {
LOG_NOTICE(pretty_telegram(telegram).c_str());
}
}
// only process broadcast telegrams or ones sent to us on request
if ((telegram->dest != 0x00) && (telegram->dest != rxservice_.ems_bus_id())) {
return false;
}
// check for common types, like the Version(0x02)
if (telegram->type_id == EMSdevice::EMS_TYPE_VERSION) {
process_version(telegram);
return true;
} else if (telegram->type_id == EMSdevice::EMS_TYPE_UBADevices) {
process_UBADevices(telegram);
return true;
}
// match device_id and type_id
// calls the associated process function for that EMS device
// returns false if the device_id doesn't recognize it
// after the telegram has been processed, call the updated_values() function to see if we need to force an MQTT publish
bool found = false;
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(telegram->src)) {
found = emsdevice->handle_telegram(telegram);
// check to see if we need to follow up after the telegram has been processed
if (found) {
if (emsdevice->updated_values()) {
emsdevice->publish_values(); // publish to MQTT if we explicitly have too
}
}
break;
}
}
}
if (!found) {
LOG_DEBUG(F("No telegram type handler found for ID 0x%02X (src 0x%02X, dest 0x%02X)"), telegram->type_id, telegram->src, telegram->dest);
}
return found;
}
// return true if we have this device already registered
bool EMSESP::device_exists(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(device_id)) {
return true;
}
}
}
return false; // not found
}
// for each device add its context menu for the console
void EMSESP::add_context_menus() {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
emsdevice->add_context_menu();
}
}
}
// for each associated EMS device go and get its system information
void EMSESP::show_devices(uuid::console::Shell & shell) {
if (emsdevices.empty()) {
shell.printfln(F("No EMS devices detected. Try using 'scan devices' from the ems menu."));
shell.println();
return;
}
shell.printfln(F("These EMS devices are currently active:"));
shell.println();
// for all device objects from emsdevice.h (UNKNOWN, SERVICEKEY, BOILER, THERMOSTAT, MIXING, SOLAR, HEATPUMP, GATEWAY, SWITCH, CONTROLLER, CONNECT)
// so we keep a consistent order
for (const auto & device_class : EMSFactory::device_handlers()) {
// shell.printf(F("[factory ID: %d] "), device_class.first);
for (const auto & emsdevice : emsdevices) {
if ((emsdevice) && (emsdevice->device_type() == device_class.first)) {
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 **"));
}
shell.println();
emsdevice->show_telegram_handlers(shell);
emsdevice->show_mqtt_handlers(shell);
shell.println();
}
}
}
}
// add a new or update existing EMS device to our list of active EMS devices
// if its not in our database, we don't add it
bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand) {
// don't add ourselves!
if (device_id == rxservice_.ems_bus_id()) {
return false;
}
// first check to see if we already have it, if so update the record
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->is_device_id(device_id)) {
LOG_DEBUG(F("Updating details to already existing device ID 0x%02X"), device_id);
emsdevice->product_id(product_id);
emsdevice->version(version);
// only set brand if it doesn't already exist
if (emsdevice->brand() == EMSdevice::Brand::NO_BRAND) {
emsdevice->brand(brand);
}
// find the name and flags in our database
for (const auto & device : device_library_) {
if (device.product_id == product_id) {
emsdevice->name(uuid::read_flash_string(device.name));
emsdevice->flags(device.flags);
}
}
return true; // finish up
}
}
}
// look up the rest of the details using the product_id and create the new device object
Device_record * device_p = nullptr;
for (auto & device : device_library_) {
if (device.product_id == product_id) {
// sometimes boilers share the same product id as controllers
// so only add boilers if the device_id is 0x08, which is fixed for EMS
if (device.device_type == DeviceType::BOILER) {
if (device_id == EMSdevice::EMS_DEVICE_ID_BOILER) {
device_p = &device;
break;
}
} else {
// it's not a boiler, but we have a match
device_p = &device;
break;
}
}
}
// if we don't recognize the product ID report it, but don't add it.
if (device_p == nullptr) {
LOG_NOTICE(F("Unrecognized EMS device with device ID 0x%02X with product ID %d. Please report on GitHub."), device_id, product_id);
return false; // not found
} 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));
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);
}
return true;
}
// send a read request, passing it into to the Tx Service, with no offset
void EMSESP::send_read_request(const uint16_t type_id, const uint8_t dest) {
txservice_.read_request(type_id, dest, 0); // 0 = no offset
}
// sends write request
void EMSESP::send_write_request(const uint16_t type_id,
const uint8_t dest,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validate_typeid) {
txservice_.add(Telegram::Operation::TX_WRITE, dest, type_id, offset, message_data, message_length);
txservice_.set_post_send_query(validate_typeid); // store which type_id to send Tx read after a write
}
// this is main entry point when data is received on the Rx line, via emsuart library
// we check if its a complete telegram or just a single byte (which could be a poll or a return status)
// the CRC check is not done here, only when it's added to the Rx queue with add()
void EMSESP::incoming_telegram(uint8_t * data, const uint8_t length) {
#ifdef EMSESP_DEBUG
static uint32_t rx_time_ = 0;
#endif
// check first for echo
uint8_t first_value = data[0];
if (((first_value & 0x7F) == txservice_.ems_bus_id()) && (length > 1)) {
// if we ask ourself at roomcontrol for version e.g. 0B 98 02 00 20
Roomctrl::check((data[1] ^ 0x80 ^ rxservice_.ems_mask()), data);
#ifdef EMSESP_DEBUG
// get_uptime is only updated once per loop, does not give the right time
LOG_DEBUG(F("[DEBUG] Echo after %d ms: %s"), ::millis() - rx_time_, Helpers::data_to_hex(data, length).c_str());
#endif
return; // it's an echo
}
// are we waiting for a response from a recent Tx Read or Write?
uint8_t op = EMSbus::tx_waiting();
if (op != Telegram::Operation::NONE) {
bool tx_successful = false;
EMSbus::tx_waiting(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 (first_value == TxService::TX_WRITE_SUCCESS) {
LOG_DEBUG(F("Last Tx write successful"));
txservice_.increment_telegram_write_count(); // last tx/write was confirmed ok
txservice_.send_poll(); // close the bus
txservice_.post_send_query(); // follow up with any post-read
txservice_.reset_retry_count();
tx_successful = true;
} else if (first_value == TxService::TX_WRITE_FAIL) {
LOG_ERROR(F("Last Tx write rejected by host"));
txservice_.send_poll(); // close the bus
txservice_.reset_retry_count();
}
} else if (op == 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];
if (txservice_.is_last_tx(src, dest)) {
LOG_DEBUG(F("Last Tx read successful"));
txservice_.increment_telegram_read_count();
txservice_.send_poll(); // close the bus
txservice_.reset_retry_count();
tx_successful = true;
}
}
// if Tx wasn't successful, retry or just give up
if (!tx_successful) {
txservice_.retry_tx(op, data, length);
return;
}
}
// check for poll
if (length == 1) {
#ifdef EMSESP_DEBUG
char s[4];
if (first_value & 0x80) {
LOG_TRACE(F("[DEBUG] next Poll %s after %d ms"), Helpers::hextoa(s, first_value), ::millis() - rx_time_);
// time measurement starts here, use millis because get_uptime is only updated once per loop
rx_time_ = ::millis();
} else {
LOG_TRACE(F("[DEBUG] Poll ack %s after %d ms"), Helpers::hextoa(s, first_value), ::millis() - rx_time_);
}
#endif
// 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
Roomctrl::send(first_value ^ 0x80 ^ rxservice_.ems_mask());
return;
} else {
#ifdef EMSESP_DEBUG
LOG_TRACE(F("[DEBUG] Reply after %d ms: %s"), ::millis() - rx_time_, Helpers::data_to_hex(data, length).c_str());
#endif
// check if there is a message for the roomcontroller
Roomctrl::check((data[1] ^ 0x80 ^ rxservice_.ems_mask()), data);
// add to RxQueue, what ever it is.
// in add() the CRC will be checked
rxservice_.add(data, length);
}
}
// sends raw data of bytes along the Tx line
void EMSESP::send_raw_telegram(const char * data) {
txservice_.send_raw(data);
}
// kick off the party, start all the services
void EMSESP::start() {
// Load our library of known devices
device_library_ = {
#include "device_library.h"
};
#ifdef ESP32
SPIFFS.begin(true);
#elif defined(ESP8266)
LittleFS.begin();
#endif
esp8266React.begin(); // starts wifi, ap, ota, security, mqtt services
emsespSettingsService.begin(); // load settings
console_.start(); // telnet and serial console
system_.start(); // starts syslog, uart, sets version, initializes LED. Requires pre-loaded settings.
mqtt_.start(EMSESP::esp8266React.getMqttClient()); // mqtt init
sensors_.start(); // dallas external sensors
shower_.start(); // initialize shower timer and shower alert
txservice_.start(); // sets bus ID, sends out request for EMS devices
webServer.begin(); // start web server
}
// loop de loop
void EMSESP::loop() {
#ifndef EMSESP_STANDALONE
esp8266React.loop();
#endif
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
console_.loop(); // telnet/serial console
// force a query on the EMS devices to fetch latest data at a set interval (1 min)
if ((uuid::get_uptime() - last_fetch_ > EMS_FETCH_FREQUENCY)) {
last_fetch_ = uuid::get_uptime();
fetch_device_values();
}
delay(1);
}
} // namespace emsesp

179
src/emsesp.h Normal file
View File

@@ -0,0 +1,179 @@
/*
* 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 EMSESP_EMSESP_H
#define EMSESP_EMSESP_H
#include <Arduino.h>
#include <vector>
#include <queue>
#include <string>
#include <uuid/common.h>
#include <uuid/console.h>
#include <uuid/log.h>
#ifndef EMSESP_STANDALONE
#include <uuid/telnet.h>
#endif
#include <ESP8266React.h>
#include "EMSESPStatusService.h"
#include "EMSESPSettingsService.h"
#include "emsdevice.h"
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "system.h"
#include "sensors.h"
#include "console.h"
#include "shower.h"
#include "roomcontrol.h"
#include "devices/boiler.h"
#define WATCH_ID_NONE 0 // no watch id set
namespace emsesp {
class Shower; // forward declaration for compiler
class EMSESP {
public:
static void start();
static void loop();
static void publish_all_values();
#ifdef EMSESP_DEBUG
static void run_test(uuid::console::Shell & shell, const std::string & command); // only for testing
static void dummy_mqtt_commands(const char * message);
static void rx_telegram(const std::vector<uint8_t> & data);
static void uart_telegram(const std::vector<uint8_t> & rx_data);
#endif
static bool process_telegram(std::shared_ptr<const Telegram> telegram);
static std::string pretty_telegram(std::shared_ptr<const Telegram> telegram);
static void send_read_request(const uint16_t type_id, const uint8_t dest);
static void send_write_request(const uint16_t type_id,
const uint8_t dest,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const uint16_t validate_typeid);
static void send_raw_telegram(const char * data);
static bool device_exists(const uint8_t device_id);
static uint8_t count_devices(const uint8_t device_type);
static uint8_t actual_master_thermostat();
static void actual_master_thermostat(const uint8_t device_id);
static void show_device_values(uuid::console::Shell & shell);
static void show_sensor_values(uuid::console::Shell & shell);
static void show_devices(uuid::console::Shell & shell);
static void show_ems(uuid::console::Shell & shell);
static void add_context_menus();
static void reset_tx(uint8_t const tx_mode);
static void incoming_telegram(uint8_t * data, const uint8_t length);
static const std::vector<Sensors::Device> sensor_devices() {
return sensors_.devices();
}
static void watch_id(uint16_t id);
static uint16_t watch_id() {
return watch_id_;
}
static void watch(uint8_t watch) {
watch_ = watch; // 0=off, 1=on, 2=raw
}
enum Watch : uint8_t { WATCH_OFF, WATCH_ON, WATCH_RAW };
static uint8_t watch() {
return watch_;
}
enum Bus_status : uint8_t { BUS_STATUS_CONNECTED = 0, BUS_STATUS_TX_ERRORS, BUS_STATUS_OFFLINE };
static uint8_t bus_status();
static bool tap_water_active() {
return tap_water_active_;
}
static void tap_water_active(const bool tap_water_active) {
tap_water_active_ = tap_water_active;
}
static void fetch_device_values(const uint8_t device_id = 0);
static bool add_device(const uint8_t device_id, const uint8_t product_id, std::string & version, const uint8_t brand);
static Mqtt mqtt_;
static System system_;
static Sensors sensors_;
static Console console_;
static Shower shower_;
static RxService rxservice_;
static TxService txservice_;
static ESP8266React esp8266React;
static EMSESPSettingsService emsespSettingsService;
static EMSESPStatusService emsespStatusService;
private:
EMSESP() = delete;
static uuid::log::Logger logger_;
static std::string device_tostring(const uint8_t device_id);
static void process_UBADevices(std::shared_ptr<const Telegram> telegram);
static void process_version(std::shared_ptr<const Telegram> telegram);
static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute
static uint32_t last_fetch_;
struct Device_record {
uint8_t product_id;
EMSdevice::DeviceType device_type;
const __FlashStringHelper * name;
uint8_t flags;
};
static std::vector<std::unique_ptr<EMSdevice>> emsdevices;
static std::vector<Device_record> device_library_;
static uint8_t actual_master_thermostat_;
static uint16_t watch_id_;
static uint8_t watch_;
static bool tap_water_active_;
};
} // namespace emsesp
#endif

100
src/emsfactory.h Normal file
View File

@@ -0,0 +1,100 @@
/*
* 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 EMSESP_EMSFACTORY_H_
#define EMSESP_EMSFACTORY_H_
#include <map>
#include <memory> // for unique_ptr
#include "emsdevice.h"
// Macro for class registration
// Anonymous namespace is used to make the definitions here private to the current
// compilation unit (current file). It is equivalent to the old C static keyword.
#define REGISTER_FACTORY(derivedClass, device_type) \
namespace { \
auto registry_##derivedClass = ConcreteEMSFactory<derivedClass>(device_type); \
}
namespace emsesp {
class EMSdevice; // forward declaration, for gcc linking
class EMSFactory {
public:
virtual ~EMSFactory() = default;
// Register factory object of derived class
// using the device_type as the unique identifier
static auto registerFactory(const uint8_t device_type, EMSFactory * factory) -> void {
auto & reg = EMSFactory::getRegister();
reg[device_type] = factory;
}
using FactoryMap = std::map<uint8_t, EMSFactory *>;
// returns all registered classes (really only for debugging)
static auto device_handlers() -> FactoryMap {
return EMSFactory::getRegister();
}
// Construct derived class returning an unique ptr
static auto add(const uint8_t device_type, uint8_t device_id, uint8_t product_id, std::string version, std::string name, uint8_t flags, uint8_t brand)
-> std::unique_ptr<EMSdevice> {
return std::unique_ptr<EMSdevice>(EMSFactory::makeRaw(device_type, device_id, product_id, version, name, flags, brand));
}
virtual 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 * = 0;
private:
// Force global variable to be initialized, thus it avoids the "initialization order fiasco"
static auto getRegister() -> FactoryMap & {
static FactoryMap classRegister{};
return classRegister;
}
// Construct derived class returning a raw pointer
// find which EMS device it is and use that class
static auto makeRaw(const uint8_t device_type, uint8_t device_id, uint8_t product_id, std::string version, std::string name, uint8_t flags, uint8_t brand)
-> EMSdevice * {
auto it = EMSFactory::getRegister().find(device_type);
if (it != EMSFactory::getRegister().end()) {
return it->second->construct(device_type, device_id, product_id, version, name, flags, brand);
}
return nullptr;
}
};
template <typename DerivedClass>
class ConcreteEMSFactory : EMSFactory {
public:
// Register this global object on the EMSFactory register
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);
}
};
} // namespace emsesp
#endif

View File

@@ -1,298 +0,0 @@
/*
* emsuart.cpp
*
* The low level UART code for ESP8266 to read and write to the EMS bus via uart
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*/
#include "emsuart.h"
#include "ems.h"
#include <user_interface.h>
_EMSRxBuf * pEMSRxBuf;
_EMSRxBuf * paEMSRxBuf[EMS_MAXBUFFERS];
uint8_t emsRxBufIdx = 0;
uint8_t phantomBreak = 0;
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
//
// Main interrupt handler
// Important: do not use ICACHE_FLASH_ATTR !
//
static void emsuart_rx_intr_handler(void * para) {
static uint8_t length;
static uint8_t uart_buffer[EMS_MAXBUFFERSIZE + 2];
// is a new buffer? if so init the thing for a new telegram
if (EMS_Sys_Status.emsRxStatus == EMS_RX_STATUS_IDLE) {
EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_BUSY; // status set to busy
length = 0;
}
// fill IRQ buffer, by emptying Rx FIFO
if (USIS(EMSUART_UART) & ((1 << UIFF) | (1 << UITO) | (1 << UIBD))) {
while ((USS(EMSUART_UART) >> USRXC) & 0xFF) {
uint8_t rx = USF(EMSUART_UART);
if (length < EMS_MAXBUFFERSIZE)
uart_buffer[length++] = rx;
}
// clear Rx FIFO full and Rx FIFO timeout interrupts
USIC(EMSUART_UART) = (1 << UIFF) | (1 << UITO);
}
// BREAK detection = End of EMS data block
if (USIS(EMSUART_UART) & ((1 << UIBD))) {
ETS_UART_INTR_DISABLE(); // disable all interrupts and clear them
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
pEMSRxBuf->length = (length > EMS_MAXBUFFERSIZE) ? EMS_MAXBUFFERSIZE : length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
length = 0;
EMS_Sys_Status.emsRxStatus = EMS_RX_STATUS_IDLE; // set the status flag stating BRK has been received and we can start a new package
ETS_UART_INTR_ENABLE(); // re-enable UART interrupts
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity
}
}
/*
* system task triggered on BRK interrupt
* incoming received messages are always asynchronous
* The full buffer is sent to the ems_parseTelegram() function in ems.cpp.
*/
static void ICACHE_FLASH_ATTR emsuart_recvTask(os_event_t * events) {
_EMSRxBuf * pCurrent = pEMSRxBuf;
pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer
uint8_t length = pCurrent->length; // number of bytes including the BRK at the end
pCurrent->length = 0;
if (phantomBreak) {
phantomBreak = 0;
length--; // remove phantom break from Rx buffer
}
if (length == 2) {
// it's a poll or status code, single byte and ok to send on
ems_parseTelegram((uint8_t *)pCurrent->buffer, 1);
} else if ((length > 4) && (length <= EMS_MAXBUFFERSIZE + 1)) {
// ignore double BRK at the end, possibly from the Tx loopback
// also telegrams with no data value
ems_parseTelegram((uint8_t *)pCurrent->buffer, length - 1); // transmit EMS buffer, excluding the BRK
}
}
/*
* flush everything left over in buffer, this clears both rx and tx FIFOs
*/
static inline void ICACHE_FLASH_ATTR emsuart_flush_fifos() {
uint32_t tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
}
/*
* init UART0 driver
*/
void ICACHE_FLASH_ATTR emsuart_init() {
ETS_UART_INTR_DISABLE();
ETS_UART_INTR_ATTACH(nullptr, nullptr);
// allocate and preset EMS Receive buffers
for (int i = 0; i < EMS_MAXBUFFERS; i++) {
_EMSRxBuf * p = (_EMSRxBuf *)malloc(sizeof(_EMSRxBuf));
paEMSRxBuf[i] = p;
}
pEMSRxBuf = paEMSRxBuf[0]; // preset EMS Rx Buffer
// pin settings
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD);
// set 9600, 8 bits, no parity check, 1 stop bit
USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD);
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1
emsuart_flush_fifos();
// conf1 params
// UCTOE = RX TimeOut enable (default is 1)
// UCTOT = RX TimeOut Threshold (7 bit) = want this when no more data after 1 characters (default is 2)
// 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!
USC1(EMSUART_UART) = 0; // reset config first
USC1(EMSUART_UART) = (0x01 << UCFFT) | (0x01 << UCTOT) | (0 << 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
USIE(EMSUART_UART) = (1 << UIBD) | (1 << UIFF) | (0 << UITO);
// set up interrupt callbacks for Rx
system_os_task(emsuart_recvTask, EMSUART_recvTaskPrio, recvTaskQueue, EMSUART_recvTaskQueueLen);
// disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655
system_set_os_print(0);
// swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
system_uart_swap();
ETS_UART_INTR_ATTACH(emsuart_rx_intr_handler, nullptr);
ETS_UART_INTR_ENABLE();
}
/*
* stop UART0 driver
* This is called prior to an OTA upload and also before a save to SPIFFS to prevent conflicts
*/
void ICACHE_FLASH_ATTR emsuart_stop() {
ETS_UART_INTR_DISABLE();
}
/*
* re-start UART0 driver
*/
void ICACHE_FLASH_ATTR emsuart_start() {
ETS_UART_INTR_ENABLE();
}
/*
* Send a BRK signal
* Which is a 11-bit set of zero's (11 cycles)
*/
void ICACHE_FLASH_ATTR emsuart_tx_brk() {
uint32_t tmp;
// must make sure Tx FIFO is empty
while (((USS(EMSUART_UART) >> USTXC) & 0xFF))
;
tmp = ((1 << UCRXRST) | (1 << UCTXRST)); // bit mask
USC0(EMSUART_UART) |= (tmp); // set bits
USC0(EMSUART_UART) &= ~(tmp); // clear bits
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will
// automatically be sent when the tx fifo is empty
tmp = (1 << UCBRK);
USC0(EMSUART_UART) |= (tmp); // set bit
if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_EMSPLUS) { // EMS+ mode
delayMicroseconds(EMSUART_TX_BRK_WAIT);
} else if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_HT3) { // junkers mode
delayMicroseconds(EMSUART_TX_WAIT_BRK - EMSUART_TX_LAG); // 1144 (11 Bits)
}
USC0(EMSUART_UART) &= ~(tmp); // clear bit
}
/*
* Send to Tx, ending with a <BRK>
*/
_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len) {
_EMS_TX_STATUS result = EMS_TX_STATUS_OK;
if (EMS_Sys_Status.emsLogging == EMS_SYS_LOGGING_JABBER) {
ems_dumpBuffer("emsuart_tx_buffer: ", buf, len); // validate and transmit the EMS buffer, excluding the BRK
}
if (len) {
if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_EMSPLUS) { // With extra tx delay for EMS+
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
delayMicroseconds(EMSUART_TX_BRK_WAIT); // https://github.com/proddy/EMS-ESP/issues/23#
}
emsuart_tx_brk(); // send <BRK>
} else if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_HT3) { // Junkers logic by @philrich
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
// just to be safe wait for tx fifo empty (needed?)
while (((USS(EMSUART_UART) >> USTXC) & 0xff))
;
// wait until bits are sent on wire
delayMicroseconds(EMSUART_TX_WAIT_BYTE - EMSUART_TX_LAG + EMSUART_TX_WAIT_GAP);
}
emsuart_tx_brk(); // send <BRK>
} else if (EMS_Sys_Status.emsTxMode == EMS_TXMODE_DEFAULT) {
/*
* based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch
* we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO.
* after sending the last char we poll the Rx status until either
* - size(Rx FIFO) == size(Tx-Telegram)
* - <BRK> is detected
* At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode.
*
* EMS-Bus error handling
* 1. Busmaster stops echoing on Tx w/o permission
* 2. Busmaster cancel telegram by sending a BRK
*
* Case 1. is handled by a watchdog counter which is reset on each
* Tx attempt. The timeout should be 20x EMSUART_BIT_TIME plus
* some smart guess for processing time on targeted EMS device.
* We set EMS_Sys_Status.emsTxStatus to EMS_TX_WTD_TIMEOUT and return
*
* Case 2. is handled via a BRK chk during transmission.
* We set EMS_Sys_Status.emsTxStatus to EMS_TX_BRK_DETECT and return
*
*/
uint16_t wdc = EMS_TX_TO_COUNT;
ETS_UART_INTR_DISABLE(); // disable rx interrupt
// clear Rx status register
USC0(EMSUART_UART) |= (1 << UCRXRST); // reset uart rx fifo
emsuart_flush_fifos();
// throw out the telegram...
for (uint8_t i = 0; i < len && result == EMS_TX_STATUS_OK;) {
wdc = EMS_TX_TO_COUNT;
volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF;
USF(EMSUART_UART) = buf[i++]; // send each Tx byte
// wait for echo from busmaster
while (((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) {
delayMicroseconds(EMSUART_BUSY_WAIT); // burn CPU cycles...
if (--wdc == 0) {
EMS_Sys_Status.emsTxStatus = result = EMS_TX_WTD_TIMEOUT;
break;
}
if (USIR(EMSUART_UART) & (1 << UIBD)) {
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
EMS_Sys_Status.emsTxStatus = result = EMS_TX_BRK_DETECT;
}
}
}
// 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 in the loopback and re=enable Rx-INT.
// worst case, we'll see an additional Rx-BRK...
if (result == EMS_TX_STATUS_OK) {
// neither bus collision nor timeout - send terminating BRK signal
if (!(USIS(EMSUART_UART) & (1 << UIBD))) {
// no bus collision - send terminating BRK signal
USC0(EMSUART_UART) |= (1 << UCLBE) | (1 << UCBRK); // enable loopback & set <BRK>
// wait until BRK detected...
while (!(USIR(EMSUART_UART) & (1 << UIBD))) {
delayMicroseconds(EMSUART_BIT_TIME);
}
USC0(EMSUART_UART) &= ~((1 << UCBRK) | (1 << UCLBE)); // disable loopback & clear <BRK>
USIC(EMSUART_UART) = (1 << UIBD); // clear BRK detect IRQ
phantomBreak = 1;
}
}
ETS_UART_INTR_ENABLE(); // receive anything from FIFO...
}
}
return result;
}

View File

@@ -1,42 +0,0 @@
/*
* emsuart.h
*
* Header file for emsuart.cpp
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*/
#pragma once
#include <ems.h>
#define EMSUART_UART 0 // UART 0
#define EMSUART_CONFIG 0x1C // 8N1 (8 bits, no stop bits, 1 parity)
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions
#define EMS_MAXBUFFERSIZE (EMS_MAX_TELEGRAM_LENGTH + 2) // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_BIT_TIME 104 // bit time @9600 baud
#define EMSUART_TX_BRK_WAIT 2070 // the BRK from Boiler master is roughly 1.039ms, so accounting for hardware lag using around 2078 (for half-duplex) - 8 (lag)
#define EMSUART_TX_WAIT_BYTE (EMSUART_BIT_TIME * 10) // Time to send one Byte (8 Bits, 1 Start Bit, 1 Stop Bit)
#define EMSUART_TX_WAIT_BRK (EMSUART_BIT_TIME * 11) // Time to send a BRK Signal (11 Bit)
#define EMSUART_TX_WAIT_GAP (EMSUART_BIT_TIME * 7) // Gap between to Bytes
#define EMSUART_TX_LAG 8
#define EMSUART_BUSY_WAIT (EMSUART_BIT_TIME / 8)
#define EMS_TX_TO_CHARS (2 + 20)
#define EMS_TX_TO_COUNT ((EMS_TX_TO_CHARS)*10 * 8)
#define EMSUART_recvTaskPrio 1
#define EMSUART_recvTaskQueueLen 64
typedef struct {
uint8_t length;
uint8_t buffer[EMS_MAXBUFFERSIZE];
} _EMSRxBuf;
void ICACHE_FLASH_ATTR emsuart_init();
void ICACHE_FLASH_ATTR emsuart_stop();
void ICACHE_FLASH_ATTR emsuart_start();
_EMS_TX_STATUS ICACHE_FLASH_ATTR emsuart_tx_buffer(uint8_t * buf, uint8_t len);

357
src/helpers.cpp Normal file
View File

@@ -0,0 +1,357 @@
/*
* 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 "helpers.h"
#include "telegram.h" // for EMS_VALUE_* settings
namespace emsesp {
// like itoa but for hex, and quicker
char * Helpers::hextoa(char * result, const uint8_t value) {
char * p = result;
uint8_t nib1 = (value >> 4) & 0x0F;
uint8_t nib2 = (value >> 0) & 0x0F;
*p++ = nib1 < 0xA ? '0' + nib1 : 'A' + nib1 - 0xA;
*p++ = nib2 < 0xA ? '0' + nib2 : 'A' + nib2 - 0xA;
*p = '\0'; // null terminate just in case
return result;
}
#ifdef EMSESP_STANDALONE
// special function to work outside of ESP's libraries
char * Helpers::ultostr(char * ptr, uint32_t value, const uint8_t base) {
unsigned long t = 0, res = 0;
unsigned long tmp = value;
int count = 0;
if (NULL == ptr) {
return NULL;
}
if (tmp == 0) {
count++;
}
while (tmp > 0) {
tmp = tmp / base;
count++;
}
ptr += count;
*ptr = '\0';
do {
res = value - base * (t = value / base);
if (res < 10) {
*--ptr = '0' + res;
} else if ((res >= 10) && (res < 16)) {
*--ptr = 'A' - 10 + res;
}
} while ((value = t) != 0);
return (ptr);
}
#endif
/*
* itoa for 2 byte signed (short) integers
* written by Lukás Chmela, Released under GPLv3. http://www.strudel.org.uk/itoa/ version 0.4
*/
char * Helpers::itoa(char * result, int16_t value, const uint8_t base) {
// check that the base if valid
if (base < 2 || base > 36) {
*result = '\0';
return result;
}
char * ptr = result, *ptr1 = result, tmp_char;
int16_t tmp_value;
do {
tmp_value = value;
value /= base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - value * base)];
} while (value);
// Apply negative sign
if (tmp_value < 0) {
*ptr++ = '-';
}
*ptr-- = '\0';
while (ptr1 < ptr) {
tmp_char = *ptr;
*ptr-- = *ptr1;
*ptr1++ = tmp_char;
}
return result;
}
// for decimals 0 to 99, printed as a 2 char string
char * Helpers::smallitoa(char * result, const uint8_t value) {
result[0] = ((value / 10) == 0) ? '0' : (value / 10) + '0';
result[1] = (value % 10) + '0';
result[2] = '\0';
return result;
}
// for decimals 0 to 999, printed as a string
char * Helpers::smallitoa(char * result, const uint16_t value) {
result[0] = ((value / 100) == 0) ? '0' : (value / 100) + '0';
result[1] = (((value % 100) / 10) == 0) ? '0' : ((value % 100) / 10) + '0';
result[2] = (value % 10) + '0';
result[3] = '\0';
return result;
}
// convert unsigned int (single byte) to text value and returns it
// format: 255=boolean, 0=no formatting, otherwise divide by format
char * Helpers::render_value(char * result, uint8_t value, uint8_t format) {
// special check if its a boolean
if (format == EMS_VALUE_BOOL) {
if (value == EMS_VALUE_BOOL_OFF) {
strlcpy(result, "off", 5);
} else if (value == EMS_VALUE_BOOL_NOTSET) {
return nullptr;
} else {
strlcpy(result, "on", 5); // assume on. could have value 0x01 or 0xFF
}
return result;
}
if (!hasValue(value)) {
return nullptr;
}
if (!format) {
itoa(result, value, 10); // format = 0
return result;
}
char s2[5];
// special case for / 2
if (format == 2) {
strlcpy(result, itoa(s2, value >> 1, 10), 5);
strlcat(result, ".", 5);
strlcat(result, ((value & 0x01) ? "5" : "0"), 5);
return result;
}
strlcpy(result, itoa(s2, value / format, 10), 5);
strlcat(result, ".", 5);
strlcat(result, itoa(s2, value % format, 10), 5);
return result;
}
// float: convert float to char
// format is the precision, 0 to 8
char * Helpers::render_value(char * result, const float value, const uint8_t format) {
long p[] = {0, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000};
char * ret = result;
long whole = (long)value;
Helpers::itoa(result, whole, 10);
while (*result != '\0') {
result++;
}
*result++ = '.';
long decimal = abs((long)((value - whole) * p[format]));
itoa(result, decimal, 10);
return ret;
}
// int16: convert short (two bytes) to text string and returns string
// format: 0=no division, other divide by the value given and render with a decimal point
char * Helpers::render_value(char * result, const int16_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
}
// just print it if mo conversion required (format = 0)
if (!format) {
itoa(result, value, 10);
return result;
}
int16_t new_value = value;
result[0] = '\0';
// check for negative values
if (new_value < 0) {
strlcpy(result, "-", 10);
new_value *= -1; // convert to positive
} else {
strlcpy(result, "", 10);
}
// do floating point
char s2[10] = {0};
if (format == 2) {
// divide by 2
strlcat(result, itoa(s2, new_value / 2, 10), 10);
strlcat(result, ".", 10);
strlcat(result, ((new_value & 0x01) ? "5" : "0"), 10);
} else {
strlcat(result, itoa(s2, new_value / format, 10), 10);
strlcat(result, ".", 10);
strlcat(result, itoa(s2, new_value % format, 10), 10);
}
return result;
}
// uint16: convert unsigned short (two bytes) to text string and prints it
char * Helpers::render_value(char * result, const uint16_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
}
return (render_value(result, (int16_t)value, format)); // use same code, force it to a signed int
}
// int8: convert signed byte to text string and prints it
char * Helpers::render_value(char * result, const int8_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
}
return (render_value(result, (int16_t)value, format)); // use same code, force it to a signed int
}
// uint32: render long (4 byte) unsigned values
char * Helpers::render_value(char * result, const uint32_t value, const uint8_t format) {
if (!hasValue(value)) {
return nullptr;
}
char s[20];
result[0] = '\0';
#ifndef EMSESP_STANDALONE
if (!format) {
strlcat(result, ltoa(value, s, 10), 20); // format is 0
} else {
strlcat(result, ltoa(value / format, s, 10), 20);
strlcat(result, ".", 20);
strlcat(result, ltoa(value % format, s, 10), 20);
}
#else
if (!format) {
strlcat(result, ultostr(s, value, 10), 20); // format is 0
} else {
strncat(result, ultostr(s, value / format, 10), 20);
strlcat(result, ".", 20);
strncat(result, ultostr(s, value % format, 10), 20);
}
#endif
return result;
}
// creates string of hex values from an arrray of bytes
std::string Helpers::data_to_hex(const uint8_t * data, const uint8_t length) {
if (length == 0) {
return uuid::read_flash_string(F("<empty>"));
}
std::string str(160, '\0');
char buffer[4];
char * p = &str[0];
for (uint8_t i = 0; i < length; i++) {
Helpers::hextoa(buffer, data[i]);
*p++ = buffer[0];
*p++ = buffer[1];
*p++ = ' '; // space
}
*--p = '\0'; // null terminate just in case, loosing the trailing space
return str;
}
// takes a hex string and convert it to an unsigned 32bit number (max 8 hex digits)
// works with only positive numbers
uint32_t Helpers::hextoint(const char * hex) {
uint32_t val = 0;
while (*hex) {
// get current character then increment
char byte = *hex++;
// transform hex character to the 4bit equivalent number, using the ascii table indexes
if (byte >= '0' && byte <= '9')
byte = byte - '0';
else if (byte >= 'a' && byte <= 'f')
byte = byte - 'a' + 10;
else if (byte >= 'A' && byte <= 'F')
byte = byte - 'A' + 10;
else
return 0; // error
// shift 4 to make space for new digit, and add the 4 bits of the new digit
val = (val << 4) | (byte & 0xF);
}
return val;
}
// quick char to long
uint16_t Helpers::atoint(const char * value) {
unsigned int x = 0;
while (*value != '\0') {
x = (x * 10) + (*value - '0');
++value;
}
return x;
}
// rounds a number to 2 decimal places
// example: round2(3.14159) -> 3.14
double Helpers::round2(double value) {
return (int)(value * 100 + 0.5) / 100.0;
}
bool Helpers::check_abs(const int32_t i) {
return ((i < 0 ? -i : i) != 0xFFFFFF);
}
// for booleans, use isBool true
bool Helpers::hasValue(const uint8_t v, bool isBool) {
if (isBool) {
return (v != EMS_VALUE_BOOL_NOTSET);
}
return (v != EMS_VALUE_UINT_NOTSET);
}
bool Helpers::hasValue(const int8_t v) {
return (v != EMS_VALUE_INT_NOTSET);
}
// for short these are typically 0x8300, 0x7D00 and sometimes 0x8000
bool Helpers::hasValue(const int16_t v) {
return (abs(v) < EMS_VALUE_USHORT_NOTSET);
}
bool Helpers::hasValue(const uint16_t v) {
return (v < EMS_VALUE_USHORT_NOTSET);
}
bool Helpers::hasValue(const uint32_t v) {
return (v != EMS_VALUE_ULONG_NOTSET);
}
} // namespace emsesp

60
src/helpers.h Normal file
View File

@@ -0,0 +1,60 @@
/*
* 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 EMSESP_HELPERS_H
#define EMSESP_HELPERS_H
#include <Arduino.h>
#include <uuid/common.h>
namespace emsesp {
class Helpers {
public:
static char * hextoa(char * result, const uint8_t value);
static std::string data_to_hex(const uint8_t * data, const uint8_t length);
static char * render_value(char * result, const float value, const uint8_t format); // format is the precision
static char * render_value(char * result, const uint8_t value, const uint8_t format);
static char * render_value(char * result, const int8_t value, const uint8_t format);
static char * render_value(char * result, const uint16_t value, const uint8_t format);
static char * render_value(char * result, const uint32_t value, const uint8_t format);
static char * render_value(char * result, const int16_t value, const uint8_t format);
static char * smallitoa(char * result, const uint8_t value);
static char * smallitoa(char * result, const uint16_t value);
static char * itoa(char * result, int16_t value, const uint8_t base = 10);
static uint32_t hextoint(const char * hex);
static uint16_t atoint(const char * value);
static bool check_abs(const int32_t i);
static double round2(double value);
#ifdef EMSESP_STANDALONE
static char * ultostr(char * ptr, uint32_t value, const uint8_t base);
#endif
static bool hasValue(const uint8_t v, bool isBool = false); // use isBool=true for bool's
static bool hasValue(const int8_t v);
static bool hasValue(const int16_t v);
static bool hasValue(const uint16_t v);
static bool hasValue(const uint32_t v);
};
} // namespace emsesp
#endif

27
src/main.cpp Normal file
View File

@@ -0,0 +1,27 @@
/*
* 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 "emsesp.h"
void setup() {
emsesp::EMSESP::start();
}
void loop() {
emsesp::EMSESP::loop();
}

423
src/mqtt.cpp Normal file
View File

@@ -0,0 +1,423 @@
/*
* 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 "mqtt.h"
#include "emsesp.h"
#include "version.h"
MAKE_PSTR_WORD(connected)
MAKE_PSTR_WORD(disconnected)
MAKE_PSTR(logger_name, "mqtt")
namespace emsesp {
#ifndef EMSESP_STANDALONE
AsyncMqttClient * Mqtt::mqttClient_;
#endif
// static parameters we make global
std::string Mqtt::hostname_;
uint8_t Mqtt::mqtt_qos_;
uint16_t Mqtt::publish_time_;
std::vector<Mqtt::MQTTFunction> Mqtt::mqtt_functions_;
uint16_t Mqtt::mqtt_publish_fails_ = 0;
size_t Mqtt::maximum_mqtt_messages_ = Mqtt::MAX_MQTT_MESSAGES;
uint16_t Mqtt::mqtt_message_id_ = 0;
std::deque<Mqtt::QueuedMqttMessage> Mqtt::mqtt_messages_;
char will_topic_[Mqtt::MQTT_TOPIC_MAX_SIZE]; // because MQTT library keeps only char pointer
uuid::log::Logger Mqtt::logger_{F_(logger_name), uuid::log::Facility::DAEMON};
Mqtt::QueuedMqttMessage::QueuedMqttMessage(uint16_t id, std::shared_ptr<MqttMessage> && content)
: id_(id)
, content_(std::move(content)) {
retry_count_ = 0;
packet_id_ = 0;
}
MqttMessage::MqttMessage(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)
: device_id_(device_id)
, topic_(topic)
, mqtt_function_(mqtt_function) {
}
// 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 is wasteful.
// strip out everything until the last /
size_t found = topic.find_last_of("/"); // returns npos which is -1
mqtt_functions_.emplace_back(device_id, std::move(topic.substr(found + 1)), 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 device
void Mqtt::subscribe(const std::string & topic, mqtt_function_p cb) {
subscribe(0, topic, cb); // no device_id needed, if generic to EMS-ESP
}
// Main MQTT loop
// Checks for connection, establishes a connection if not
// sends out top item on publish queue
void Mqtt::loop() {
// exit if MQTT is not enabled or ig there is no WIFI
#ifndef EMSESP_STANDALONE
if (!connected()) {
#else
if (false) {
#endif
return;
}
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;
EMSESP::publish_all_values();
}
// publish top item from MQTT queue to stop flooding
if ((uint32_t)(currentMillis - last_mqtt_poll_) > MQTT_PUBLISH_WAIT) {
last_mqtt_poll_ = currentMillis;
process_queue();
}
}
// print MQTT log and other stuff to console
void Mqtt::show_mqtt(uuid::console::Shell & shell) {
shell.printfln(F("MQTT is %s"), connected() ? uuid::read_flash_string(F_(connected)).c_str() : uuid::read_flash_string(F_(disconnected)).c_str());
shell.printfln(F("MQTT publish fails: %lu"), mqtt_publish_fails_);
shell.println();
// show queues
if (mqtt_messages_.empty()) {
shell.printfln(F("MQTT queue is empty"));
shell.println();
return;
}
shell.printfln(F("MQTT queue (%d messages):"), mqtt_messages_.size());
for (const auto & message : mqtt_messages_) {
auto content = message.content_;
if (content->operation == Operation::PUBLISH) {
// Publish messages
if (message.retry_count_ == 0) {
if (message.packet_id_ == 0) {
shell.printfln(F(" [%02d] (Pub) topic=%s payload=%s"), message.id_, content->topic.c_str(), content->payload.c_str());
} else {
shell.printfln(F(" [%02d] (Pub) topic=%s payload=%s (pid %d)"),
message.id_,
content->topic.c_str(),
content->payload.c_str(),
message.packet_id_);
}
} else {
shell.printfln(F(" [%02d] (Pub) topic=%s payload=%s (pid %d, retry #%d)"),
message.id_,
content->topic.c_str(),
content->payload.c_str(),
message.packet_id_,
message.retry_count_);
}
} else {
// Subscribe messages
shell.printfln(F(" [%02d] (Sub) topic=%s"), message.id_, content->topic.c_str());
}
}
shell.println();
}
// simulate receiving a MQTT message, only for testing
void Mqtt::incoming(char * topic, char * payload) {
on_message(topic, payload, strlen(payload));
}
// received MQTT message
void Mqtt::on_message(char * topic, char * payload, size_t len) {
if (len == 0) {
return;
}
// convert payload to a null-terminated char string
char message[len + 2];
strlcpy(message, payload, len + 1);
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] Received %s => %s (length %d)"), topic, message, len);
#endif
// strip out everything until the last /
char * topic_magnitude = strrchr(topic, '/');
if (topic_magnitude != nullptr) {
topic = topic_magnitude + 1;
}
// Send message event to custom service
// this will pick the first topic that matches, so for multiple devices of the same type it's gonna fail
for (const auto & mf : mqtt_functions_) {
if (strcmp(topic, mf.topic_.c_str()) == 0) {
(mf.mqtt_function_)(message);
return;
}
}
// if we got here we didn't find a topic match
LOG_DEBUG(F("No responding handler found for topic %s"), topic);
}
// 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_; })
== 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());
}
}
shell.println();
}
// called when an MQTT Publish ACK is received
// its a poor man's QOS we assume the ACK represents the last Publish sent
// check if ACK matches the last Publish we sent, if not report an error. Only if qos is 1 or 2
// and always remove from queue
void Mqtt::on_publish(uint16_t packetId) {
// find the MQTT message in the queue and remove it
if (mqtt_messages_.empty()) {
return;
}
auto mqtt_message = mqtt_messages_.front();
// if the last published failed, don't bother checking it. wait for the next retry
if (mqtt_message.packet_id_ == 0) {
return;
}
if (mqtt_message.packet_id_ != packetId) {
LOG_DEBUG(F("Mismatch, expecting PID %d, got %d"), mqtt_message.packet_id_, packetId);
mqtt_publish_fails_++; // increment error count
}
mqtt_messages_.pop_front(); // always remove from queue, regardless if there was a successful ACK
}
// builds up a topic by prefixing the hostname
char * Mqtt::make_topic(char * result, const std::string & topic) {
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(AsyncMqttClient * mqttClient) {
mqttClient_ = mqttClient; // copy over from esp8266-react's MQTT service
// get the hostname, which we'll use to prefix to all topics
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) { hostname_ = wifiSettings.hostname.c_str(); });
// fetch MQTT settings
// EMSESP::esp8266React.getMqttSettingsService()->read([&](MqttSettings & mqttSettings) { mqtt_enabled_ = mqttSettings.enabled; });
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
publish_time_ = settings.publish_time * 1000; // convert to milliseconds
mqtt_qos_ = settings.mqtt_qos;
});
#ifndef EMSESP_STANDALONE
mqttClient_->setWill(make_topic(will_topic_, "status"), 1, true, "offline"); // with qos 1, retain true
#endif
}
void Mqtt::set_publish_time(uint16_t publish_time) {
publish_time_ = publish_time * 1000; // convert to milliseconds
}
void Mqtt::set_qos(uint8_t mqtt_qos) {
mqtt_qos_ = mqtt_qos;
}
// MQTT onConnect - when a connect is established
void Mqtt::on_connect() {
// send info topic appended with the version information as JSON
StaticJsonDocument<90> doc;
doc["event"] = "start";
doc["version"] = EMSESP_APP_VERSION;
doc["ip"] = WiFi.localIP().toString();
publish("info", doc, false); // send with retain off
publish("status", "online", true); // say we're alive to the Last Will topic, with retain on
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
if (topic.empty()) {
return;
}
auto message = std::make_shared<MqttMessage>(Operation::PUBLISH, 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));
}
// 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);
LOG_DEBUG(F("Adding a subscription for %s"), topic.c_str());
// 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));
}
// MQTT Publish, using a specific retain flag
void Mqtt::publish(const std::string & topic, const std::string & payload, bool retain) {
queue_publish_message(topic, payload, retain);
}
void Mqtt::publish(const std::string & topic, const JsonDocument & payload, bool retain) {
// convert json to string
std::string payload_text;
serializeJson(payload, payload_text);
queue_publish_message(topic, payload_text, retain);
}
// for booleans, which get converted to string values 1 and 0
void Mqtt::publish(const std::string & topic, const bool value) {
queue_publish_message(topic, value ? "1" : "0", false);
}
// no payload
void Mqtt::publish(const std::string & topic) {
queue_publish_message(topic, "", false);
}
// publish all queued messages to MQTT
void Mqtt::process_all_queue() {
while (!mqtt_messages_.empty()) {
process_queue();
}
}
// take top from queue and try and publish it
// assumes there is an MQTT connection
void Mqtt::process_queue() {
if (mqtt_messages_.empty()) {
return;
}
// fetch first from queue and create the full topic name
auto mqtt_message = mqtt_messages_.front();
auto message = mqtt_message.content_;
// append the hostname to the topic, unless we're doing native HA which has a different format
// if the topic starts with "homeassistant" we leave it untouched, otherwise append host
char full_topic[MQTT_TOPIC_MAX_SIZE];
if (strncmp(message->topic.c_str(), "homeassistant/", 13) == 0) {
strcpy(full_topic, message->topic.c_str());
} else {
make_topic(full_topic, message->topic);
}
// if we're subscribing...
if (message->operation == Operation::SUBSCRIBE) {
LOG_DEBUG(F("Subscribing to topic: %s"), full_topic);
#ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_->subscribe(full_topic, mqtt_qos_);
#else
uint16_t packet_id = 1;
#endif
if (!packet_id) {
LOG_DEBUG(F("Error subscribing to %s, error %d"), full_topic, packet_id);
}
mqtt_messages_.pop_front(); // remove the message from the queue
return;
}
// if this has already been published and we're waiting for an ACK, don't publish again
// it will have a real packet ID
if (mqtt_message.packet_id_ > 0) {
return;
}
// else try and publish it
#ifndef EMSESP_STANDALONE
uint16_t packet_id = mqttClient_->publish(full_topic, mqtt_qos_, message->retain, message->payload.c_str(), message->payload.size(), false, mqtt_message.id_);
#else
uint16_t packet_id = 1;
#endif
LOG_DEBUG(F("Publishing topic %s (#%02d, attempt #%d, pid %d)"), full_topic, mqtt_message.id_, mqtt_message.retry_count_ + 1, packet_id);
if (packet_id == 0) {
// it failed. if we retried n times, give up. remove from queue
if (mqtt_message.retry_count_ == (MQTT_PUBLISH_MAX_RETRY - 1)) {
LOG_ERROR(F("Failed to publish to %s after %d attempts"), full_topic, mqtt_message.retry_count_ + 1);
mqtt_publish_fails_++; // increment failure counter
mqtt_messages_.pop_front(); // delete
return;
} else {
mqtt_messages_.front().retry_count_++;
LOG_DEBUG(F("Failed to publish to %s. Trying again, #%d"), full_topic, mqtt_message.retry_count_ + 1);
return; // leave on queue for next time so it gets republished
}
}
// if we have ACK set with QOS 1 or 2, leave on queue and let the ACK process remove it
// but add the packet_id so we can check it later
if (mqtt_qos_ != 0) {
mqtt_messages_.front().packet_id_ = packet_id;
// LOG_DEBUG(F("Setting packetID for ACK to %d"), packet_id);
return;
}
mqtt_messages_.pop_front(); // remove the message from the queue
}
} // namespace emsesp

172
src/mqtt.h Normal file
View File

@@ -0,0 +1,172 @@
/*
* 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 EMSESP_MQTT_H_
#define EMSESP_MQTT_H_
#include <Arduino.h>
#include <ArduinoJson.h>
#include <string>
#include <vector>
#include <deque>
#include <functional>
#ifndef EMSESP_STANDALONE
#include <AsyncMqttClient.h>
#endif
#include "helpers.h"
#include "system.h"
#include "console.h"
#include <uuid/log.h>
using uuid::console::Shell;
#define EMSESP_MAX_JSON_SIZE_SMALL 200 // for smaller json docs
#define EMSESP_MAX_JSON_SIZE_MEDIUM 800 // for smaller json docs from ems devices
#define EMSESP_MAX_JSON_SIZE_LARGE 1500 // for large json docs from ems devices, like boiler or thermostat data
namespace emsesp {
using mqtt_function_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() = default;
const uint8_t operation;
const std::string topic;
const std::string payload;
const bool retain;
};
class Mqtt {
public:
void loop();
void start(AsyncMqttClient * mqttClient);
void set_publish_time(uint16_t publish_time);
void set_qos(uint8_t mqtt_qos);
enum Operation { PUBLISH, SUBSCRIBE };
static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 60; // include host and base etc
// are static to be accessed from EMS devices
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 publish(const std::string & topic, const std::string & payload, bool retain = false);
static void publish(const std::string & topic, const JsonDocument & payload, bool retain = false);
static void publish(const std::string & topic, const bool value);
static void publish(const std::string & topic);
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_id);
static void show_mqtt(uuid::console::Shell & shell);
static void on_connect();
void disconnect() {
#ifdef EMSESP_STANDALONE
return;
#else
mqttClient_->disconnect();
#endif
}
void incoming(char * topic, char * payload); // for testing
static bool connected() {
#ifdef EMSESP_STANDALONE
return true;
#else
return mqttClient_->connected();
#endif
}
static uint32_t publish_fails() {
return mqtt_publish_fails_;
}
private:
static uuid::log::Logger logger_;
class QueuedMqttMessage {
public:
QueuedMqttMessage(uint16_t id, std::shared_ptr<MqttMessage> && content);
~QueuedMqttMessage() = default;
const uint16_t id_;
const std::shared_ptr<const MqttMessage> content_;
uint8_t retry_count_;
uint16_t packet_id_;
};
static std::deque<QueuedMqttMessage> mqtt_messages_;
#ifndef EMSESP_STANDALONE
static AsyncMqttClient * mqttClient_;
#endif
static constexpr size_t MAX_MQTT_MESSAGES = 50;
static size_t maximum_mqtt_messages_;
static uint16_t mqtt_message_id_;
static bool mqtt_retain_;
static constexpr uint8_t MQTT_QUEUE_MAX_SIZE = 50;
static constexpr uint32_t MQTT_PUBLISH_WAIT = 250; // 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);
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 {
public:
MQTTFunction(uint8_t device_id, const std::string && topic, mqtt_function_p mqtt_function);
~MQTTFunction() = default;
uint8_t device_id_; // which device ID owns this
std::string topic_;
mqtt_function_p mqtt_function_;
};
static std::vector<MQTTFunction> mqtt_functions_; // list of mqtt 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_;
}; // namespace emsesp
} // namespace emsesp
#endif

View File

@@ -1,102 +0,0 @@
/*
* my_config.h
*
* All configurations and customization's go here
*
* Paul Derbyshire - https://github.com/proddy/EMS-ESP
*/
#pragma once
#include "ems.h"
// TOPICS with *_CMD_* are used for receiving commands from an MQTT Broker
// EMS-ESP will subscribe to these topics
#define TOPIC_GENERIC_CMD "generic_cmd" // for receiving generic system commands via MQTT
// MQTT for thermostat
// these topics can be suffixed with a Heating Circuit number, e.g. thermostat_cmd_temp1 and thermostat_data1
#define TOPIC_THERMOSTAT_DATA "thermostat_data" // for sending thermostat values to MQTT
#define TOPIC_THERMOSTAT_CMD "thermostat_cmd" // for receiving thermostat commands via MQTT
#define TOPIC_THERMOSTAT_CMD_TEMP_HA "thermostat_cmd_temp" // temp changes via MQTT, for HA climate component
#define TOPIC_THERMOSTAT_CMD_MODE_HA "thermostat_cmd_mode" // mode changes via MQTT, for HA climate component
#define TOPIC_THERMOSTAT_CMD_TEMP "temp" // temp changes via MQTT
#define TOPIC_THERMOSTAT_CMD_MODE "mode" // mode changes via MQTT
#define TOPIC_THERMOSTAT_CMD_DAYTEMP "daytemp" // day temp (RC35 specific)
#define TOPIC_THERMOSTAT_CMD_NIGHTTEMP "nighttemp" // night temp (RC35 specific)
#define TOPIC_THERMOSTAT_CMD_HOLIDAYTEMP "holidayttemp" // holiday temp (RC35 specific)
#define TOPIC_THERMOSTAT_CMD_NOFROSTTEMP "nofrosttemp" // frost temp (Junkers specific)
#define TOPIC_THERMOSTAT_CMD_ECOTEMP "ecotemp" // eco temp (Junkers specific)
#define TOPIC_THERMOSTAT_CMD_HEATTEMP "heattemp" // heat temp (Junkers specific)
#define THERMOSTAT_CURRTEMP "currtemp" // current temperature
#define THERMOSTAT_SELTEMP "seltemp" // selected temperature
#define THERMOSTAT_HC "hc" // which heating circuit number
#define THERMOSTAT_MODE "mode" // mode
#define THERMOSTAT_MODETYPE "modetype" // mode type
#define THERMOSTAT_DAYTEMP "daytemp" // RC35 specific
#define THERMOSTAT_NIGHTTEMP "nighttemp" // RC35 specific
#define THERMOSTAT_HOLIDAYTEMP "holidayttemp" // RC35 specific
#define THERMOSTAT_HEATINGTYPE "heatingtype" // RC35 specific (3=floorheating)
#define THERMOSTAT_CIRCUITCALCTEMP "circuitcalctemp" // RC35 specific
// mixing module
#define MIXING_HC "hc" // which heating circuit number
#define MIXING_WWC "wwc" // which warm water circuit number
// MQTT for boiler
#define TOPIC_BOILER_DATA "boiler_data" // for sending boiler values to MQTT
#define TOPIC_BOILER_TAPWATER_ACTIVE "tapwater_active" // if hot tap water is running
#define TOPIC_BOILER_HEATING_ACTIVE "heating_active" // if heating is on
#define TOPIC_BOILER_CMD "boiler_cmd" // for receiving boiler commands via MQTT
#define TOPIC_BOILER_CMD_WWACTIVATED "boiler_cmd_wwactivated" // change water on/off
#define TOPIC_BOILER_CMD_WWONETIME "boiler_cmd_wwonetime" // warm warter one time loading
#define TOPIC_BOILER_CMD_WWCIRCULATION "boiler_cmd_wwcirculation" // start warm warter circulation
#define TOPIC_BOILER_CMD_WWTEMP "boiler_cmd_wwtemp" // wwtemp changes via MQTT
#define TOPIC_BOILER_CMD_COMFORT "comfort" // ww comfort setting via MQTT
#define TOPIC_BOILER_CMD_FLOWTEMP "flowtemp" // flowtemp value via MQTT
// MQTT for settings
#define TOPIC_SETTINGS_DATA "settings_data" // for sending settings values to MQTT
#define TOPIC_SETTINGS_CMD "settings_cmd" // for receiving settings commands via MQTT
#define TOPIC_SETTINGS_CMD_DISPLAY "display" // change display
#define TOPIC_SETTINGS_CMD_LANGUAGE "language" // change language
#define TOPIC_SETTINGS_CMD_BUILDING "building" // change building
#define TOPIC_SETTINGS_CMD_MINEXTTEMP "minextTemp" // change min. ext. temp.
// MQTT for mixing device
#define TOPIC_MIXING_DATA "mixing_data" // for sending mixing device values to MQTT
// MQTT for SM10/SM100/SM200 Solar Module
#define TOPIC_SM_DATA "sm_data" // topic name
#define SM_COLLECTORTEMP "collectortemp" // collector temp
#define SM_BOTTOMTEMP "bottomtemp" // bottom temp1
#define SM_BOTTOMTEMP2 "bottomtemp2" // bottom temp2
#define SM_PUMPMODULATION "pumpmodulation" // pump modulation
#define SM_PUMP "pump" // pump active
#define SM_VALVESTATUS "valvestatus" // valve status
#define SM_ENERGYLASTHOUR "energylasthour" // energy last hour
#define SM_ENERGYTODAY "energytoday" // energy today
#define SM_ENERGYTOTAL "energytotal" // energy total
#define SM_PUMPWORKMIN "pumpWorkMin" // Total minutes
// MQTT for HP (HeatPump)
#define TOPIC_HP_DATA "hp_data" // topic name
#define HP_PUMPMODULATION "pumpmodulation" // pump modulation
#define HP_PUMPSPEED "pumpspeed" // pump speed
// shower time
#define TOPIC_SHOWER_DATA "shower_data" // for sending shower time results
#define TOPIC_SHOWER_TIMER "timer" // toggle switch for enabling the shower logic
#define TOPIC_SHOWER_ALERT "alert" // toggle switch for enabling the shower alarm logic
#define TOPIC_SHOWER_COLDSHOT "coldshot" // used to trigger a coldshot from an MQTT command
#define TOPIC_SHOWER_DURATION "duration" // duration of the last shower
// MQTT for External Sensors
#define TOPIC_EXTERNAL_SENSORS "sensors" // topic for sending sensor values to MQTT
#define PAYLOAD_EXTERNAL_SENSOR_NUM "sensor" // which sensor #
#define PAYLOAD_EXTERNAL_SENSOR_ID "id"
#define PAYLOAD_EXTERNAL_SENSOR_TEMP "temp"

141
src/roomcontrol.cpp Normal file
View File

@@ -0,0 +1,141 @@
/*
* 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 "roomcontrol.h"
namespace emsesp {
static uint32_t rc_time_ = 0;
static int16_t remotetemp[4] = {EMS_VALUE_SHORT_NOTSET, EMS_VALUE_SHORT_NOTSET, EMS_VALUE_SHORT_NOTSET, EMS_VALUE_SHORT_NOTSET};
/**
* set the temperature,
*/
void Roomctrl::set_remotetemp(const uint8_t hc, const int16_t temp) {
if (hc > 3) {
return;
}
remotetemp[hc] = temp;
}
/**
* if remote control is active send the temperature every minute
*/
void Roomctrl::send(const uint8_t addr) {
uint8_t hc_ = addr - ADDR;
// check address, reply only on addresses 0x18..0x1B
if (hc_ > 3) {
return;
}
// no reply if the temperature is not set
if (remotetemp[hc_] == EMS_VALUE_SHORT_NOTSET) {
return;
}
if (uuid::get_uptime() - rc_time_ > SEND_INTERVAL) { // send every minute
rc_time_ = uuid::get_uptime(); // use EMS-ESP's millis() to prevent overhead
temperature(addr, 0x00); // send to all
} else {
// acknowledge every poll, otherwise the master shows error A22-816
EMSuart::send_poll(addr);
}
}
/**
* check if there is a message for the remote room controller
*/
void Roomctrl::check(const uint8_t addr, const uint8_t * data) {
uint8_t hc_ = (addr & 0x7F) - ADDR;
// check address, reply only on addresses 0x18..0x1B
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.
return;
}
// for now we only reply to version and remote temperature
if (data[2] == 0x02) {
version(addr, data[0]);
} else if (data[2] == 0xAF && data[3] == 0) {
temperature(addr, data[0]);
} else {
unknown(addr, data[0], data[2], data[3]);
}
}
/**
* send version info RC20 (Prod. 113, Ver. 02.01) or RC20RF (Prod. 93, Ver. 02.00)
*/
void Roomctrl::version(uint8_t addr, uint8_t dst) {
uint8_t data[10];
data[0] = addr;
data[1] = dst;
data[2] = 0x02;
data[3] = 0;
data[4] = 113; // set RC20 id 113, Ver 02.01
data[5] = 0x02;
data[6] = 0x01;
data[7] = EMSbus::calculate_crc(data, 7); // apppend CRC
EMSuart::transmit(data, 8);
}
/**
* unknown message id, we reply with empty message
*/
void Roomctrl::unknown(uint8_t addr, uint8_t dst, uint8_t type, uint8_t offset) {
uint8_t data[10];
data[0] = addr;
data[1] = dst;
data[2] = type;
data[3] = offset;
data[4] = EMSbus::calculate_crc(data, 4); // apppend CRC
EMSuart::transmit(data, 5);
}
/**
* send the room temperature in message 0xAF
*/
void Roomctrl::temperature(uint8_t addr, uint8_t dst) {
uint8_t data[10];
uint8_t hc_ = addr - ADDR;
data[0] = addr;
data[1] = dst;
data[2] = 0xAF;
data[3] = 0;
data[4] = (uint8_t)(remotetemp[hc_] >> 8);
data[5] = (uint8_t)(remotetemp[hc_] & 0xFF);
data[6] = 0;
data[7] = EMSbus::calculate_crc(data, 7); // apppend CRC
EMSuart::transmit(data, 8);
}
/**
* send a nack if someone want to write to us.
*/
void Roomctrl::nack_write() {
uint8_t data[1];
data[0] = TxService::TX_WRITE_FAIL;
EMSuart::transmit(data, 1);
}
} // namespace emsesp

44
src/roomcontrol.h Normal file
View File

@@ -0,0 +1,44 @@
/*
* 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 EMSESP_ROOMCONTROL_H
#define EMSESP_ROOMCONTROL_H
#include "emsesp.h"
namespace emsesp {
class Roomctrl {
public:
static void send(const uint8_t addr);
static void check(const uint8_t addr, const uint8_t * data);
static void set_remotetemp(const uint8_t hc, const int16_t temp);
private:
static constexpr uint8_t ADDR = 0x18;
static constexpr uint32_t SEND_INTERVAL = 60000; // 1 minute
static void version(uint8_t addr, uint8_t dst);
static void unknown(uint8_t addr, uint8_t dst, uint8_t type, uint8_t offset);
static void temperature(uint8_t addr, uint8_t dst);
static void nack_write();
};
} // namespace emsesp
#endif

301
src/sensors.cpp Normal file
View File

@@ -0,0 +1,301 @@
/*
* 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/>.
*/
// code written by nomis - https://github.com/nomis
#include "sensors.h"
#include "emsesp.h"
MAKE_PSTR(logger_name, "sensors")
namespace emsesp {
uuid::log::Logger Sensors::logger_{F_(logger_name), uuid::log::Facility::DAEMON};
void Sensors::start() {
// copy over values from MQTT so we don't keep on quering the filesystem
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
mqtt_format_ = settings.mqtt_format; // single, nested or ha
});
// if we're using HA MQTT Discovery, send out the config
// currently we just do this for a single sensor (sensor1)
if (mqtt_format_ == MQTT_format::HA) {
// Mqtt::publish(topic); // empty payload, this remove any previous config sent to HA
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
doc["dev_cla"] = "temperature";
doc["name"] = "ems-esp-sensor1";
doc["uniq_id"] = "ems-esp-sensor1";
doc["~"] = "homeassistant/sensor/ems-esp/external";
doc["stat_t"] = "~/state";
doc["unit_of_meas"] = "°C";
doc["val_tpl"] = "{{value_json.sensor1.temp}}";
Mqtt::publish("homeassistant/sensor/ems-esp/external/config", doc, true); // publish the config payload with retain flag
}
#ifndef EMSESP_STANDALONE
bus_.begin(SENSOR_GPIO);
#endif
}
void Sensors::loop() {
#ifndef EMSESP_STANDALONE
uint32_t time_now = uuid::get_uptime();
if (state_ == State::IDLE) {
if (time_now - last_activity_ >= READ_INTERVAL_MS) {
// LOG_DEBUG(F("Read sensor temperature")); // uncomment for debug
if (bus_.reset()) {
yield();
bus_.skip();
bus_.write(CMD_CONVERT_TEMP);
state_ = State::READING;
} else {
// no sensors found
// LOG_ERROR(F("Bus reset failed")); // uncomment for debug
devices_.clear(); // remove all know devices incase we have a disconnect
}
last_activity_ = time_now;
}
} else if (state_ == State::READING) {
if (temperature_convert_complete() && (time_now - last_activity_ > CONVERSION_MS)) {
// LOG_DEBUG(F("Scanning for sensors")); // uncomment for debug
bus_.reset_search();
found_.clear();
state_ = State::SCANNING;
last_activity_ = time_now;
} else if (time_now - last_activity_ > READ_TIMEOUT_MS) {
LOG_ERROR(F("Sensor read timeout"));
state_ = State::IDLE;
last_activity_ = time_now;
}
} 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;
} else {
uint8_t addr[ADDR_LEN] = {0};
if (bus_.search(addr)) {
bus_.depower();
if (bus_.crc8(addr, ADDR_LEN - 1) == addr[ADDR_LEN - 1]) {
switch (addr[0]) {
case TYPE_DS18B20:
case TYPE_DS18S20:
case TYPE_DS1822:
case TYPE_DS1825:
found_.emplace_back(addr);
found_.back().temperature_c_ = get_temperature_c(addr);
/*
// comment out for debugging
char result[10];
LOG_DEBUG(F("Temp of %s = %s"),
found_.back().to_string().c_str(),
Helpers::render_value(result, found_.back().temperature_c_, 2));
*/
break;
default:
LOG_ERROR(F("Unknown sensor %s"), Device(addr).to_string().c_str());
break;
}
} else {
LOG_ERROR(F("Invalid sensor %s"), Device(addr).to_string().c_str());
}
} else {
bus_.depower();
devices_ = std::move(found_);
found_.clear();
// LOG_DEBUG(F("Found %zu sensor(s). Adding them."), devices_.size()); // uncomment for debug
state_ = State::IDLE;
last_activity_ = time_now;
}
}
}
#endif
}
bool Sensors::temperature_convert_complete() {
#ifndef EMSESP_STANDALONE
return bus_.read_bit() == 1;
#else
return 1;
#endif
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
float Sensors::get_temperature_c(const uint8_t addr[]) {
#ifndef EMSESP_STANDALONE
if (!bus_.reset()) {
LOG_ERROR(F("Bus reset failed before reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
yield();
uint8_t scratchpad[SCRATCHPAD_LEN] = {0};
bus_.select(addr);
bus_.write(CMD_READ_SCRATCHPAD);
bus_.read_bytes(scratchpad, SCRATCHPAD_LEN);
yield();
if (!bus_.reset()) {
LOG_ERROR(F("Bus reset failed after reading scratchpad from %s"), Device(addr).to_string().c_str());
return NAN;
}
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],
scratchpad[1],
scratchpad[2],
scratchpad[3],
scratchpad[4],
scratchpad[5],
scratchpad[6],
scratchpad[7],
scratchpad[8],
Device(addr).to_string().c_str());
return NAN;
}
int16_t raw_value = ((int16_t)scratchpad[SCRATCHPAD_TEMP_MSB] << 8) | scratchpad[SCRATCHPAD_TEMP_LSB];
// Adjust based on device resolution
int resolution = 9 + ((scratchpad[SCRATCHPAD_CONFIG] >> 5) & 0x3);
switch (resolution) {
case 9:
raw_value &= ~0x1;
break;
case 10:
raw_value &= ~0x3;
break;
case 11:
raw_value &= ~0x7;
break;
case 12:
break;
}
return (float)raw_value / 16;
#else
return NAN;
#endif
}
#pragma GCC diagnostic pop
const std::vector<Sensors::Device> Sensors::devices() const {
return devices_;
}
Sensors::Device::Device(const uint8_t addr[])
: id_(((uint64_t)addr[0] << 56) | ((uint64_t)addr[1] << 48) | ((uint64_t)addr[2] << 40) | ((uint64_t)addr[3] << 32) | ((uint64_t)addr[4] << 24)
| ((uint64_t)addr[5] << 16) | ((uint64_t)addr[6] << 8) | (uint64_t)addr[7]) {
}
uint64_t Sensors::Device::id() const {
return id_;
}
std::string Sensors::Device::to_string() const {
std::string str(20, '\0');
snprintf_P(&str[0],
str.capacity() + 1,
PSTR("%02X-%04X-%04X-%04X-%02X"),
(unsigned int)(id_ >> 56) & 0xFF,
(unsigned int)(id_ >> 40) & 0xFFFF,
(unsigned int)(id_ >> 24) & 0xFFFF,
(unsigned int)(id_ >> 8) & 0xFFFF,
(unsigned int)(id_)&0xFF);
return str;
}
// send all dallas sensor values as a JSON package to MQTT
// assumes there are devices
void Sensors::publish_values() {
uint8_t num_devices = devices_.size();
if (num_devices == 0) {
return;
}
// if we're not using nested JSON, send each sensor out seperately
// sensor1, sensor2 etc...
// e.g. sensor_1 = {"temp":20.2}
if (mqtt_format_ == MQTT_format::SINGLE) {
StaticJsonDocument<100> doc;
for (const auto & device : devices_) {
char s[5];
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);
Mqtt::publish(topic, doc);
doc.clear(); // clear json doc so we can reuse the buffer again
}
return;
}
// group all sensors together - https://github.com/proddy/EMS-ESP/issues/327
// This is used for both NESTED and HA modes
// sensors = {
// "sensor1":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor2":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor3":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"},
// "sensor4":{"id":"28-EA41-9497-0E03-5F","temp":"23.25"}
// }
// const size_t capacity = num_devices * JSON_OBJECT_SIZE(2) + JSON_OBJECT_SIZE(num_devices);
DynamicJsonDocument doc(100 * num_devices);
uint8_t i = 1;
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);
} else {
char sensorID[10]; // sensor{1-n}
strlcpy(sensorID, "sensor", 10);
char s[5];
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);
}
}
if (mqtt_format_ == MQTT_format::HA) {
Mqtt::publish("homeassistant/sensor/ems-esp/external/state", doc);
} else {
Mqtt::publish("sensors", doc);
}
}
} // namespace emsesp

118
src/sensors.h Normal file
View File

@@ -0,0 +1,118 @@
/*
* 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/>.
*/
// code written by nomis - https://github.com/nomis
#ifndef EMSESP_SENSORS_H
#define EMSESP_SENSORS_H
#include <string>
#include <vector>
#include "helpers.h"
#include "mqtt.h"
#include "console.h"
#include <uuid/log.h>
#ifndef EMSESP_STANDALONE
#include <OneWire.h>
#endif
namespace emsesp {
class Sensors {
public:
class Device {
public:
Device(const uint8_t addr[]);
~Device() = default;
uint64_t id() const;
std::string to_string() const;
float temperature_c_ = NAN;
private:
const uint64_t id_;
};
Sensors() = default;
~Sensors() = default;
void start();
void loop();
void publish_values();
const std::vector<Device> devices() const;
private:
#if defined(ESP8266)
static constexpr uint8_t SENSOR_GPIO = 14; // D5
#elif defined(ESP32)
#ifdef WEMOS_D1_32
static constexpr uint8_t SENSOR_GPIO = 18; // Wemos D1-32 for compatibility D5
#else
static constexpr uint8_t SENSOR_GPIO = 14; // D5 is LED on wemos lolin D32, so use GPIO14
#endif
#endif
enum class State { IDLE, READING, SCANNING };
static constexpr size_t ADDR_LEN = 8;
static constexpr size_t SCRATCHPAD_LEN = 9;
static constexpr size_t SCRATCHPAD_TEMP_MSB = 1;
static constexpr size_t SCRATCHPAD_TEMP_LSB = 0;
static constexpr size_t SCRATCHPAD_CONFIG = 4;
// dallas chips
static constexpr uint8_t TYPE_DS18B20 = 0x28;
static constexpr uint8_t TYPE_DS18S20 = 0x10;
static constexpr uint8_t TYPE_DS1822 = 0x22;
static constexpr uint8_t TYPE_DS1825 = 0x3B;
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 uint8_t CMD_CONVERT_TEMP = 0x44;
static constexpr uint8_t CMD_READ_SCRATCHPAD = 0xBE;
static uuid::log::Logger logger_;
#ifndef EMSESP_STANDALONE
OneWire bus_;
#endif
bool temperature_convert_complete();
float get_temperature_c(const uint8_t addr[]);
uint32_t last_activity_ = uuid::get_uptime();
uint32_t last_publish_ = uuid::get_uptime();
State state_ = State::IDLE;
std::vector<Device> found_;
std::vector<Device> devices_;
uint8_t mqtt_format_;
};
} // namespace emsesp
#endif

137
src/shower.cpp Normal file
View File

@@ -0,0 +1,137 @@
/*
* 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 "shower.h"
MAKE_PSTR(logger_name, "shower")
namespace emsesp {
uuid::log::Logger Shower::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
void Shower::start() {
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
shower_timer_ = settings.shower_timer;
shower_alert_ = settings.shower_alert;
});
}
void Shower::loop() {
if (!shower_timer_) {
return;
}
uint32_t time_now = uuid::get_uptime();
// if already in cold mode, ignore all this logic until we're out of the cold blast
if (!doing_cold_shot_) {
// is the hot water running?
if (EMSESP::tap_water_active()) {
// if heater was previously off, start the timer
if (timer_start_ == 0) {
// hot water just started...
timer_start_ = time_now;
timer_pause_ = 0; // remove any last pauses
doing_cold_shot_ = false;
duration_ = 0;
shower_on_ = false;
} else {
// hot water has been on for a while
// first check to see if hot water has been on long enough to be recognized as a Shower/Bath
if (!shower_on_ && (time_now - timer_start_) > SHOWER_MIN_DURATION) {
shower_on_ = true;
Mqtt::publish("shower_active", (bool)true);
LOG_DEBUG(F("[Shower] hot water still running, starting shower timer"));
}
// check if the shower has been on too long
else if ((((time_now - timer_start_) > SHOWER_MAX_DURATION) && !doing_cold_shot_) && shower_alert_) {
shower_alert_start();
}
}
} else { // hot water is off
// if it just turned off, record the time as it could be a short pause
if ((timer_start_) && (timer_pause_ == 0)) {
timer_pause_ = time_now;
}
// if shower has been off for longer than the wait time
if ((timer_pause_) && ((time_now - timer_pause_) > SHOWER_PAUSE_TIME)) {
// it is over the wait period, so assume that the shower has finished and calculate the total time and publish
// because its unsigned long, can't have negative so check if length is less than OFFSET_TIME
if ((timer_pause_ - timer_start_) > SHOWER_OFFSET_TIME) {
duration_ = (timer_pause_ - timer_start_ - SHOWER_OFFSET_TIME);
if (duration_ > SHOWER_MIN_DURATION) {
Mqtt::publish("shower_active", (bool)false);
LOG_DEBUG(F("[Shower] finished with duration %d"), duration_);
publish_values();
}
}
// reset everything
timer_start_ = 0;
timer_pause_ = 0;
shower_on_ = false;
shower_alert_stop();
}
}
}
}
// turn back on the hot water for the shower
void Shower::shower_alert_stop() {
if (doing_cold_shot_) {
LOG_DEBUG(F("Shower Alert stopped"));
// Boiler::set_tapwarmwater_activated(true);
doing_cold_shot_ = false;
// showerColdShotStopTimer.detach(); // disable the timer
}
}
// turn off hot water to send a shot of cold
void Shower::shower_alert_start() {
if (shower_alert_) {
LOG_DEBUG(F("Shower Alert started!"));
// Boiler::set_tapwarmwater_activated(false);
doing_cold_shot_ = true;
// start the timer for n seconds which will reset the water back to hot
// showerColdShotStopTimer.attach(SHOWER_COLDSHOT_DURATION, _showerColdShotStop);
}
}
// Publish shower data
// returns true if added to MQTT queue went ok
void Shower::publish_values() {
StaticJsonDocument<90> doc;
doc["shower_timer"] = shower_timer_ ? "1" : "0";
doc["shower_alert"] = shower_alert_ ? "1" : "0";
// only publish shower duration if there is a value
char s[50];
if (duration_ > SHOWER_MIN_DURATION) {
char buffer[16] = {0};
strlcpy(s, Helpers::itoa(buffer, (uint8_t)((duration_ / (1000 * 60)) % 60), 10), 50);
strlcat(s, " minutes and ", 50);
strlcat(s, Helpers::itoa(buffer, (uint8_t)((duration_ / 1000) % 60), 10), 50);
strlcat(s, " seconds", 50);
doc["duration"] = s;
}
Mqtt::publish("shower_data", doc);
}
} // namespace emsesp

83
src/shower.h Normal file
View File

@@ -0,0 +1,83 @@
/*
* 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 EMSESP_SHOWER_H
#define EMSESP_SHOWER_H
#include <Arduino.h>
#include <ArduinoJson.h>
#include "helpers.h"
#include "console.h"
#include "mqtt.h"
#include "telegram.h"
#include "emsesp.h"
#include <uuid/log.h>
namespace emsesp {
class Shower {
public:
void start();
void loop();
bool shower_alert() const {
return shower_alert_;
}
void shower_alert(const bool shower_alert) {
shower_alert_ = shower_alert;
}
bool shower_timer() const {
return shower_timer_;
}
void shower_timer(const bool shower_timer) {
shower_timer_ = shower_timer;
}
private:
static uuid::log::Logger logger_;
static constexpr uint32_t SHOWER_PAUSE_TIME = 15000; // in ms. 15 seconds, max time if water is switched off & on during a shower
static constexpr uint32_t SHOWER_MIN_DURATION = 120000; // in ms. 2 minutes, before recognizing its a shower
static constexpr uint32_t SHOWER_OFFSET_TIME = 5000; // in ms. 5 seconds grace time, to calibrate actual time under the shower
static constexpr uint32_t SHOWER_COLDSHOT_DURATION = 10; // in seconds. 10 seconds for cold water before turning back hot water
static constexpr uint32_t SHOWER_MAX_DURATION = 420000; // in ms. 7 minutes, before trigger a shot of cold water
void console_commands();
void publish_values();
void shower_alert_start();
void shower_alert_stop();
bool shower_timer_; // true if we want to report back on shower times
bool shower_alert_; // true if we want the alert of cold water
bool shower_on_;
uint32_t timer_start_; // ms
uint32_t timer_pause_; // ms
uint32_t duration_; // ms
bool doing_cold_shot_; // true if we've just sent a jolt of cold water
};
} // namespace emsesp
#endif

583
src/system.cpp Normal file
View File

@@ -0,0 +1,583 @@
/*
* 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 "system.h"
#include "emsesp.h" // for send_raw_telegram() command
#include "version.h" // firmware version of EMS-ESP
MAKE_PSTR_WORD(passwd)
MAKE_PSTR_WORD(hostname)
MAKE_PSTR_WORD(wifi)
MAKE_PSTR_WORD(ssid)
MAKE_PSTR_WORD(heartbeat)
MAKE_PSTR(host_fmt, "Host = %s")
MAKE_PSTR(hostname_fmt, "Hostname = %s")
MAKE_PSTR(mark_interval_fmt, "Mark interval = %lus");
MAKE_PSTR(wifi_ssid_fmt, "WiFi SSID = %s");
MAKE_PSTR(wifi_password_fmt, "WiFi Password = %S")
MAKE_PSTR(system_heartbeat_fmt, "Heartbeat = %s")
MAKE_PSTR(logger_name, "system")
namespace emsesp {
uuid::log::Logger System::logger_{F_(logger_name), uuid::log::Facility::KERN};
#ifndef EMSESP_STANDALONE
uuid::syslog::SyslogService System::syslog_;
#endif
uint32_t System::heap_start_ = 0;
int System::reset_counter_;
// handle generic system related MQTT commands
void System::mqtt_commands(const char * message) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
LOG_DEBUG(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
}
// restart EMS-ESP
if (strcmp(message, "restart") == 0) {
LOG_INFO(F("Restart command received"));
restart();
}
if (doc["send"] != nullptr) {
const char * data = doc["send"];
EMSESP::send_raw_telegram(data);
LOG_INFO(F("Sending raw: %s"), data);
}
#if defined(ESP8266)
const uint8_t d0_ = 16;
const uint8_t d1_ = 5;
const uint8_t d2_ = 4;
const uint8_t d3_ = 0;
#elif defined(ESP32)
const uint8_t d0_ = 26;
const uint8_t d1_ = 22;
const uint8_t d2_ = 21;
const uint8_t d3_ = 17;
#endif
#ifndef EMSESP_STANDALONE
if (doc["D0"] != nullptr) {
const int8_t set = doc["D0"];
pinMode(d0_, OUTPUT);
if (set == 1) {
digitalWrite(d0_, HIGH);
} else if (set == 0) {
digitalWrite(d0_, LOW);
}
LOG_INFO(F("Port D0 set to %d"), set);
}
if (doc["D1"] != nullptr) {
const int8_t set = doc["D1"];
pinMode(d1_, OUTPUT);
if (set == 1) {
digitalWrite(d1_, HIGH);
} else if (set == 0) {
digitalWrite(d1_, LOW);
}
LOG_INFO(F("Port D1 set to %d"), set);
}
if (doc["D2"] != nullptr) {
const int8_t set = doc["D2"];
pinMode(d2_, OUTPUT);
if (set == 1) {
digitalWrite(d2_, HIGH);
} else if (set == 0) {
digitalWrite(d2_, LOW);
}
LOG_INFO(F("Port D2 set to %d"), set);
}
if (doc["D3"] != nullptr) {
const int8_t set = doc["D3"];
pinMode(d3_, OUTPUT);
if (set == 1) {
digitalWrite(d3_, HIGH);
} else if (set == 0) {
digitalWrite(d3_, LOW);
}
LOG_INFO(F("Port D3 set to %d"), set);
}
#endif
const char * command = doc["cmd"];
if (command == nullptr) {
return;
}
// send raw command
if (strcmp(command, "send") == 0) {
const char * data = doc["data"];
if (data == nullptr) {
return;
}
EMSESP::send_raw_telegram(data);
LOG_INFO(F("Sending raw: %s"), data);
return;
}
}
// restart EMS-ESP
void System::restart() {
LOG_NOTICE("Restarting system...");
Shell::loop_all();
delay(1000); // wait a second
#if defined(ESP8266)
ESP.reset();
#elif defined(ESP32)
ESP.restart();
#endif
}
// return free heap mem as a percentage
uint8_t System::free_mem() {
#ifndef EMSESP_STANDALONE
uint32_t free_memory = ESP.getFreeHeap();
#else
uint32_t free_memory = 1000;
#endif
return (100 * free_memory / heap_start_);
}
void System::syslog_init() {
// fetch settings
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
syslog_level_ = settings.syslog_level;
syslog_mark_interval_ = settings.syslog_mark_interval;
syslog_host_ = settings.syslog_host;
});
#ifndef EMSESP_STANDALONE
syslog_.start(); // syslog service
// 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);
#endif
}
void System::set_heartbeat(bool system_heartbeat) {
system_heartbeat_ = system_heartbeat;
}
// first call. Sets memory and starts up the UART Serial bridge
void System::start() {
// set the inital free mem
if (heap_start_ == 0) {
#ifndef EMSESP_STANDALONE
heap_start_ = ESP.getFreeHeap();
#else
heap_start_ = 2000;
#endif
}
// fetch settings
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
tx_mode_ = settings.tx_mode;
system_heartbeat_ = settings.system_heartbeat;
});
syslog_init();
#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
if (LED_GPIO) {
pinMode(LED_GPIO, OUTPUT); // LED pin, 0 means disabled
}
// register MQTT system commands
Mqtt::subscribe("cmd", std::bind(&System::mqtt_commands, this, _1));
#ifndef EMSESP_FORCE_SERIAL
EMSuart::start(tx_mode_); // start UART
#endif
}
// checks system health and handles LED flashing wizardry
void System::loop() {
#ifndef EMSESP_STANDALONE
syslog_.loop();
#endif
led_monitor(); // check status and report back using the LED
system_check(); // check system health
// send out heartbeat
uint32_t currentMillis = uuid::get_uptime();
if (!last_heartbeat_ || (currentMillis - last_heartbeat_ > SYSTEM_HEARTBEAT_INTERVAL)) {
last_heartbeat_ = currentMillis;
if (system_heartbeat_) {
send_heartbeat();
}
}
}
// send periodic MQTT message with system information
void System::send_heartbeat() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
int rssid = wifi_quality();
if (rssid != -1) {
doc["rssid"] = rssid;
}
doc["uptime"] = uuid::log::format_timestamp_ms(uuid::get_uptime_ms(), 3);
doc["uptime_sec"] = uuid::get_uptime_sec();
doc["freemem"] = free_mem();
doc["mqttpublishfails"] = Mqtt::publish_fails();
Mqtt::publish("heartbeat", doc, false); // send to MQTT with retain off
}
// sets rate of led flash
void System::set_led_speed(uint32_t speed) {
led_flash_speed_ = speed;
led_monitor();
}
// check health of system, done every few seconds
void System::system_check() {
static uint32_t last_system_check_ = 0;
if (!last_system_check_ || ((uint32_t)(uuid::get_uptime() - last_system_check_) >= SYSTEM_CHECK_FREQUENCY)) {
last_system_check_ = uuid::get_uptime();
#ifndef EMSESP_STANDALONE
if (WiFi.status() != WL_CONNECTED) {
set_led_speed(LED_WARNING_BLINK_FAST);
system_healthy_ = false;
return;
}
#endif
// not healthy if bus not connected
if (!EMSbus::bus_connected()) {
system_healthy_ = false;
set_led_speed(LED_WARNING_BLINK); // flash every 1/2 second from now on
// LOG_ERROR(F("Error: No connection to the EMS bus"));
} else {
// if it was unhealthy but now we're better, make sure the LED is solid again cos we've been healed
if (!system_healthy_) {
system_healthy_ = true;
if (LED_GPIO) {
digitalWrite(LED_GPIO, LED_ON); // LED on, for ever
}
}
}
}
}
// flashes the LED
void System::led_monitor() {
if (!LED_GPIO) {
return;
}
static uint32_t led_last_blink_ = 0;
if (!led_last_blink_ || (uint32_t)(uuid::get_uptime() - led_last_blink_) >= led_flash_speed_) {
led_last_blink_ = uuid::get_uptime();
// if bus_not_connected or network not connected, start flashing
if (!system_healthy_) {
digitalWrite(LED_GPIO, !digitalRead(LED_GPIO));
}
}
}
// Return the quality (Received Signal Strength Indicator) of the WiFi network as a %. Or -1 if disconnected.
// High quality: 90% ~= -55dBm
// Medium quality: 50% ~= -75dBm
// Low quality: 30% ~= -85dBm
// Unusable quality: 8% ~= -96dBm
int8_t System::wifi_quality() {
#ifndef EMSESP_STANDALONE
if (WiFi.status() != WL_CONNECTED) {
return -1;
}
int dBm = WiFi.RSSI();
#else
int8_t dBm = -70;
#endif
if (dBm <= -100) {
return 0;
}
if (dBm >= -50) {
return 100;
}
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));
shell.println();
#if defined(ESP8266)
shell.printfln(F("Chip ID: 0x%08x"), ESP.getChipId());
shell.printfln(F("SDK version: %s"), ESP.getSdkVersion());
shell.printfln(F("Core version: %s"), ESP.getCoreVersion().c_str());
shell.printfln(F("Full version: %s"), ESP.getFullVersion().c_str());
shell.printfln(F("Boot version: %u"), ESP.getBootVersion());
shell.printfln(F("Boot mode: %u"), ESP.getBootMode());
shell.printfln(F("CPU frequency: %u MHz"), ESP.getCpuFreqMHz());
shell.printfln(F("Flash chip: 0x%08X (%u bytes)"), ESP.getFlashChipId(), ESP.getFlashChipRealSize());
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.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());
shell.printfln(F("Heap fragmentation: %u%"), ESP.getHeapFragmentation());
shell.printfln(F("Free continuations stack: %lu bytes"), (unsigned long)ESP.getFreeContStack());
#elif defined(ESP32)
shell.printfln(F("SDK version: %s"), ESP.getSdkVersion());
shell.printfln(F("CPU frequency: %u MHz"), ESP.getCpuFreqMHz());
shell.printfln(F("Sketch size: %u bytes (%u bytes free)"), ESP.getSketchSize(), ESP.getFreeSketchSpace());
shell.printfln(F("Free heap: %lu bytes"), (unsigned long)ESP.getFreeHeap());
shell.printfln(F("Free mem: %d %%"), free_mem());
#endif
if (shell.has_flags(CommandFlags::ADMIN)) {
shell.println();
#ifndef EMSESP_STANDALONE
switch (WiFi.status()) {
case WL_IDLE_STATUS:
shell.printfln(F("WiFi: idle"));
break;
case WL_NO_SSID_AVAIL:
shell.printfln(F("WiFi: network not found"));
break;
case WL_SCAN_COMPLETED:
shell.printfln(F("WiFi: network scan complete"));
break;
case WL_CONNECTED: {
shell.printfln(F("WiFi: connected"));
shell.println();
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());
shell.println();
shell.printfln(F("MAC address: %s"), WiFi.macAddress().c_str());
#if defined(ESP8266)
shell.printfln(F("Hostname: %s"), WiFi.hostname().c_str());
#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"));
break;
case WL_CONNECTION_LOST:
shell.printfln(F("WiFi: connection lost"));
break;
case WL_DISCONNECTED:
shell.printfln(F("WiFi: disconnected"));
break;
case WL_NO_SHIELD:
default:
shell.printfln(F("WiFi: unknown"));
break;
}
}
#endif
}
// console commands to add
void System::console_commands(Shell & shell, unsigned int context) {
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(restart)},
[](Shell & shell __attribute__((unused)), const std::vector<std::string> & arguments __attribute__((unused))) {
restart();
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(passwd)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) {
if (completed) {
shell.enter_password(F_(new_password_prompt2),
[password1](Shell & shell, bool completed, const std::string & password2) {
if (completed) {
if (password1 == password2) {
EMSESP::esp8266React.getSecuritySettingsService()->update(
[&](SecuritySettings & securitySettings) {
securitySettings.jwtSecret = password2.c_str();
return StateUpdateResult::CHANGED;
},
"local");
shell.println(F("Admin password updated"));
} else {
shell.println(F("Passwords do not match"));
}
}
});
}
});
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::USER,
flash_string_vector{F_(show)},
[=](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
show_system(shell); // has to be static
shell.println();
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
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) {
EMSESP::esp8266React.getWiFiSettingsService()->update(
[&](WiFiSettings & wifiSettings) {
wifiSettings.hostname = arguments.front().c_str();
return StateUpdateResult::CHANGED;
},
"local");
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(wifi), F_(ssid)},
flash_string_vector{F_(name_mandatory)},
[](Shell & shell, const std::vector<std::string> & arguments) {
EMSESP::esp8266React.getWiFiSettingsService()->update(
[&](WiFiSettings & wifiSettings) {
wifiSettings.ssid = arguments.front().c_str();
return StateUpdateResult::CHANGED;
},
"local");
shell.println(F("WiFi SSID updated"));
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::ADMIN,
flash_string_vector{F_(set), F_(wifi), F_(password)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
shell.enter_password(F_(new_password_prompt1), [](Shell & shell, bool completed, const std::string & password1) {
if (completed) {
shell.enter_password(F_(new_password_prompt2),
[password1](Shell & shell, bool completed, const std::string & password2) {
if (completed) {
if (password1 == password2) {
EMSESP::esp8266React.getWiFiSettingsService()->update(
[&](WiFiSettings & wifiSettings) {
wifiSettings.password = password2.c_str();
return StateUpdateResult::CHANGED;
},
"local");
shell.println(F("WiFi password updated"));
} else {
shell.println(F("Passwords do not match"));
}
}
});
}
});
});
EMSESPShell::commands->add_command(
ShellContext::SYSTEM,
CommandFlags::USER,
flash_string_vector{F_(set)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) {
shell.printfln(F_(hostname_fmt), wifiSettings.hostname.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : wifiSettings.hostname.c_str());
});
if (shell.has_flags(CommandFlags::ADMIN)) {
shell.printfln("Wifi:");
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) {
shell.print(" ");
shell.printfln(F_(wifi_ssid_fmt), wifiSettings.ssid.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str() : wifiSettings.ssid.c_str());
shell.print(" ");
shell.printfln(F_(wifi_password_fmt), wifiSettings.ssid.isEmpty() ? F_(unset) : F_(asterisks));
});
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) {
shell.printfln(F("Syslog:"));
shell.print(" ");
shell.printfln(F_(host_fmt), !settings.syslog_host.isEmpty() ? settings.syslog_host.c_str() : uuid::read_flash_string(F_(unset)).c_str());
shell.print(" ");
shell.printfln(F_(log_level_fmt), uuid::log::format_level_uppercase(static_cast<uuid::log::Level>(settings.syslog_level)));
shell.print(" ");
shell.printfln(F_(mark_interval_fmt), settings.syslog_mark_interval);
shell.println();
shell.printfln(F_(system_heartbeat_fmt), settings.system_heartbeat ? F_(enabled) : F_(disabled));
});
}
});
EMSESPShell::commands->add_command(ShellContext::SYSTEM,
CommandFlags::USER,
flash_string_vector{F_(show), F_(mqtt)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) { Mqtt::show_mqtt(shell); });
// enter the context
Console::enter_custom_context(shell, context);
}
} // namespace emsesp

107
src/system.h Normal file
View File

@@ -0,0 +1,107 @@
/*
* 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 EMSESP_SYSTEM_H_
#define EMSESP_SYSTEM_H_
#include <Arduino.h>
#include <ArduinoJson.h>
#include "helpers.h"
#include "console.h"
#include "mqtt.h"
#include "telegram.h"
#ifndef EMSESP_STANDALONE
#include <uuid/syslog.h>
#endif
#include <uuid/log.h>
using uuid::console::Shell;
namespace emsesp {
class System {
public:
void start();
void loop();
static void restart();
static uint8_t free_mem();
void syslog_init();
void set_heartbeat(bool system_heartbeat);
static void console_commands(Shell & shell, unsigned int context);
private:
static uuid::log::Logger logger_;
#ifndef EMSESP_STANDALONE
static uuid::syslog::SyslogService syslog_;
#endif
static constexpr uint32_t SYSTEM_CHECK_FREQUENCY = 10000; // check every 10 seconds
static constexpr uint32_t LED_WARNING_BLINK = 1000; // pulse to show no connection, 1 sec
static constexpr uint32_t LED_WARNING_BLINK_FAST = 100; // flash quickly for boot up sequence
static constexpr uint32_t SYSTEM_HEARTBEAT_INTERVAL = 60000; // in milliseconds, how often the MQTT heartbeat is sent (1 min)
// internal LED
#ifndef EMSESP_NO_LED
#if defined(ESP8266)
static constexpr uint8_t LED_GPIO = 2;
static constexpr uint8_t LED_ON = LOW;
#elif defined(ESP32)
#ifdef WEMOS_D1_32
static constexpr uint8_t LED_GPIO = 2; // on Wemos D1-32
static constexpr uint8_t LED_ON = HIGH;
#else
static constexpr uint8_t LED_GPIO = 5;
static constexpr uint8_t LED_ON = LOW;
#endif
#endif
#else
static constexpr uint8_t LED_GPIO = 0; // no LED
static constexpr uint8_t LED_ON = 0;
#endif
void led_monitor();
void set_led_speed(uint32_t speed);
void mqtt_commands(const char * message);
void send_heartbeat();
void system_check();
static void show_system(uuid::console::Shell & shell);
static int8_t wifi_quality();
bool system_healthy_ = false;
uint32_t led_flash_speed_ = LED_WARNING_BLINK_FAST; // default boot flashes quickly
static uint32_t heap_start_;
static int reset_counter_;
uint32_t last_heartbeat_ = 0;
// settings
uint8_t tx_mode_;
bool system_heartbeat_;
uint8_t syslog_level_;
uint32_t syslog_mark_interval_;
String syslog_host_;
};
} // namespace emsesp
#endif

614
src/telegram.cpp Normal file
View File

@@ -0,0 +1,614 @@
/*
* 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 "telegram.h"
#include "emsesp.h"
MAKE_PSTR(logger_name, "telegram")
namespace emsesp {
// CRC lookup table with poly 12 for faster checking
const uint8_t ems_crc_table[] = {0x00, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, 0x0E, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x24, 0x26,
0x28, 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x38, 0x3A, 0x3C, 0x3E, 0x40, 0x42, 0x44, 0x46, 0x48, 0x4A, 0x4C, 0x4E,
0x50, 0x52, 0x54, 0x56, 0x58, 0x5A, 0x5C, 0x5E, 0x60, 0x62, 0x64, 0x66, 0x68, 0x6A, 0x6C, 0x6E, 0x70, 0x72, 0x74, 0x76,
0x78, 0x7A, 0x7C, 0x7E, 0x80, 0x82, 0x84, 0x86, 0x88, 0x8A, 0x8C, 0x8E, 0x90, 0x92, 0x94, 0x96, 0x98, 0x9A, 0x9C, 0x9E,
0xA0, 0xA2, 0xA4, 0xA6, 0xA8, 0xAA, 0xAC, 0xAE, 0xB0, 0xB2, 0xB4, 0xB6, 0xB8, 0xBA, 0xBC, 0xBE, 0xC0, 0xC2, 0xC4, 0xC6,
0xC8, 0xCA, 0xCC, 0xCE, 0xD0, 0xD2, 0xD4, 0xD6, 0xD8, 0xDA, 0xDC, 0xDE, 0xE0, 0xE2, 0xE4, 0xE6, 0xE8, 0xEA, 0xEC, 0xEE,
0xF0, 0xF2, 0xF4, 0xF6, 0xF8, 0xFA, 0xFC, 0xFE, 0x19, 0x1B, 0x1D, 0x1F, 0x11, 0x13, 0x15, 0x17, 0x09, 0x0B, 0x0D, 0x0F,
0x01, 0x03, 0x05, 0x07, 0x39, 0x3B, 0x3D, 0x3F, 0x31, 0x33, 0x35, 0x37, 0x29, 0x2B, 0x2D, 0x2F, 0x21, 0x23, 0x25, 0x27,
0x59, 0x5B, 0x5D, 0x5F, 0x51, 0x53, 0x55, 0x57, 0x49, 0x4B, 0x4D, 0x4F, 0x41, 0x43, 0x45, 0x47, 0x79, 0x7B, 0x7D, 0x7F,
0x71, 0x73, 0x75, 0x77, 0x69, 0x6B, 0x6D, 0x6F, 0x61, 0x63, 0x65, 0x67, 0x99, 0x9B, 0x9D, 0x9F, 0x91, 0x93, 0x95, 0x97,
0x89, 0x8B, 0x8D, 0x8F, 0x81, 0x83, 0x85, 0x87, 0xB9, 0xBB, 0xBD, 0xBF, 0xB1, 0xB3, 0xB5, 0xB7, 0xA9, 0xAB, 0xAD, 0xAF,
0xA1, 0xA3, 0xA5, 0xA7, 0xD9, 0xDB, 0xDD, 0xDF, 0xD1, 0xD3, 0xD5, 0xD7, 0xC9, 0xCB, 0xCD, 0xCF, 0xC1, 0xC3, 0xC5, 0xC7,
0xF9, 0xFB, 0xFD, 0xFF, 0xF1, 0xF3, 0xF5, 0xF7, 0xE9, 0xEB, 0xED, 0xEF, 0xE1, 0xE3, 0xE5, 0xE7};
uint32_t EMSbus::last_bus_activity_ = 0; // timestamp of last time a valid Rx came in
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;
uuid::log::Logger EMSbus::logger_{F_(logger_name), uuid::log::Facility::CONSOLE};
// Calculates CRC checksum using lookup table for speed
// length excludes the last byte (which mainly is the CRC)
uint8_t EMSbus::calculate_crc(const uint8_t * data, const uint8_t length) {
uint8_t i = 0;
uint8_t crc = 0;
while (i < length) {
crc = ems_crc_table[crc];
crc ^= data[i++];
}
return crc;
}
// creates a telegram object
// stores header in separate member objects and the rest in the message_data block
Telegram::Telegram(const uint8_t operation,
const uint8_t src,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
const uint8_t * data,
const uint8_t message_length)
: operation(operation)
, src(src)
, dest(dest)
, type_id(type_id)
, offset(offset)
, message_length(message_length) {
// copy complete telegram data over, preventing buffer overflow
for (uint8_t i = 0; ((i < message_length) && (i != EMS_MAX_TELEGRAM_MESSAGE_LENGTH - 1)); i++) {
message_data[i] = data[i];
}
}
// returns telegram's message data bytes in hex
std::string Telegram::to_string() const {
if (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();
if (this->operation == Telegram::Operation::TX_READ) {
data[1] = this->dest | 0x80;
data[4] = this->message_data[0];
if (this->type_id > 0xFF) {
data[2] = 0xFF;
data[5] = (this->type_id >> 8) - 1;
data[6] = this->type_id & 0xFF;
length = 7;
} else {
data[2] = this->type_id;
length = 5;
}
}
if (this->operation == Telegram::Operation::TX_WRITE) {
data[1] = this->dest;
if (this->type_id > 0xFF) {
data[2] = 0xFF;
data[4] = (this->type_id >> 8) - 1;
data[5] = this->type_id & 0xFF;
length = 6;
} else {
data[2] = this->type_id;
length = 4;
}
for (uint8_t i = 0; i < this->message_length; i++) {
data[length++] = this->message_data[i];
}
}
return Helpers::data_to_hex(data, length);
}
// returns telegram's full telegram message in hex
std::string Telegram::to_string(const uint8_t * telegram, uint8_t length) const {
return Helpers::data_to_hex(telegram, length);
}
RxService::QueuedRxTelegram::QueuedRxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram)
: id_(id)
, telegram_(std::move(telegram)) {
}
// empty queue, don't process them
void RxService::flush_rx_queue() {
rx_telegrams_.clear();
rx_telegram_id_ = 0;
}
// Rx loop, run as many times as you can
// processes all telegrams on the queue. Assumes there are valid (i.e. CRC checked)
void RxService::loop() {
while (!rx_telegrams_.empty()) {
auto telegram = rx_telegrams_.front().telegram_;
(void)EMSESP::process_telegram(telegram); // further process the telegram
increment_telegram_count(); // increase count
rx_telegrams_.pop_front(); // remove it from the queue
}
}
// add a new rx telegram object
// data is the whole telegram, assuming last byte holds the CRC
// length includes the CRC
// for EMS+ the type_id has the value + 256. We look for these type of telegrams with F7, F9 and FF in 3rd byte
void RxService::add(uint8_t * data, uint8_t length) {
if (length < 2) {
return;
}
// 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);
increment_telegram_error_count();
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
if (ems_mask() == EMS_MASK_UNSET) {
ems_mask(data[0]);
}
// src, dest and offset are always in fixed positions
uint8_t src = data[0] & 0x7F; // strip MSB (HT3 adds it)
uint8_t dest = data[1] & 0x7F; // strip MSB, don't care if its read or write for processing
uint8_t offset = data[3]; // offset is always 4th byte
uint16_t type_id;
uint8_t * message_data; // where the message block starts
uint8_t message_length; // length of the message block, excluding CRC
// work out depending on the type, where the data message block starts and the message length
// EMS 1 has type_id always in data[2], if it gets a ems+ inquiry it will reply with FF but short length
// i.e. sending 0B A1 FF 00 01 D8 20 CRC to a MM10 Mixer (ems1.0), the reply is 21 0B FF 00 CRC
// see: https://github.com/proddy/EMS-ESP/issues/380#issuecomment-633663007
if (data[2] < 0xF0 || length < 6) {
// EMS 1.0
type_id = data[2];
message_data = data + 4;
message_length = length - 5;
} else {
// EMS 2.0 / EMS+
uint8_t shift = 0; // default when data[2] is 0xFF
if (data[2] != 0xFF) {
// its F9 or F7, re-calculate shift. If the 5th byte is not 0xFF then telegram is 1 byte longer
shift = (data[4] != 0xFF) ? 2 : 1;
}
type_id = (data[4 + shift] << 8) + data[5 + shift] + 256;
message_data = data + 6 + shift;
message_length = length - 6 - shift;
}
// if we don't have a type_id or empty data block, exit
if ((type_id == 0) || (message_length == 0)) {
return;
}
// if we're watching and "raw" print out actual telegram as bytes to the console
if (EMSESP::watch() == EMSESP::Watch::WATCH_RAW) {
uint16_t trace_watch_id = EMSESP::watch_id();
if ((trace_watch_id == WATCH_ID_NONE) || (src == trace_watch_id) || (dest == trace_watch_id) || (type_id == trace_watch_id)) {
LOG_NOTICE(F("Rx: %s"), Helpers::data_to_hex(data, length).c_str());
}
}
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] New Rx [#%d] telegram, message length %d"), rx_telegram_id_, message_length);
#endif
// create the telegram
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, src, dest, type_id, offset, message_data, message_length);
// check if queue is full, if so remove top item to make space
if (rx_telegrams_.size() >= MAX_RX_TELEGRAMS) {
rx_telegrams_.pop_front();
}
rx_telegrams_.emplace_back(rx_telegram_id_++, std::move(telegram)); // add to queue
}
//
// Tx CODE starts here...
//
TxService::QueuedTxTelegram::QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram, bool retry)
: id_(id)
, telegram_(std::move(telegram))
, retry_(retry) {
}
// empty queue, don't process
void TxService::flush_tx_queue() {
tx_telegrams_.clear();
tx_telegram_id_ = 0;
}
// start and initialize Tx
void TxService::start() {
// grab the bus ID
EMSESP::emsespSettingsService.read([&](EMSESPSettings & settings) { ems_bus_id(settings.ems_bus_id); });
// 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());
}
// 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
if (tx_telegrams_.empty()) {
send_poll();
return;
}
// send next telegram in the queue (which is actually a list!)
send_telegram(tx_telegrams_.front());
// remove the telegram from the queue
tx_telegrams_.pop_front();
}
// process a Tx telegram
void TxService::send_telegram(const QueuedTxTelegram & tx_telegram) {
static uint8_t telegram_raw[EMS_MAX_TELEGRAM_LENGTH];
// build the header
auto telegram = tx_telegram.telegram_;
// src - set MSB if it's Junkers/HT3
uint8_t src = telegram->src;
if (ems_mask() != EMS_MASK_UNSET) {
src ^= ems_mask();
}
telegram_raw[0] = src;
// dest - for READ the MSB must be set
// fix the READ or WRITE depending on the operation
uint8_t dest = telegram->dest;
if (telegram->operation == Telegram::Operation::TX_READ) {
dest |= 0x80; // read has 8th bit set for the destination
}
telegram_raw[1] = dest;
uint8_t message_p = 0; // this is the position in the telegram where we want to put our message data
bool copy_data = true; // true if we want to copy over the data message block to the end of the telegram header
if (telegram->type_id > 0xFF) {
// it's EMS 2.0/+
telegram_raw[2] = 0xFF; // fixed value indicating an extended message
telegram_raw[3] = telegram->offset;
// EMS+ has different format for read and write. See https://github.com/proddy/EMS-ESP/wiki/RC3xx-Thermostats
if (telegram->operation == Telegram::Operation::TX_WRITE) {
// WRITE
telegram_raw[4] = (telegram->type_id >> 8) - 1; // type, 1st byte, high-byte, subtract 0x100
telegram_raw[5] = telegram->type_id & 0xFF; // type, 2nd byte, low-byte
message_p = 6;
} else {
// READ
telegram_raw[4] = telegram->message_data[0]; // #bytes to return, which we assume is the only byte in the message block
telegram_raw[5] = (telegram->type_id >> 8) - 1; // type, 1st byte, high-byte, subtract 0x100
telegram_raw[6] = telegram->type_id & 0xFF; // type, 2nd byte, low-byte
message_p = 7;
copy_data = false; // there are no more data values after the type_id when reading on EMS+
}
} else {
// EMS 1.0
telegram_raw[2] = telegram->type_id;
telegram_raw[3] = telegram->offset;
message_p = 4;
}
if (copy_data) {
if (telegram->message_length > EMS_MAX_TELEGRAM_MESSAGE_LENGTH) {
return; // too big
}
// add the data to send to to the end of the header
for (uint8_t i = 0; i < telegram->message_length; i++) {
telegram_raw[message_p++] = telegram->message_data[i];
}
}
uint8_t length = message_p;
telegram_last_ = std::make_shared<Telegram>(*telegram); // make a copy of the telegram
telegram_raw[length] = calculate_crc(telegram_raw, length); // generate and append CRC to the end
length++; // add one since we want to now include the CRC
LOG_DEBUG(F("Sending %s Tx [#%d], telegram: %s"),
(telegram->operation == Telegram::Operation::TX_WRITE) ? F("write") : F("read"),
tx_telegram.id_,
telegram->to_string(telegram_raw, length).c_str());
#ifdef EMSESP_DEBUG
// if watching in 'raw' mode
if (EMSESP::watch() == EMSESP::Watch::WATCH_RAW) {
LOG_NOTICE(F("[DEBUG] Tx: %s"), Helpers::data_to_hex(telegram_raw, length).c_str());
}
#endif
// send the telegram to the UART Tx
uint16_t status = EMSuart::transmit(telegram_raw, length);
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
return;
}
tx_waiting(telegram->operation); // tx now in a wait state
}
// send an array of bytes as a telegram
// we need to calculate the CRC and append it before sending
// this function is fire-and-forget. there are no checks or post-send validations
void TxService::send_telegram(const uint8_t * data, const uint8_t length) {
uint8_t telegram_raw[EMS_MAX_TELEGRAM_LENGTH];
for (uint8_t i = 0; i < length; i++) {
telegram_raw[i] = data[i];
}
telegram_raw[length] = calculate_crc(telegram_raw, length); // apppend CRC
tx_waiting(Telegram::Operation::NONE); // no post validation needed
// send the telegram to the UART Tx
uint16_t status = EMSuart::transmit(telegram_raw, length);
if (status == EMS_TX_STATUS_ERR) {
LOG_ERROR(F("Failed to transmit Tx via UART."));
increment_telegram_fail_count(); // another Tx fail
}
}
// builds a Tx telegram and adds to queue
// given some details like the destination, type, offset and message block
void TxService::add(const uint8_t operation,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const bool front) {
auto telegram = std::make_shared<Telegram>(operation, ems_bus_id(), dest, type_id, offset, message_data, message_length);
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
#endif
// if the queue is full, make room but removing the last one
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front();
}
if (front) {
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram), false); // add to back of queue
} else {
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false); // add to back of queue
}
}
// builds a Tx telegram and adds to queue
// this is used by the retry() function to put the last failed Tx back into the queue
// format is EMS 1.0 (src, dest, type_id, offset, data)
// length is the length of the whole telegram data, excluding the CRC
void TxService::add(uint8_t operation, const uint8_t * data, const uint8_t length, const bool front) {
// build header. src, dest and offset have fixed positions
uint8_t src = data[0];
uint8_t dest = data[1];
uint8_t offset = data[3];
uint16_t type_id;
const uint8_t * message_data; // where the message block starts
uint8_t message_length; // length of the message block, excluding CRC
// work out depending on the type, where the data message block starts and the message length
// same logic as in RxService::add(), but adjusted for no appended CRC
if (data[2] < 0xF0) {
// EMS 1.0
type_id = data[2];
message_data = data + 4;
message_length = length - 4;
} else {
// EMS 2.0 / EMS+
uint8_t shift = 0; // default when data[2] is 0xFF
if (data[2] != 0xFF) {
// its F9 or F7, re-calculate shift. If the 5th byte is not 0xFF then telegram is 1 byte longer
shift = (data[4] != 0xFF) ? 2 : 1;
}
type_id = (data[4 + shift] << 8) + data[5 + shift] + 256;
message_data = data + 6 + shift;
message_length = length - 6 - shift;
}
// if we don't have a type_id or empty data block, exit
if ((type_id == 0) || (message_length == 0)) {
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] Tx telegram type %d failed, length %d"), type_id, message_length);
#endif
return;
}
if (operation == Telegram::Operation::TX_RAW) {
if (dest & 0x80) {
operation = Telegram::Operation::TX_READ;
} else {
operation = Telegram::Operation::TX_WRITE;
}
}
auto telegram = std::make_shared<Telegram>(operation, src, dest, type_id, offset, message_data, message_length); // operation is TX_WRITE or TX_READ
// if the queue is full, make room but removing the last one
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_front();
}
#ifdef EMSESP_DEBUG
LOG_DEBUG(F("[DEBUG] New Tx [#%d] telegram, length %d"), tx_telegram_id_, message_length);
#endif
if (front) {
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram), false); // add to back of queue
} else {
tx_telegrams_.emplace_back(tx_telegram_id_++, std::move(telegram), false); // add to back of queue
}
}
// send a Tx telegram to request data from an EMS device
void TxService::read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset) {
LOG_DEBUG(F("Tx read request to device 0x%02X for type ID 0x%02X"), dest, type_id);
uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes
add(Telegram::Operation::TX_READ, dest, type_id, offset, message_data, 1);
}
// Send a raw telegram to the bus, telegram is a text string of hex values
void TxService::send_raw(const char * telegram_data) {
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
char telegram[EMS_MAX_TELEGRAM_LENGTH * 3];
for (uint8_t i = 0; i < strlen(telegram_data); i++) {
telegram[i] = telegram_data[i];
}
telegram[strlen(telegram_data)] = '\0'; // make sure its terminated
uint8_t count = 0;
char * p;
char value[10] = {0};
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
// get first value, which should be the src
if ((p = strtok(telegram, " ,"))) { // delimiter
strlcpy(value, p, 10);
data[0] = (uint8_t)strtol(value, 0, 16);
}
// and iterate until end
while (p != 0) {
if ((p = strtok(nullptr, " ,"))) {
strlcpy(value, p, 10);
uint8_t val = (uint8_t)strtol(value, 0, 16);
data[++count] = val;
}
}
if (count == 0) {
return; // nothing to send
}
add(Telegram::Operation::TX_RAW, data, count + 1); // add to Tx queue
}
// add last Tx to tx queue and increment count
// returns retry count, or 0 if all done
void TxService::retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length) {
// have we reached the limit? if so, reset count and give up
if (++retry_count_ > MAXIMUM_TX_RETRIES) {
reset_retry_count(); // give up
increment_telegram_fail_count(); // another Tx fail
LOG_ERROR(F("Last Tx %s operation failed after %d retries. Ignoring request."),
(operation == Telegram::Operation::TX_WRITE) ? F("Write") : F("Read"),
MAXIMUM_TX_RETRIES);
return;
}
LOG_DEBUG(F("[DEBUG] Last Tx %s operation failed. Retry #%d. sent message: %s, received: %s"),
(operation == Telegram::Operation::TX_WRITE) ? F("Write") : F("Read"),
retry_count_,
telegram_last_->to_string().c_str(),
Helpers::data_to_hex(data, length).c_str());
// add to the top of the queue
if (tx_telegrams_.size() >= MAX_TX_TELEGRAMS) {
tx_telegrams_.pop_back();
}
tx_telegrams_.emplace_front(tx_telegram_id_++, std::move(telegram_last_), true);
}
// checks if a telegram is sent to us matches the last Tx request
// incoming Rx src must match the last Tx dest
// and incoming Rx dest must be us (our ems_bus_id)
// for both src and dest we strip the MSB 8th bit
// returns true if the src/dest match the last Tx sent
bool TxService::is_last_tx(const uint8_t src, const uint8_t dest) const {
return (((telegram_last_->dest & 0x7F) == (src & 0x7F)) && ((dest & 0x7F) == ems_bus_id()));
}
// sends a type_id read request to fetch values after a successful Tx write operation
void TxService::post_send_query() {
if (telegram_last_post_send_query_) {
uint8_t dest = (telegram_last_->dest & 0x7F);
uint8_t message_data[1] = {EMS_MAX_TELEGRAM_LENGTH}; // request all data, 32 bytes
add(Telegram::Operation::TX_READ, dest, telegram_last_post_send_query_, 0, message_data, 1, true);
// read_request(telegram_last_post_send_query_, dest, 0); // no offset
LOG_DEBUG(F("Sending post validate read, type ID 0x%02X to dest 0x%02X"), telegram_last_post_send_query_, dest);
}
}
// print out the last Tx that was sent
void TxService::print_last_tx() {
LOG_DEBUG(F("Last Tx %s operation: %s"),
(telegram_last_->operation == Telegram::Operation::TX_WRITE) ? F("Write") : F("Read"),
telegram_last_->to_string().c_str());
}
} // namespace emsesp

384
src/telegram.h Normal file
View File

@@ -0,0 +1,384 @@
/*
* 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 EMSESP_TELEGRAM_H
#define EMSESP_TELEGRAM_H
#include <string>
#include <deque>
#include <memory> // for unique ptrs
#include <vector>
// UART drivers
#if defined(ESP8266)
#include "uart/emsuart_esp8266.h"
#elif defined(ESP32)
#include "uart/emsuart_esp32.h"
#elif defined(EMSESP_STANDALONE)
#include <emsuart_standalone.h>
#endif
#include <uuid/log.h>
#include "helpers.h"
// default values for null values
static constexpr uint8_t EMS_VALUE_BOOL = 0xFF; // boolean
static constexpr uint8_t EMS_VALUE_BOOL_OFF = 0x00; // boolean false. True can be 0x01 or 0xFF sometimes.
static constexpr uint8_t EMS_VALUE_BOOL_NOTSET = 0xFE; // random number for booleans, that's not 0, 1 or FF
static constexpr uint8_t EMS_VALUE_UINT_NOTSET = 0xFF; // for 8-bit unsigned ints/bytes
static constexpr int8_t EMS_VALUE_INT_NOTSET = 0x7F; // for signed 8-bit ints/bytes
static constexpr uint16_t EMS_VALUE_USHORT_NOTSET = 0x7D00; // 32000: for 2-byte unsigned shorts
static constexpr int16_t EMS_VALUE_SHORT_NOTSET = 0x7D00; // 32000: for 2-byte signed shorts
static constexpr uint32_t EMS_VALUE_ULONG_NOTSET = 0xFFFFFFFF; // for 3-byte and 4-byte longs
static constexpr uint8_t EMS_MAX_TELEGRAM_LENGTH = 32; // max length of a complete EMS telegram
static constexpr uint8_t EMS_MAX_TELEGRAM_MESSAGE_LENGTH = 27; // max length of message block, assuming EMS1.0
namespace emsesp {
// creates a telegram
// from Rx (receiving one) or Tx for preparing one for sending
class Telegram {
public:
Telegram(const uint8_t operation,
const uint8_t src,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
const uint8_t * message_data,
const uint8_t message_length);
~Telegram() = default;
const uint8_t operation; // is Operation mode
const uint8_t src; // device_id
const uint8_t dest; // device_id
const uint16_t type_id;
const uint8_t offset;
const uint8_t message_length;
uint8_t message_data[EMS_MAX_TELEGRAM_MESSAGE_LENGTH];
enum Operation : uint8_t {
NONE = 0,
RX,
TX_RAW,
TX_READ,
TX_WRITE,
};
std::string to_string() const;
std::string to_string(const uint8_t * telegram, uint8_t length) const;
// reads a bit value from a given telegram position
void read_bitvalue(uint8_t & value, const uint8_t index, const uint8_t bit) const {
uint8_t abs_index = (index - offset);
if (abs_index >= message_length - 1) {
return; // out of bounds
}
value = (uint8_t)(((message_data[abs_index]) >> (bit)) & 0x01);
}
// read values from a telegram. We always store the value, regardless if its garbage
template <typename Value>
// assuming negative numbers are stored as 2's-complement
// https://medium.com/@LeeJulija/how-integers-are-stored-in-memory-using-twos-complement-5ba04d61a56c
// 2-compliment : https://www.rapidtables.com/convert/number/decimal-to-hex.html
// https://en.wikipedia.org/wiki/Two%27s_complement
// s is to override number of bytes read (e.g. use 3 to simulat a uint24_t)
void read_value(Value & value, const uint8_t index, uint8_t s = 0) const {
uint8_t size = (!s) ? sizeof(Value) : s;
int8_t abs_index = ((index - offset + size - 1) >= message_length - 1) ? -1 : (index - offset);
if (abs_index < 0) {
return; // out of bounds, we don't change the value
}
value = 0;
for (uint8_t i = 0; i < size; i++) {
// shift
value = (value << 8) + message_data[abs_index + i];
}
}
private:
int8_t _getDataPosition(const uint8_t index, const uint8_t size) const;
};
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 bool bus_connected() {
#ifndef EMSESP_STANDALONE
if ((uuid::get_uptime() - last_bus_activity_) > EMS_BUS_TIMEOUT) {
bus_connected_ = false;
}
return bus_connected_;
#else
return true;
#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 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 uint8_t calculate_crc(const uint8_t * data, const uint8_t length);
private:
static constexpr uint32_t EMS_BUS_TIMEOUT = 30000; // timeout in ms before recognizing the ems bus is offline (30 seconds)
static uint32_t last_bus_activity_; // timestamp of last time a valid Rx came in
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
};
class RxService : public EMSbus {
public:
static constexpr size_t MAX_RX_TELEGRAMS = 20;
RxService() = default;
~RxService() = default;
void loop();
void add(uint8_t * data, uint8_t length);
void flush_rx_queue();
uint16_t telegram_count() const {
return telegram_count_;
}
uint16_t telegram_error_count() const {
return telegram_error_count_;
}
void increment_telegram_count() {
telegram_count_++;
}
void increment_telegram_error_count() {
telegram_error_count_++;
}
class QueuedRxTelegram {
public:
QueuedRxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram);
~QueuedRxTelegram() = default;
uint16_t id_; // sequential identifier
const std::shared_ptr<const Telegram> telegram_;
};
const std::deque<QueuedRxTelegram> queue() const {
return rx_telegrams_;
}
private:
static constexpr uint32_t RX_LOOP_WAIT = 800; // delay in processing Rx queue
uint32_t last_rx_check_ = 0;
uint8_t rx_telegram_id_ = 0; // queue counter
uint16_t telegram_count_ = 0; // # Rx received
uint16_t telegram_error_count_ = 0; // # Rx CRC errors
std::deque<QueuedRxTelegram> rx_telegrams_;
};
class TxService : public EMSbus {
public:
static constexpr size_t MAX_TX_TELEGRAMS = 40; // size of Tx queue
static constexpr uint8_t TX_WRITE_FAIL = 4;
static constexpr uint8_t TX_WRITE_SUCCESS = 1;
TxService() = default;
~TxService() = default;
void start();
void loop();
void send();
void add(const uint8_t operation,
const uint8_t dest,
const uint16_t type_id,
const uint8_t offset,
uint8_t * message_data,
const uint8_t message_length,
const bool front = false);
void add(const uint8_t operation, const uint8_t * data, const uint8_t length, const bool front = false);
void read_request(const uint16_t type_id, const uint8_t dest, const uint8_t offset = 0);
void send_raw(const char * telegram_data);
void send_poll();
void flush_tx_queue();
void retry_tx(const uint8_t operation, const uint8_t * data, const uint8_t length);
uint8_t retry_count() const {
return retry_count_;
}
void reset_retry_count() {
retry_count_ = 0;
}
bool is_last_tx(const uint8_t src, const uint8_t dest) const;
void set_post_send_query(uint16_t type_id) {
telegram_last_post_send_query_ = type_id;
}
uint16_t telegram_read_count() const {
return telegram_read_count_;
}
void telegram_read_count(uint8_t telegram_read_count) {
telegram_read_count_ = telegram_read_count;
}
void increment_telegram_read_count() {
telegram_read_count_++;
}
uint16_t telegram_fail_count() const {
return telegram_fail_count_;
}
void telegram_fail_count(uint8_t telegram_fail_count) {
telegram_fail_count_ = telegram_fail_count;
}
void increment_telegram_fail_count() {
telegram_fail_count_++;
}
uint16_t telegram_write_count() const {
return telegram_write_count_;
}
void telegram_write_count(uint8_t telegram_write_count) {
telegram_write_count_ = telegram_write_count;
}
void increment_telegram_write_count() {
telegram_write_count_++;
}
void post_send_query();
void print_last_tx();
class QueuedTxTelegram {
public:
QueuedTxTelegram(uint16_t id, std::shared_ptr<Telegram> && telegram, bool retry);
~QueuedTxTelegram() = default;
uint16_t id_; // sequential identifier
const std::shared_ptr<const Telegram> telegram_;
bool retry_; // is a retry
};
const std::deque<QueuedTxTelegram> queue() const {
return tx_telegrams_;
}
static constexpr uint8_t MAXIMUM_TX_RETRIES = 3;
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;
std::deque<QueuedTxTelegram> tx_telegrams_;
uint16_t telegram_read_count_ = 0; // # Tx successful reads
uint16_t telegram_write_count_ = 0; // # Tx successful writes
uint16_t telegram_fail_count_ = 0; // # Tx unsuccessful transmits
std::shared_ptr<Telegram> telegram_last_;
uint16_t telegram_last_post_send_query_; // which type ID to query after a successful send, to read back the values just written
uint8_t retry_count_ = 0; // count for # Tx retries
void send_telegram(const QueuedTxTelegram & tx_telegram);
void send_telegram(const uint8_t * data, const uint8_t length);
};
} // namespace emsesp
#endif

695
src/test/test.cpp Normal file
View File

@@ -0,0 +1,695 @@
/*
* 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 "test.h"
namespace emsesp {
// create some fake test data
// used with the 'test' command, under su/admin
void Test::run_test(uuid::console::Shell & shell, const std::string & command) {
if (command == "render") {
uint8_t test1 = 12;
int8_t test2 = -12;
uint16_t test3 = 456;
int16_t test4 = -456;
uint8_t test5 = 1; // bool = on
uint32_t test6 = 305419896;
float test7 = 89.43;
uint8_t test1u = EMS_VALUE_UINT_NOTSET;
int8_t test2u = EMS_VALUE_INT_NOTSET;
uint8_t test5u = EMS_VALUE_BOOL_NOTSET;
uint32_t test6u = EMS_VALUE_ULONG_NOTSET;
uint16_t test3u = EMS_VALUE_USHORT_NOTSET;
int16_t test4u = EMS_VALUE_SHORT_NOTSET;
EMSdevice::print_value(shell, 2, F("Selected flow temperature1"), test1, F_(degrees)); // 12
EMSdevice::print_value(shell, 2, F("Selected flow temperature2"), test2, F_(degrees)); // -12
EMSdevice::print_value(shell, 2, F("Selected flow temperature3"), test3, F_(degrees), 10); // 45.6
EMSdevice::print_value(shell, 2, F("Selected flow temperature4"), test4, F_(degrees), 10); // -45.6
EMSdevice::print_value(shell, 2, F("Selected flow temperature5"), test5, nullptr, EMS_VALUE_BOOL); // on
EMSdevice::print_value(shell, 2, F("Selected flow temperature6"), test6, F_(degrees)); //
EMSdevice::print_value(shell, 2, F("Selected flow temperature7"), test7, F_(degrees), 2); // 89.43
EMSdevice::print_value(shell, 2, F("Warm Water comfort setting"), F("Intelligent"));
char s[100];
strcpy(s, "Not very intelligent");
EMSdevice::print_value(shell, 2, F("Warm Water comfort setting2"), s);
shell.println();
EMSdevice::print_value(shell, 2, F("Selected flow temperature1u"), test1u, F_(degrees));
EMSdevice::print_value(shell, 2, F("Selected flow temperature2u"), test2u, F_(degrees));
EMSdevice::print_value(shell, 2, F("Selected flow temperature3u"), test3u, F_(degrees), 10);
EMSdevice::print_value(shell, 2, F("Selected flow temperature4u"), test4u, F_(degrees), 10);
EMSdevice::print_value(shell, 2, F("Selected flow temperature5u"), test5u, F_(degrees), EMS_VALUE_BOOL);
EMSdevice::print_value(shell, 2, F("Selected flow temperature6u"), test6u, F_(degrees), 100);
shell.println();
// check read_value to make sure it handles all the data type correctly
uint8_t message_data[] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // message_length is 9
auto telegram = std::make_shared<Telegram>(Telegram::Operation::RX, 0x10, 0x11, 0x1234, 0, message_data, sizeof(message_data));
uint8_t uint8b = EMS_VALUE_UINT_NOTSET;
telegram->read_value(uint8b, 0);
shell.printfln("uint8: expecting %02X, got:%02X", 1, uint8b);
int8_t int8b = EMS_VALUE_INT_NOTSET;
telegram->read_value(int8b, 0);
shell.printfln("int8: expecting %02X, got:%02X", 1, int8b);
uint16_t uint16b = EMS_VALUE_USHORT_NOTSET;
telegram->read_value(uint16b, 1);
shell.printfln("uint16: expecting %02X, got:%02X", 0x0203, uint16b);
int16_t int16b = EMS_VALUE_SHORT_NOTSET;
telegram->read_value(int16b, 1);
shell.printfln("int16: expecting %02X, got:%02X", 0x0203, int16b);
int16_t int16b8 = EMS_VALUE_SHORT_NOTSET;
telegram->read_value(int16b8, 1, 1); // force to 1 byte
shell.printfln("int16 1 byte: expecting %02X, got:%02X", 0x02, int16b8);
uint32_t uint32b = EMS_VALUE_ULONG_NOTSET;
telegram->read_value(uint32b, 1, 3);
shell.printfln("uint32 3 bytes: expecting %02X, got:%02X", 0x020304, uint32b);
uint32b = EMS_VALUE_ULONG_NOTSET;
telegram->read_value(uint32b, 1);
shell.printfln("uint32 4 bytes: expecting %02X, got:%02X", 0x02030405, uint32b);
// check out of bounds
uint16_t uint16 = EMS_VALUE_USHORT_NOTSET;
telegram->read_value(uint16, 9);
shell.printfln("uint16 out-of-bounds: was:%02X, new:%02X", EMS_VALUE_USHORT_NOTSET, uint16);
uint8_t uint8oob = EMS_VALUE_UINT_NOTSET;
telegram->read_value(uint8oob, 9);
shell.printfln("uint8 out-of-bounds: was:%02X, new:%02X", EMS_VALUE_UINT_NOTSET, uint8oob);
// check read bit
uint8_t uint8bitb = EMS_VALUE_UINT_NOTSET;
telegram->read_bitvalue(uint8bitb, 1, 1); // value is 0x02 = 0000 0010
shell.printfln("uint8 bit read: expecting 1, got:%d", uint8bitb);
uint8bitb = EMS_VALUE_UINT_NOTSET;
telegram->read_bitvalue(uint8bitb, 0, 0); // value is 0x01 = 0000 0001
shell.printfln("uint8 bit read: expecting 1, got:%d", uint8bitb);
shell.loop_all();
return;
}
if (command == "devices") {
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS); // this is important otherwise nothing will be picked up!
//emsdevices.push_back(EMSFactory::add(EMSdevice::DeviceType::BOILER, EMSdevice::EMS_DEVICE_ID_BOILER, 0, "", "My Boiler", 0, 0));
// A fake response - UBADevices(0x07)
rx_telegram({0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
}
if (command == "boiler2") {
// question: do we need to set the mask?
std::string version("1.2.3");
EMSESP::add_device(0x08, 123, version, EMSdevice::Brand::BUDERUS); // Nefit Trendline
}
// unknown device -
if ((command == "unknown") || (command == "u")) {
// question: do we need to set the mask?
std::string version("1.2.3");
// add boiler
EMSESP::add_device(0x08, 84, version, EMSdevice::Brand::BUDERUS);
EMSESP::rxservice_.loop();
// add Controller - BC10 GB142 - but using the same device_id to see what happens
EMSESP::add_device(0x09, 84, version, EMSdevice::Brand::BUDERUS);
EMSESP::rxservice_.loop();
// simulate getting version information back from an unknown device
// note there is no brand (byte 9)
rx_telegram({0x09, 0x0B, 0x02, 0x00, 0x59, 0x09, 0x0a});
shell.loop_all();
EMSESP::show_device_values(shell);
}
if (command == "unknown2") {
// simulate getting version information back from an unknown device
rx_telegram({0x09, 0x0B, 0x02, 0x00, 0x5A, 0x01, 0x02}); // product id is 90 which doesn't exist
}
if (command == "gateway") {
// add 0x48 KM200, via a version command
rx_telegram({0x48, 0x0B, 0x02, 0x00, 0xBD, 0x04, 0x06, 00, 00, 00, 00, 00, 00, 00});
// Boiler(0x08) -> All(0x00), UBADevices(0x07), data: 09 01 00 00 00 00 00 00 01 00 00 00 00
// check: make sure 0x48 is not detected again !
rx_telegram({0x08, 0x00, 0x07, 0x00, 0x09, 01, 00, 00, 00, 00, 00, 00, 01, 00, 00, 00, 00});
// add thermostat - Thermostat: RC300/RC310/Moduline 3000/CW400/Sense II (DeviceID:0x10, ProductID:158, Version:03.03) ** master device **
std::string version("01.03");
EMSESP::add_device(0x10, 158, version, EMSdevice::Brand::BUDERUS);
EMSESP::rxservice_.loop();
// simulate incoming telegram
// Thermostat(0x10) -> 48(0x48), ?(0x26B), data: 6B 08 4F 00 00 00 02 00 00 00 02 00 03 00 03 00 03
rx_telegram({0x10, 0x48, 0xFF, 00, 01, 0x6B, 00, 0x6B, 0x08, 0x4F, 00, 00, 00, 02, 00, 00, 00, 02, 00, 03, 00, 03, 00, 03});
}
if (command == "thermostat") {
shell.printfln(F("Testing adding devices on the EMS bus..."));
// create some fake devices
std::string version("1.2.3");
// add_device(0x10, 165, version, EMSdevice::Brand::BUDERUS);
// add_device(0x17, 125, version, EMSdevice::Brand::BUDERUS); // test unknown class test
// add_device(0x17, 93, version, EMSdevice::Brand::BUDERUS);
// add_device(0x17, 254, version, EMSdevice::Brand::BUDERUS); // test unknown product_id
EMSESP::add_device(0x18, 157, version, EMSdevice::Brand::BOSCH); // Bosch CR100 - https://github.com/proddy/EMS-ESP/issues/355
}
if (command == "solar") {
shell.printfln(F("Testing Solar"));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
std::string version("1.2.3");
EMSESP::add_device(0x30, 163, version, EMSdevice::Brand::BUDERUS); // SM100
EMSESP::rxservice_.loop();
// SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200
// B0 0B FF 00 02 62 00 44 02 7A 80 00 80 00 80 00 80 00 80 00 80 00 00 7C 80 00 80 00 80 00 80
rx_telegram({0xB0, 0x0B, 0xFF, 00, 0x02, 0x62, 00, 0x44, 0x02, 0x7A, 0x80, 00, 0x80, 0x00, 0x80, 00,
0x80, 00, 0x80, 00, 0x80, 00, 00, 0x7C, 0x80, 00, 0x80, 00, 0x80, 00, 0x80});
EMSESP::show_device_values(shell);
rx_telegram({0xB0, 0x0B, 0xFF, 0x00, 0x02, 0x62, 0x01, 0x44, 0x03, 0x30, 0x80, 00, 0x80, 00, 0x80, 00,
0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 00, 0x80, 0x33});
EMSESP::show_device_values(shell);
rx_telegram({0xB0, 00, 0xFF, 0x18, 02, 0x62, 0x80, 00, 0xB8});
EMSESP::show_device_values(shell);
EMSESP::send_raw_telegram("B0 00 FF 18 02 62 80 00 B8");
uart_telegram("30 00 FF 0A 02 6A 04"); // SM100 pump on 1
uart_telegram("30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0B 09 64 00 00 00 00"); // SM100 modulation
EMSESP::rxservice_.loop();
EMSESP::show_device_values(shell);
uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off 0
EMSESP::rxservice_.loop();
EMSESP::show_device_values(shell);
}
if (command == "km") {
shell.printfln(F("Testing KM200 Gateway"));
emsesp::EMSESP::watch(EMSESP::Watch::WATCH_RAW); // raw
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
std::string version("1.2.3");
EMSESP::add_device(0x10, 158, version, EMSdevice::Brand::BUDERUS); // RC300
EMSESP::add_device(0x48, 189, version, EMSdevice::Brand::BUDERUS); // KM200
EMSESP::rxservice_.loop();
// see https://github.com/proddy/EMS-ESP/issues/390
/*
uart_telegram_withCRC("90 48 FF 04 01 A6 5C");
uart_telegram_withCRC("90 48 FF 00 01 A6 4C");
uart_telegram_withCRC("90 48 FF 08 01 A7 6D");
uart_telegram("90 48 F9 00 FF 01 B0 08 0B 00 00 00 14 00 00 00 19 00 00 00 4B 00 00");
uart_telegram("90 48 F9 00 FF 01 9C 08 03 00 00 00 1E 00 00 00 4B 00 00 00 55 00 00");
uart_telegram("90 48 F9 00 FF 01 9C 07 03 00 00 00 1E 00 00 00 30 00 00 00 3C 00 00");
uart_telegram_withCRC("90 48 F9 00 FF 01 9D 00 43 00 00 00 01 00 00 00 02 00 03 00 06 00 03 00 02 05");
uart_telegram_withCRC("90 48 F9 00 FF 01 9D 07 03 00 00 00 1E 00 00 00 30 00 00 00 3C 00 00 00 30 C4");
uart_telegram_withCRC("90 48 F9 00 FF 01 9D 08 03 00 00 00 1E 00 00 00 4B 00 00 00 55 00 00 00 4B C8");
uart_telegram_withCRC("90 48 F9 00 FF 01 B1 08 0B 00 00 00 14 00 00 00 19 00 00 00 4B 00 00 00 19 A2");
uart_telegram_withCRC("90 48 FF 07 01 A7 51");
uart_telegram_withCRC("90 48 FF 08 01 A7 6D");
uart_telegram_withCRC("90 48 FF 00 01 A7 4D");
uart_telegram_withCRC("90 48 FF 25 01 A6 D8");
uart_telegram_withCRC("90 48 FF 07 01 A7 51");
uart_telegram_withCRC("90 0B 06 00 14 06 17 08 03 22 00 01 10 FF 00 18"); // time
uart_telegram("90 0B FF 00 01 A5 80 00 01 28 17 00 28 2A 05 A0 02 03 03 05 A0 05 A0 00 00 11 01 02 FF FF 00");
uart_telegram("90 0B FF 00 01 B9 00 2E 26 26 1B 03 00 FF FF 05 28 01 E1 20 01 0F 05 2A");
uart_telegram("90 0B FF 00 01 A6 90 0B FF 00 01 A6 18");
uart_telegram("90 0B FF 00 01 B9 00 2E 26 26 1B 03 00 FF FF 05 28 01 E1 20 01 0F 05 2A");
uart_telegram("90 0B FF 00 01 A6 90 0B FF 00 01 A6 18");
uart_telegram("90 0B FF 00 01 BA 00 2E 2A 26 1E 03 00 FF FF 05 2A 01 E1 20 01 0F 05 2A");
uart_telegram("90 0B FF 00 01 A7 90 0B FF 00 01 A7 19");
uart_telegram("90 0B FF 00 01 BB 00 2E 2A 26 1E 03 00 FF FF 05 2A 01 E1 20 01 0F 05 2A");
uart_telegram("90 0B FF 00 01 A8 90 0B FF 00 01 A8 16");
uart_telegram("90 0B FF 00 01 BC 00 2E 2A 26 1E 03 00 FF FF 05 2A 01 E1 20 01 0F 05 2A");
uart_telegram("90 0B FF 00 01 A5 80 00 01 28 17 00 28 2A 05 A0 02 03 03 05 A0 05 A0 00 00 11 01 02 FF FF 00");
uart_telegram_withCRC("C8 90 F7 02 01 FF 01 A6 BA");
uart_telegram_withCRC("90 48 FF 03 01 A6 40");
uart_telegram_withCRC("C8 90 FF 00 02 01 A6 D0");
// uart_telegram_withCRC("10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00");
*/
uart_telegram_withCRC("C8 90 F7 02 01 FF 01 A6 BA");
uart_telegram_withCRC("90 48 FF 04 01 A6 5C");
uart_telegram_withCRC("90 00 FF 00 01 A5 80 00 01 27 16 00 27 2A 05 A0 02 03 03 05 A0 05 A0 00 00 11 01 02 FF FF 00 9A");
uart_telegram_withCRC("90 00 FF 19 01 A5 01 04 00 00 00 00 FF 64 2A 00 3C 01 FF 92");
uart_telegram_withCRC("90 0B FF 00 01 A5 80 00 01 26 15 00 26 2A 05 A0 03 03 03 05 A0 05 A0 00 00 11 01 03 FF FF 00 FE");
uart_telegram_withCRC("90 00 FF 19 01 A5 01 04 00 00 00 00 FF 64 2A 00 3C 01 FF 92");
EMSESP::show_ems(shell);
EMSESP::rxservice_.loop();
EMSESP::show_device_values(shell);
}
if (command == "cr100") {
shell.printfln(F("Testing CR100"));
emsesp::EMSESP::watch(EMSESP::Watch::WATCH_RAW); // raw
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_HT3); // switch to junkers
std::string version("1.2.3");
EMSESP::add_device(0x18, 157, version, EMSdevice::Brand::BOSCH); // Bosch CR100 - https://github.com/proddy/EMS-ESP/issues/355
EMSESP::rxservice_.loop();
// RCPLUSStatusMessage_HC1(0x01A5)
// 98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 (no CRC)
uart_telegram({0x98, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xCF, 0x21, 0x2E, 0x00, 0x00, 0x2E, 0x24,
0x03, 0x25, 0x03, 0x03, 0x01, 0x03, 0x25, 0x00, 0xC8, 0x00, 0x00, 0x11, 0x01, 0x03});
uart_telegram("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03"); // without CRC
uart_telegram_withCRC("98 00 FF 00 01 A5 00 CF 21 2E 00 00 2E 24 03 25 03 03 01 03 25 00 C8 00 00 11 01 03 13"); // with CRC
shell.loop_all();
EMSESP::rxservice_.loop();
EMSESP::txservice_.flush_tx_queue();
shell.loop_all();
EMSESP::show_device_values(shell);
shell.invoke_command("thermostat");
shell.loop_all();
// shell.invoke_command("set temp 20");
shell.invoke_command("set mode auto");
shell.loop_all();
EMSESP::show_ems(shell);
shell.loop_all();
EMSESP::txservice_.send(); // send it to UART
}
if (command == "rx") {
shell.printfln(F("Testing Rx..."));
// fake telegrams. length includes CRC
// Boiler -> Me, UBAMonitorFast(0x18), telegram: 08 00 18 00 00 02 5A 73 3D 0A 10 65 40 02 1A 80 00 01 E1 01 76 0E 3D 48 00 C9 44 02 00 (#data=25)
uart_telegram({0x08, 0x00, 0x18, 0x00, 0x00, 0x02, 0x5A, 0x73, 0x3D, 0x0A, 0x10, 0x65, 0x40, 0x02, 0x1A,
0x80, 0x00, 0x01, 0xE1, 0x01, 0x76, 0x0E, 0x3D, 0x48, 0x00, 0xC9, 0x44, 0x02, 0x00});
// Boiler -> Thermostat, UBAParameterWW(0x33), telegram: 08 97 33 00 23 24 (#data=2)
uart_telegram({0x08, 0x97, 0x33, 0x00, 0x23, 0x24});
// Boiler -> Me, UBAParameterWW(0x33), telegram: 08 0B 33 00 08 FF 34 FB 00 28 00 00 46 00 FF FF 00 (#data=13)
uart_telegram({0x08, 0x0B, 0x33, 0x00, 0x08, 0xFF, 0x34, 0xFB, 0x00, 0x28, 0x00, 0x00, 0x46, 0x00, 0xFF, 0xFF, 0x00});
// Thermostat -> Me, RC20StatusMessage(0x91), telegram: 17 0B 91 05 44 45 46 47 (#data=4)
uart_telegram({0x17, 0x0B, 0x91, 0x05, 0x44, 0x45, 0x46, 0x47});
// bad CRC - corrupt telegram - CRC should be 0x8E
uint8_t t5[] = {0x17, 0x0B, 0x91, 0x05, 0x44, 0x45, 0x46, 0x47, 0x99};
EMSESP::rxservice_.add(t5, sizeof(t5));
// simulating a Tx record
uart_telegram({0x0B, 0x88, 0x07, 0x00, 0x20});
// Version Boiler
uart_telegram({0x08, 0x0B, 0x02, 0x00, 0x7B, 0x06, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04});
// Version Thermostat, device_id 0x11
uart_telegram({0x11, 0x0B, 0x02, 0x00, 0x4D, 0x03, 0x03});
// Thermostat -> all, telegram: 10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00
// 0x1A5 test ems+
uart_telegram({0x10, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xD7, 0x21, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x84,
0x01, 0x01, 0x03, 0x01, 0x84, 0x01, 0xF1, 0x00, 0x00, 0x11, 0x01, 0x00, 0x08, 0x63, 0x00});
// setting temp from 21.5 to 19.9C
uart_telegram({0x10, 0x00, 0xFF, 0x00, 0x01, 0xA5, 0x00, 0xC7, 0x21, 0x00, 0x00, 0x00, 0x00, 0x30, 0x01, 0x84,
0x01, 0x01, 0x03, 0x01, 0x84, 0x01, 0xF1, 0x00, 0x00, 0x11, 0x01, 0x00, 0x08, 0x63, 0x00});
// Thermostat -> Boiler, UBAFlags(0x35), telegram: 17 08 35 00 11 00 (#data=2)
uart_telegram({0x17, 0x08, 0x35, 0x00, 0x11, 0x00});
// Thermostat -> Boiler, UBASetPoints(0x1A), telegram: 17 08 1A 00 00 00 00 00 (#data=4)
uart_telegram({0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00});
// Thermostat -> Me, RC20Set(0xA8), telegram: 17 0B A8 00 01 00 FF F6 01 06 00 01 0D 01 00 FF FF 01 02 02 02 00 00 05 1F 05 1F 02 0E 00 FF (#data=27)
uart_telegram({0x17, 0x0B, 0xA8, 0x00, 0x01, 0x00, 0xFF, 0xF6, 0x01, 0x06, 0x00, 0x01, 0x0D, 0x01, 0x00, 0xFF,
0xFF, 0x01, 0x02, 0x02, 0x02, 0x00, 0x00, 0x05, 0x1F, 0x05, 0x1F, 0x02, 0x0E, 0x00, 0xFF});
// Boiler(0x08) -> All(0x00), UBAMonitorWW(0x34), data: 36 01 A5 80 00 21 00 00 01 00 01 3E 8D 03 77 91 00 80 00
uart_telegram(
{0x08, 0x00, 0x34, 0x00, 0x36, 0x01, 0xA5, 0x80, 0x00, 0x21, 0x00, 0x00, 0x01, 0x00, 0x01, 0x3E, 0x8D, 0x03, 0x77, 0x91, 0x00, 0x80, 0x00});
// test 0x2A - DHWStatus3
uart_telegram({0x88, 00, 0x2A, 00, 00, 00, 00, 00, 00, 00, 00, 00, 0xD2, 00, 00, 0x80, 00, 00, 01, 0x9D, 0x80, 0x00, 0x02, 0x79, 00});
}
if (command == "send") {
shell.printfln(F("Sending to Tx..."));
EMSESP::show_ems(shell);
EMSESP::txservice_.send(); // send it to UART
}
if (command == "tx") {
shell.printfln(F("Testing Tx..."));
EMSESP::txservice_.flush_tx_queue();
// TX queue example - Me -> Thermostat, (0x91), telegram: 0B 17 91 05 44 45 46 47 (#data=4)
uint8_t t11[] = {0x44, 0x45, 0x46, 0x47};
EMSESP::txservice_.add(Telegram::Operation::TX_RAW, 0x17, 0x91, 0x05, t11, sizeof(t11));
// TX - raw example test
uint8_t t12[] = {0x10, 0x08, 0x63, 0x04, 0x64};
EMSESP::txservice_.add(Telegram::Operation::TX_RAW, t12, sizeof(t12));
// TX - sending raw string
EMSESP::txservice_.send_raw("10 08 63 03 64 65 66");
// TX - send a read request
EMSESP::send_read_request(0x18, 0x08);
// TX - send a write request
uint8_t t18[] = {0x52, 0x79};
EMSESP::send_write_request(0x91, 0x17, 0x00, t18, sizeof(t18), 0x00);
// TX - send EMS+
const uint8_t t13[] = {0x90, 0x0B, 0xFF, 00, 01, 0xBA, 00, 0x2E, 0x2A, 0x26, 0x1E, 0x03,
00, 0xFF, 0xFF, 05, 0x2A, 01, 0xE1, 0x20, 0x01, 0x0F, 05, 0x2A};
EMSESP::txservice_.add(Telegram::Operation::TX_RAW, t13, sizeof(t13));
// EMS+ Junkers read request
EMSESP::send_read_request(0x16F, 0x10);
EMSESP::show_ems(shell);
// process whole Tx queue
for (uint8_t i = 0; i < 10; i++) {
EMSESP::txservice_.send(); // send it to UART
}
shell.loop_all();
EMSESP::txservice_.flush_tx_queue();
}
if (command == "poll") {
shell.printfln(F("Testing Poll..."));
// check if sending works when a poll comes in, with retries
EMSESP::txservice_.flush_tx_queue();
// simulate sending a read request
// uint8_t t16[] = {0x44, 0x45, 0x46, 0x47}; // Me -> Thermostat, (0x91), telegram: 0B 17 91 05 44 45 46 47 (#data=4)
// EMSESP::txservice_.add(Telegram::Operation::TX_RAW, 0x17, 0x91, 0x05, t16, sizeof(t16));
EMSESP::send_read_request(0x91, 0x17);
// EMSESP::txservice_.show_tx_queue();
// Simulate adding a Poll, so read request is sent
uint8_t poll[1] = {0x8B};
EMSESP::incoming_telegram(poll, 1);
// incoming Rx
uart_telegram({0x17, 0x08, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3A});
// Simulate adding a Poll - should send retry
EMSESP::incoming_telegram(poll, 1);
EMSESP::show_ems(shell);
uint8_t t2[] = {0x21, 0x22};
EMSESP::send_write_request(0x91, 0x17, 0x00, t2, sizeof(t2), 0);
EMSESP::show_ems(shell);
EMSESP::txservice_.flush_tx_queue();
}
if (command == "mqtt1") {
shell.printfln(F("Testing MQTT..."));
// MQTT test
EMSESP::txservice_.flush_tx_queue();
EMSESP::EMSESP::mqtt_.publish("boiler_cmd", "test me");
// EMSESP::mqtt_.show_queue();
// simulate an incoming mqtt msg
char topic[Mqtt::MQTT_TOPIC_MAX_SIZE];
char payload[100];
strcpy(topic, "boiler_cmd");
strcpy(payload, "12345");
EMSESP::mqtt_.incoming(topic, payload);
EMSESP::mqtt_.incoming(payload, payload); // should report error
strcpy(topic, "thermostat_cmd_mode");
strcpy(payload, "auto");
EMSESP::mqtt_.incoming(topic, payload);
strcpy(topic, "thermostat_cmd_temp");
strcpy(payload, "20");
EMSESP::mqtt_.incoming(topic, payload);
strcpy(topic, "thermostat_cmd");
strcpy(payload, "{\"cmd\":\"temp\",\"data\":22.52}");
EMSESP::mqtt_.incoming(topic, payload);
strcpy(topic, "boiler_cmd_wwtemp");
strcpy(payload, "66");
EMSESP::mqtt_.incoming(topic, payload);
strcpy(topic, "thermostat_cmd");
strcpy(payload, "{\"cmd\":\"temp\",\"hc\":2,\"data\":22}");
EMSESP::mqtt_.incoming(topic, payload);
strcpy(topic, "home/ems-esp/cmd");
strcpy(payload, "restart");
EMSESP::mqtt_.incoming(topic, payload);
// EMSESP::txservice_.show_tx_queue();
EMSESP::publish_all_values();
}
if (command == "poll2") {
shell.printfln(F("Testing Tx Sending last message on queue..."));
EMSESP::show_ems(shell);
uint8_t poll[1] = {0x8B};
EMSESP::incoming_telegram(poll, 1);
EMSESP::rxservice_.loop();
EMSESP::show_ems(shell);
EMSESP::txservice_.flush_tx_queue();
}
if (command == "rx2") {
uart_telegram({0x1B, 0x5B, 0xFD, 0x2D, 0x9E, 0x3A, 0xB6, 0xE5, 0x02, 0x20, 0x33, 0x30, 0x32, 0x3A, 0x20, 0x5B,
0x73, 0xFF, 0xFF, 0xCB, 0xDF, 0xB7, 0xA7, 0xB5, 0x67, 0x77, 0x77, 0xE4, 0xFF, 0xFD, 0x77, 0xFF});
}
// https://github.com/proddy/EMS-ESP/issues/380#issuecomment-633663007
if (command == "rx3") {
uart_telegram({0x21, 0x0B, 0xFF, 0x00});
}
if (command == "mqtt2") {
// Mqtt::subscribe("cmd", std::bind(&EMSESP::dummy_EMSESP::mqtt_commands, this, _1));
for (uint8_t i = 0; i < 30; i++) {
Mqtt::subscribe("cmd", dummy_mqtt_commands);
}
}
// testing the UART tx command, without a queue
if (command == "tx2") {
uint8_t t[] = {0x0B, 0x88, 0x18, 0x00, 0x20, 0xD4}; // including CRC
EMSuart::transmit(t, sizeof(t));
}
// send read request with offset
if (command == "offset") {
// send_read_request(0x18, 0x08);
EMSESP::txservice_.read_request(0x18, 0x08, 27); // no offset
}
if (command == "mixing") {
shell.printfln(F("Testing Mixing..."));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
std::string version("1.2.3");
EMSESP::add_device(0x28, 160, version, EMSdevice::Brand::BUDERUS); // MM100, WWC
EMSESP::add_device(0x29, 161, version, EMSdevice::Brand::BUDERUS); // MM200, WWC
EMSESP::add_device(0x20, 160, version, EMSdevice::Brand::BOSCH); // MM100
EMSESP::rxservice_.loop();
// WWC1 on 0x29
rx_telegram({0xA9, 0x00, 0xFF, 0x00, 0x02, 0x32, 0x02, 0x6C, 0x00, 0x3C, 0x00, 0x3C, 0x3C, 0x46, 0x02, 0x03, 0x03, 0x00, 0x3C});
// WWC2 on 0x28
rx_telegram({0xA8, 0x00, 0xFF, 0x00, 0x02, 0x31, 0x02, 0x35, 0x00, 0x3C, 0x00, 0x3C, 0x3C, 0x46, 0x02, 0x03, 0x03, 0x00, 0x3C});
// check for error "[emsesp] No telegram type handler found for ID 0x255 (src 0x20, dest 0x00)"
rx_telegram({0xA0, 0x00, 0xFF, 0x00, 0x01, 0x55, 0x00, 0x1A});
}
// finally dump to console
shell.loop_all();
}
// simulates a telegram in the Rx queue, but without the CRC which is added automatically
void Test::rx_telegram(const std::vector<uint8_t> & rx_data) {
uint8_t len = rx_data.size();
uint8_t data[50];
std::copy(rx_data.begin(), rx_data.end(), data);
data[len] = EMSESP::rxservice_.calculate_crc(rx_data.data(), len);
EMSESP::rxservice_.add(data, len + 1);
EMSESP::rxservice_.loop();
}
// simulates a telegram straight from UART, but without the CRC which is added automatically
void Test::uart_telegram(const std::vector<uint8_t> & rx_data) {
uint8_t len = rx_data.size();
uint8_t data[50];
std::copy(rx_data.begin(), rx_data.end(), data);
data[len] = EMSESP::rxservice_.calculate_crc(rx_data.data(), len);
EMSESP::incoming_telegram(data, len + 1);
EMSESP::rxservice_.loop();
}
// takes raw string, assuming it contains the CRC. This is what is output from 'watch raw'
void Test::uart_telegram_withCRC(const char * rx_data) {
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
char telegram[(EMS_MAX_TELEGRAM_LENGTH * 3) + 1];
for (uint8_t i = 0; i < strlen(rx_data); i++) {
telegram[i] = rx_data[i];
}
telegram[strlen(rx_data)] = '\0'; // make sure its terminated
uint8_t count = 0;
char * p;
char value[10] = {0};
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
// get first value, which should be the src
if ((p = strtok(telegram, " ,"))) { // delimiter
strlcpy(value, p, 10);
data[0] = (uint8_t)strtol(value, 0, 16);
}
// and iterate until end
while (p != 0) {
if ((p = strtok(nullptr, " ,"))) {
strlcpy(value, p, 10);
uint8_t val = (uint8_t)strtol(value, 0, 16);
data[++count] = val;
}
}
if (count == 0) {
return; // nothing to send
}
EMSESP::incoming_telegram(data, count + 1);
}
// takes raw string, adds CRC to end
void Test::uart_telegram(const char * rx_data) {
// since the telegram data is a const, make a copy. add 1 to grab the \0 EOS
char telegram[(EMS_MAX_TELEGRAM_LENGTH * 3) + 1];
for (uint8_t i = 0; i < strlen(rx_data); i++) {
telegram[i] = rx_data[i];
}
telegram[strlen(rx_data)] = '\0'; // make sure its terminated
uint8_t count = 0;
char * p;
char value[10] = {0};
uint8_t data[EMS_MAX_TELEGRAM_LENGTH];
// get first value, which should be the src
if ((p = strtok(telegram, " ,"))) { // delimiter
strlcpy(value, p, 10);
data[0] = (uint8_t)strtol(value, 0, 16);
}
// and iterate until end
while (p != 0) {
if ((p = strtok(nullptr, " ,"))) {
strlcpy(value, p, 10);
uint8_t val = (uint8_t)strtol(value, 0, 16);
data[++count] = val;
}
}
if (count == 0) {
return; // nothing to send
}
data[count + 1] = EMSESP::rxservice_.calculate_crc(data, count + 1); // add CRC
EMSESP::incoming_telegram(data, count + 2);
}
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
void Test::dummy_mqtt_commands(const char * message) {
//
}
#pragma GCC diagnostic pop
} // namespace emsesp

51
src/test/test.h Normal file
View File

@@ -0,0 +1,51 @@
/*
* 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 EMSESP_TEST_H
#define EMSESP_TEST_H
#include <Arduino.h>
#include <vector>
#include <string>
#include <uuid/common.h>
#include <uuid/console.h>
#include <uuid/log.h>
#include "emsdevice.h"
#include "emsfactory.h"
#include "telegram.h"
#include "mqtt.h"
#include "emsesp.h"
namespace emsesp {
class Test {
public:
static void run_test(uuid::console::Shell & shell, const std::string & command); // only for testing
static void dummy_mqtt_commands(const char * message);
static void rx_telegram(const std::vector<uint8_t> & data);
static void uart_telegram(const std::vector<uint8_t> & rx_data);
static void uart_telegram(const char * rx_data);
static void uart_telegram_withCRC(const char * rx_data);
};
} // namespace emsesp
#endif

View File

@@ -1,63 +0,0 @@
#ifdef TESTS
static const char * TEST_DATA[] = {
"08 00 34 00 3E 02 1D 80 00 31 00 00 01 00 01 0B AE 02", // test 1 - EMS
"10 00 FF 00 01 A5 80 00 01 30 28 00 30 28 01 54 03 03 01 01 54 02 A8 00 00 11 01 03 FF FF 00", // test 2 - RC310 ems+
"10 00 FF 19 01 A5 06 04 00 00 00 00 FF 64 37 00 3C 01 FF 01", // test 3 - RC310 ems+
"30 00 FF 00 02 62 00 A1 01 3F 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 4 - SM100
"10 00 FF 00 01 A5 00 D7 21 00 00 00 00 30 01 84 01 01 03 01 84 01 F1 00 00 11 01 00 08 63 00", // test 5 - Moduline 1010
"18 00 FF 00 01 A5 00 DD 21 23 00 00 23 00 00 00 00 00 00 00 00 00 00 00 00 00 01 00", // test 6 - RC300
"90 00 FF 00 00 6F 01 01 00 46 00 B9", // test 7 - FR10
"30 00 FF 00 02 62 01 FB 01 9E 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 2B", // test 8 - SM100
"30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0C 20 64 00 00 00 00 E9", // test 9 - SM100
"30 09 FF 00 00 01", // test 10 - EMS+
"30 0B 97 00", // test 11 - SM100
"30 00 FF 00 02 62 01 CA", // test 12 - SM100
"30 00 FF 00 02 8E 00 00 00 00 00 00 05 19 00 00 75 D3", // test 13 - SM100
"30 00 FF 00 02 63 80 00 80 00 00 00 80 00 80 00 80 00 00", // test 14 - SM100
"30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 0B 09 64 00 00 00 00", // test 15 - SM100
"30 00 FF 00 02 62 01 CA 01 93 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 16 - SM100
"30 00 FF 00 02 6A 03 03 03 00 03 03 03 03 03 00 03 03", // test 17 - SM100
"30 00 FF 00 02 6A 03 03 03 00 03 03 03 03 03 00 04 03", // test 18 - SM100
"30 00 FF 00 02 64 00 00 00 04 00 00 FF 00 00 1E 09 08 64 00 00 00 00", // test 19 - SM100
"10 00 FF 07 01 A5 32", // test 20 - RC EMS+
"38 10 FF 00 03 2B 00 D1 08 2A 01", // test 21 - heatpump
"38 10 FF 00 03 7B 08 24 00 4B", // test 22 - heatpump
"08 00 FF 31 03 94 00 00 00 00 00 00 00", // test 23 - heatpump
"08 00 FF 00 03 95 00 6D C5 0E 00 05 BA 7C 00 68 0A 92 00 00 00 00 00 00 00 00 00 00 00 CD", // test 24 - heatpump
"08 00 FF 48 03 95 00 00 01 47 00 00 00 00 00 00 00 00", // test 25 - heatpump
"08 00 FF 00 03 A2 10 01 02 02 00", // test 26 - heatpump
"08 00 FF 00 03 A3 00 0B 00 00 00 00 00 00 00 00 00 09 00 00 00 00 00 00 00 00 08 00 00 00 0A", // test 27 - heatpump
"30 00 FF 0A 02 6A 04", // test 28 - SM100 pump on
"30 00 FF 0A 02 6A 03", // test 29 - SM100 pump off
"48 90 02 00 01", // test 30 - version test
"10 48 02 00 9E", // test 31 - version test
"30 00 FF 00 02 8E 00 00 0C F3 00 00 06 02 00 00 76 33", // test 32 - SM100 energy
"30 00 FF 00 02 8E 00 00 07 9E 00 00 06 C5 00 00 76 35", // test 33 - SM100 energy
"30 00 FF 00 02 8E 00 00 00 00 00 00 06 C5 00 00 76 35", // test 34 - SM100 energy
"10 48 F9 00 FF 01 6C 08 4F 00 00 00 02 00 00 00 02 00 03 00 03 00 03 00 02", // test 35 - F9
"48 90 F9 00 11 FF 01 6D 08", // test 36 - F9
"10 48 F9 00 FF 01 6D 08", // test 37 - F9
"10 00 F7 00 FF 01 B9 35 19", // test 38 - F7
"30 00 FF 00 02 62 00 E7 01 AE 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00 80 00", // test 39 - SM100
"30 00 FF 00 02 62 00 E4", // test 40 - SM100
"10 48 F7 00 FF 01 A5 DF FF F7 7F 1F", // test 41 - gateway
"30 00 FF 09 02 64 1E", // test 42 - SM100
"08 00 18 00 05 03 30 00 00 00 00 04 40 80 00 02 17 80 00 00 00 FF 30 48 00 CB 00 00 00", // test 43 - sys pressure
"90 00 FF 00 00 6F 03 01 00 BE 00 BF", // test 44 - FR10
"08 00 E3 00 01 00 01 00 00 00 00 00 00 00 00 00 DF 00 64 55", // test 45 - heatpump Enviline
"08 00 E5 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0A", // test 46 - heatpump Enviline
"38 10 FF 00 03 2B 00 C7 07 C3 01", // test 47 - heatpump Enviline
"08 0B 19 00 00 F7 80 00 80 00 00 00 00 00 03 58 97 0C 7B 1F 00 00 00 06 C4 DF 02 64 48 80 00", // test 48 - outdoor temp check
"88 00 19 00 00 DC 80 00 80 00 FF FF 00 00 00 21 9A 06 E1 7C 00 00 00 06 C2 13 00 1E 90 80 00", // test 49 - check max length
"30 00 FF 00 02 8E 00 00 41 82 00 00 28 36 00 00 82 21", // test 50 - SM100
"10 00 FF 08 01 B9 26", // test 51 - EMS+ 0x1B9 set temp
"10 00 F7 00 FF 01 B9 21 E9", // test 52 - EMS+ 0x1B9 F7 test
"08 00 D1 00 00 80" // test 53 - outdoor temp
};
#endif

270
src/uart/emsuart_esp32.cpp Normal file
View File

@@ -0,0 +1,270 @@
/*
* 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/>.
*/
/*
* ESP32 UART port by @ArwedL and improved by @MichaelDvP. See https://github.com/proddy/EMS-ESP/issues/380
*/
#if defined(ESP32)
#include "uart/emsuart_esp32.h"
#include "emsesp.h"
namespace emsesp {
static intr_handle_t uart_handle;
static RingbufHandle_t buf_handle = NULL;
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;
uint32_t emsTxWait;
/*
* Task to handle the incoming data
*/
void EMSuart::emsuart_recvTask(void * para) {
while (1) {
size_t item_size;
uint8_t * telegram = (uint8_t *)xRingbufferReceive(buf_handle, &item_size, portMAX_DELAY);
uint8_t telegramSize = item_size;
if (telegram) {
EMSESP::incoming_telegram(telegram, telegramSize);
vRingbufferReturnItem(buf_handle, (void *)telegram);
}
}
}
/*
* UART interrupt, on break read the fifo and put the whole telegram to ringbuffer
*/
void IRAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
static uint8_t rxbuf[EMS_MAXBUFFERSIZE];
static uint8_t length;
if (EMS_UART.int_st.brk_det) {
EMS_UART.int_clr.brk_det = 1; // clear flag
EMS_UART.conf0.txd_brk = 0; // disable <brk>
if (emsTxBufIdx < emsTxBufLen) { // timer tx_mode is interrupted by <brk>
emsTxBufIdx = emsTxBufLen; // stop timer mode
drop_next_rx = true; // we have trash in buffer
}
length = 0;
while (EMS_UART.status.rxfifo_cnt) {
uint8_t rx = EMS_UART.fifo.rw_byte; // read all bytes from fifo
if (length < EMS_MAXBUFFERSIZE) {
rxbuf[length++] = rx;
} else {
drop_next_rx = true; // we have a overflow
}
}
if ((!drop_next_rx) && ((length == 2) || (length > 4))) {
int baseType = 0;
xRingbufferSendFromISR(buf_handle, rxbuf, length - 1, &baseType);
}
drop_next_rx = false;
}
}
void IRAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() {
if (emsTxBufLen == 0) {
return;
}
if (tx_mode_ > 50) {
for (uint8_t i = 0; i < emsTxBufLen; i++) {
EMS_UART.fifo.rw_byte = emsTxBuf[i];
}
EMS_UART.conf0.txd_brk = 1; // <brk> after send
} else {
if (emsTxBufIdx + 1 < emsTxBufLen) {
EMS_UART.fifo.rw_byte = emsTxBuf[emsTxBufIdx];
timerAlarmWrite(timer, emsTxWait, false);
timerAlarmEnable(timer);
} else if (emsTxBufIdx + 1 == emsTxBufLen) {
EMS_UART.fifo.rw_byte = emsTxBuf[emsTxBufIdx];
EMS_UART.conf0.txd_brk = 1; // <brk> after send
}
emsTxBufIdx++;
}
}
/*
* init UART driver
*/
void EMSuart::start(const uint8_t tx_mode) {
if (tx_mode_ != 0xFF) { // uart already initialized
tx_mode_ = tx_mode;
restart();
return;
}
tx_mode_ = tx_mode;
uart_config_t uart_config = {
.baud_rate = EMSUART_BAUD,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
uart_param_config(EMSUART_UART, &uart_config);
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.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, &uart_handle);
xTaskCreate(emsuart_recvTask, "emsuart_recvTask", 2048, NULL, configMAX_PRIORITIES - 1, NULL);
timer = timerBegin(1, 80, true); // timer prescale to 1 µs, countup
timerAttachInterrupt(timer, &emsuart_tx_timer_intr_handler, true); // Timer with edge interrupt
restart();
}
/*
* Stop, disables interrupt
*/
void EMSuart::stop() {
EMS_UART.int_ena.val = 0; // disable all intr.
// timerAlarmDisable(timer);
};
/*
* Restart Interrupt
*/
void EMSuart::restart() {
if (EMS_UART.int_raw.brk_det) {
EMS_UART.int_clr.brk_det = 1; // clear flag
drop_next_rx = true; // and drop first frame
}
EMS_UART.int_ena.brk_det = 1; // activate only break
emsTxBufIdx = 0;
emsTxBufLen = 0;
if (tx_mode_ == 1) {
EMS_UART.idle_conf.tx_idle_num = 5;
} else if (tx_mode_ == 2) {
EMS_UART.idle_conf.tx_idle_num = 10;
} else if (tx_mode_ == 3) {
EMS_UART.idle_conf.tx_idle_num = 7;
} else if (tx_mode_ == 4) {
EMS_UART.idle_conf.tx_idle_num = 2;
} else if (tx_mode_ == 5) {
EMS_UART.idle_conf.tx_idle_num = 2;
} else if (tx_mode_ <= 50) {
EMS_UART.idle_conf.tx_idle_num = tx_mode_;
emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ + 10);
} else {
EMS_UART.idle_conf.tx_idle_num = 2;
emsTxWait = EMSUART_TX_BIT_TIME * (tx_mode_ - 50);
}
}
/*
* Sends a 1-byte poll, ending with a <BRK>
*/
void EMSuart::send_poll(const uint8_t data) {
EMS_UART.fifo.rw_byte = data;
EMS_UART.conf0.txd_brk = 1; // <brk> after send
return;
}
/*
* Send data to Tx line, ending with a <BRK>
* buf contains the CRC and len is #bytes including the CRC
* returns code, 1=success
*/
uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
if (len == 0 || len >= EMS_MAXBUFFERSIZE) {
return EMS_TX_STATUS_ERR;
}
// needs to be disabled for the delayed modes otherwise the uart makes a <brk> after every byte
EMS_UART.conf0.txd_brk = 0;
if (tx_mode_ > 5) { // timer controlled modes
for (uint8_t i = 0; i < len; i++) {
emsTxBuf[i] = buf[i];
}
emsTxBufIdx = 0;
emsTxBufLen = len;
timerAlarmWrite(timer, emsTxWait, false);
timerAlarmEnable(timer);
return EMS_TX_STATUS_OK;
}
if (tx_mode_ == 5) { // wait before sending
vTaskDelay(4 / portTICK_PERIOD_MS);
for (uint8_t i = 0; i < len; i++) {
EMS_UART.fifo.rw_byte = buf[i];
}
EMS_UART.conf0.txd_brk = 1; // <brk> after send
return EMS_TX_STATUS_OK;
}
if (tx_mode_ == EMS_TXMODE_NEW) { // hardware controlled modes
for (uint8_t i = 0; i < len; i++) {
EMS_UART.fifo.rw_byte = buf[i];
}
EMS_UART.conf0.txd_brk = 1; // <brk> after send
return EMS_TX_STATUS_OK;
}
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // EMS+ with long delay
for (uint8_t i = 0; i < len - 1; i++) {
EMS_UART.fifo.rw_byte = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_PLUS);
}
EMS_UART.fifo.rw_byte = buf[len - 1];
EMS_UART.conf0.txd_brk = 1; // <brk> after send, cleard by hardware after send
return EMS_TX_STATUS_OK;
}
if (tx_mode_ == EMS_TXMODE_HT3) { // HT3 with 7 bittimes delay
for (uint8_t i = 0; i < len - 1; i++) {
EMS_UART.fifo.rw_byte = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_HT3);
}
EMS_UART.fifo.rw_byte = buf[len - 1];
EMS_UART.conf0.txd_brk = 1; // <brk> after send, cleard by hardware after send
return EMS_TX_STATUS_OK;
}
// mode 1
// flush fifos -- not supported in ESP32 uart #2!
// EMS_UART.conf0.rxfifo_rst = 1;
// EMS_UART.conf0.txfifo_rst = 1;
for (uint8_t i = 0; i < len - 1; i++) {
volatile uint8_t _usrxc = EMS_UART.status.rxfifo_cnt;
EMS_UART.fifo.rw_byte = buf[i]; // send each Tx byte
uint16_t timeoutcnt = EMSUART_TX_TIMEOUT;
while ((EMS_UART.status.rxfifo_cnt == _usrxc) && (--timeoutcnt > 0)) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT); // burn CPU cycles...
}
}
EMS_UART.fifo.rw_byte = buf[len - 1]; // send each Tx byte
EMS_UART.conf0.txd_brk = 1; // <brk> after send, cleard by hardware after send
return EMS_TX_STATUS_OK;
}
} // namespace emsesp
#endif

98
src/uart/emsuart_esp32.h Normal file
View File

@@ -0,0 +1,98 @@
/*
* 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/>.
*/
/*
* ESP32 UART port by @ArwedL and improved by @MichaelDvP. See https://github.com/proddy/EMS-ESP/issues/380
*/
#if defined(ESP32)
#ifndef EMSESP_EMSUART_H
#define EMSESP_EMSUART_H
#include <Arduino.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/ringbuf.h"
#include "freertos/queue.h"
#include <driver/uart.h>
#include <driver/timer.h>
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra for BRK
#define EMSUART_UART UART_NUM_2 // on the ESP32 we're using UART2
#define EMS_UART UART2 // for intr setting
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_TXMODE_DEFAULT 1
#define EMS_TXMODE_EMSPLUS 2
#define EMS_TXMODE_HT3 3
#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
// 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
// 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
// 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
// customize the GPIO pins for RX and TX here
#ifdef WEMOS_D1_32
#define EMSUART_RXPIN 23 // 17 is UART2 RX. Use 23 for D7 on a Wemos D1-32 mini for backwards compatabilty
#define EMSUART_TXPIN 5 // 16 is UART2 TX. Use 5 for D8 on a Wemos D1-32 mini for backwards compatabilty
#else
#define EMSUART_RXPIN 17 // 17 is UART2 RX. Use 23 for D7 on a Wemos D1-32 mini for backwards compatabilty
#define EMSUART_TXPIN 16 // 16 is UART2 TX. Use 5 for D8 on a Wemos D1-32 mini for backwards compatabilty
#endif
namespace emsesp {
#define EMS_TX_STATUS_OK 1
#define EMS_TX_STATUS_ERR 0
class EMSuart {
public:
EMSuart() = default;
~EMSuart() = default;
static void start(const uint8_t tx_mode);
static void send_poll(const uint8_t data);
static void stop();
static void restart();
static uint16_t transmit(const uint8_t * buf, const uint8_t len);
private:
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();
};
} // namespace emsesp
#endif
#endif

View File

@@ -0,0 +1,437 @@
/*
* 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/>.
*/
#if defined(ESP8266)
#include "uart/emsuart_esp8266.h"
#include "emsesp.h"
namespace emsesp {
os_event_t recvTaskQueue[EMSUART_recvTaskQueueLen]; // our Rx queue
EMSuart::EMSRxBuf_t * pEMSRxBuf;
EMSuart::EMSRxBuf_t * paEMSRxBuf[EMS_MAXBUFFERS];
uint8_t emsRxBufIdx = 0;
uint8_t tx_mode_ = 0xFF;
bool drop_next_rx = true;
uint8_t emsTxBuf[EMS_MAXBUFFERSIZE];
uint8_t emsTxBufIdx;
uint8_t emsTxBufLen;
uint32_t emsTxWait;
bool EMSuart::sending_ = false;
//
// Main interrupt handler
// Important: must not use ICACHE_FLASH_ATTR
//
void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
static uint8_t length = 0;
static uint8_t uart_buffer[EMS_MAXBUFFERSIZE + 2];
if (USIS(EMSUART_UART) & ((1 << UIBD))) { // BREAK detection = End of EMS data block
USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset tx-brk
if (emsTxBufIdx < emsTxBufLen) { // irq tx_mode is interrupted by <brk>
emsTxBufIdx = emsTxBufLen; // stop tx
drop_next_rx = true; // we have trash in buffer
}
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
USIE(EMSUART_UART) &= ~(1 << UIFF); // disable fifo-full irq
length = 0;
while ((USS(EMSUART_UART) >> USRXC) & 0x0FF) { // read fifo into buffer
uint8_t rx = USF(EMSUART_UART);
if (length < EMS_MAXBUFFERSIZE) {
uart_buffer[length++] = rx;
} else {
drop_next_rx = true;
}
}
if (!drop_next_rx) {
pEMSRxBuf->length = length;
os_memcpy((void *)pEMSRxBuf->buffer, (void *)&uart_buffer, pEMSRxBuf->length); // copy data into transfer buffer, including the BRK 0x00 at the end
system_os_post(EMSUART_recvTaskPrio, 0, 0); // call emsuart_recvTask() at next opportunity
}
drop_next_rx = false;
sending_ = false;
}
}
/*
* system task triggered on BRK interrupt
* incoming received messages are always asynchronous
* The full buffer is sent to EMSESP::incoming_telegram()
*/
void ICACHE_FLASH_ATTR EMSuart::emsuart_recvTask(os_event_t * events) {
EMSRxBuf_t * pCurrent = pEMSRxBuf;
pEMSRxBuf = paEMSRxBuf[++emsRxBufIdx % EMS_MAXBUFFERS]; // next free EMS Receive buffer
uint8_t length = pCurrent->length; // number of bytes including the BRK at the end
pCurrent->length = 0;
// Ignore telegrams with no data value, then transmit EMS buffer, excluding the BRK
if (length > 4 || length == 2) {
EMSESP::incoming_telegram((uint8_t *)pCurrent->buffer, length - 1);
}
}
/*
* flush everything left over in buffer, this clears both rx and tx FIFOs
*/
void ICACHE_FLASH_ATTR EMSuart::emsuart_flush_fifos() {
USC0(EMSUART_UART) |= ((1 << UCRXRST) | (1 << UCTXRST)); // set bits
USC0(EMSUART_UART) &= ~((1 << UCRXRST) | (1 << UCTXRST)); // clear bits
}
// ISR to Fire when Timer is triggered
void ICACHE_RAM_ATTR EMSuart::emsuart_tx_timer_intr_handler() {
if (tx_mode_ > 50) {
for (uint8_t i = 0; i < emsTxBufLen; i++) {
USF(EMSUART_UART) = emsTxBuf[i];
}
USC0(EMSUART_UART) |= (1 << UCBRK); // set <BRK>
} else {
if (emsTxBufIdx > emsTxBufLen) {
return;
}
if (emsTxBufIdx < emsTxBufLen) {
USF(EMSUART_UART) = emsTxBuf[emsTxBufIdx];
timer1_write(emsTxWait);
} else if (emsTxBufIdx == emsTxBufLen) {
USC0(EMSUART_UART) |= (1 << UCBRK); // set <BRK>
}
emsTxBufIdx++;
}
}
/*
* init UART0 driver
*/
void ICACHE_FLASH_ATTR EMSuart::start(uint8_t tx_mode) {
if (tx_mode > 50) {
emsTxWait = 5 * EMSUART_TX_BIT_TIME * (tx_mode - 50); // bittimes wait before sending
} else if (tx_mode > 5) {
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();
return;
}
tx_mode_ = tx_mode;
// allocate and preset EMS Receive buffers
for (int i = 0; i < EMS_MAXBUFFERS; i++) {
EMSRxBuf_t * p = (EMSRxBuf_t *)malloc(sizeof(EMSRxBuf_t));
p->length = 0;
paEMSRxBuf[i] = p;
}
pEMSRxBuf = paEMSRxBuf[0]; // reset EMS Rx Buffer
ETS_UART_INTR_DISABLE();
ETS_UART_INTR_ATTACH(nullptr, nullptr);
// pin settings
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0TXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
PIN_PULLUP_DIS(PERIPHS_IO_MUX_U0RXD_U);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0RXD_U, FUNC_U0RXD);
// set 9600, 8 bits, no parity check, 1 stop bit
USD(EMSUART_UART) = (UART_CLK_FREQ / EMSUART_BAUD);
if (tx_mode_ == 5) {
USC0(EMSUART_UART) = 0x2C; // 8N1.5
} else {
USC0(EMSUART_UART) = EMSUART_CONFIG; // 8N1
}
emsuart_flush_fifos();
// conf1 params
// UCTOE = RX TimeOut enable (default is 1)
// UCTOT = RX TimeOut Threshold (7 bit) = want this when no more data after 1 characters (default is 2)
// 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) = (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);
// disable esp debug which will go to Tx and mess up the line - see https://github.com/espruino/Espruino/issues/655
system_set_os_print(0);
// swap Rx and Tx pins to use GPIO13 (D7) and GPIO15 (D8) respectively
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
}
/*
* stop UART0 driver
* This is called prior to an OTA upload and also before a save to the filesystem to prevent conflicts
*/
void ICACHE_FLASH_ATTR EMSuart::stop() {
ETS_UART_INTR_DISABLE();
timer1_disable();
}
/*
* re-start UART0 driver
*/
void ICACHE_FLASH_ATTR EMSuart::restart() {
if (USIR(EMSUART_UART) & ((1 << UIBD))) {
USIC(EMSUART_UART) = (1 << UIBD); // INT clear the BREAK detect interrupt
drop_next_rx = true;
}
ETS_UART_INTR_ENABLE();
emsTxBufIdx = 0;
emsTxBufLen = 0;
timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
}
/*
* 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)) {
}
// To create a 11-bit <BRK> we set TXD_BRK bit so the break signal will
// automatically be sent when the tx fifo is empty
ETS_UART_INTR_DISABLE();
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
ETS_UART_INTR_ENABLE();
}
/*
* 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_ > 50) { // timer controlled modes
emsTxBuf[0] = data;
emsTxBufLen = 1;
timer1_write(emsTxWait);
} else if (tx_mode_ > 5) { // timer controlled modes
emsTxBuf[0] = data;
emsTxBufIdx = 0;
emsTxBufLen = 1;
timer1_write(emsTxWait);
} else if (tx_mode_ == 5) {
delayMicroseconds(3000);
USF(EMSUART_UART) = data;
USC0(EMSUART_UART) |= (1 << UCBRK);
} 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>
} else if (tx_mode_ == EMS_TXMODE_EMSPLUS) {
USF(EMSUART_UART) = data;
delayMicroseconds(EMSUART_TX_WAIT_PLUS);
tx_brk(); // send <BRK>
} else {
// tx_mode 1
// EMS1.0, same logic as in transmit
ETS_UART_INTR_DISABLE();
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...
}
USC0(EMSUART_UART) |= (1 << UCBRK); // set <BRK>
timeoutcnt = EMSUART_TX_TIMEOUT;
while (!(USIR(EMSUART_UART) & (1 << UIBD)) && (--timeoutcnt > 0)) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT);
}
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK>
ETS_UART_INTR_ENABLE();
}
}
/*
* Send data to Tx line, ending with a <BRK>
* buf contains the CRC and len is #bytes including the CRC
* returns code, 0=success, 1=brk error, 2=watchdog timeout
*/
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;
// all at once after a inititial timer delay
if (tx_mode_ > 50) {
for (uint8_t i = 0; i < len; i++) {
emsTxBuf[i] = buf[i];
}
emsTxBufLen = len;
timer1_write(emsTxWait);
return EMS_TX_STATUS_OK;
}
// timer controlled modes with extra delay
if (tx_mode_ > 5) {
for (uint8_t i = 0; i < len; i++) {
emsTxBuf[i] = buf[i];
}
emsTxBufIdx = 0;
emsTxBufLen = len;
timer1_write(emsTxWait);
return EMS_TX_STATUS_OK;
}
// fixed dealy before sending
if (tx_mode_ == 5) {
delayMicroseconds(3000);
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
}
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
return EMS_TX_STATUS_OK;
}
// new code from Michael. See https://github.com/proddy/EMS-ESP/issues/380
if (tx_mode_ == EMS_TXMODE_NEW) { // tx_mode 4
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
}
USC0(EMSUART_UART) |= (1 << UCBRK); // send <BRK> at the end
return EMS_TX_STATUS_OK;
}
// EMS+ https://github.com/proddy/EMS-ESP/issues/23#
if (tx_mode_ == EMS_TXMODE_EMSPLUS) { // tx_mode 2, With extra tx delay for EMS+
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
delayMicroseconds(EMSUART_TX_WAIT_PLUS); // 2070
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
// Junkers logic by @philrich, tx_mode 3
if (tx_mode_ == EMS_TXMODE_HT3) {
for (uint8_t i = 0; i < len; i++) {
USF(EMSUART_UART) = buf[i];
// just to be safe wait for tx fifo empty (still needed?)
while (((USS(EMSUART_UART) >> USTXC) & 0xff)) {
}
// wait until bits are sent on wire
delayMicroseconds(EMSUART_TX_WAIT_HT3);
}
tx_brk(); // send <BRK>
return EMS_TX_STATUS_OK;
}
/*
* Logic for tx_mode of 1
* based on code from https://github.com/proddy/EMS-ESP/issues/103 by @susisstrolch
*
* Logic:
* we emit the whole telegram, with Rx interrupt disabled, collecting busmaster response in FIFO.
* after sending the last char we poll the Rx status until either
* - size(Rx FIFO) == size(Tx-Telegram)
* - <BRK> is detected
* At end of receive we re-enable Rx-INT and send a Tx-BRK in loopback mode.
*
* EMS-Bus error handling
* 1. Busmaster stops echoing on Tx w/o permission
* 2. Busmaster cancel telegram by sending a BRK
*
* Case 1. is handled by a watchdog counter which is reset on each
* Tx attempt. The timeout should be 20x EMSUART_TX_BIT_TIME plus
* some smart guess for processing time on targeted EMS device.
* We set Status to EMS_TX_WTD_TIMEOUT and return
*
* Case 2. is handled via a BRK chk during transmission.
* We set Status to EMS_TX_BRK_DETECT and return
*
*/
// 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
for (uint8_t i = 0; i < len; i++) {
volatile uint8_t _usrxc = (USS(EMSUART_UART) >> USRXC) & 0xFF;
uint16_t timeoutcnt = EMSUART_TX_TIMEOUT;
USF(EMSUART_UART) = buf[i]; // send each Tx byte
// wait for echo
while ((((USS(EMSUART_UART) >> USRXC) & 0xFF) == _usrxc) && (--timeoutcnt > 0)) {
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 in the loopback and re=enable Rx-INT.
// 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
USC0(EMSUART_UART) |= (1 << UCBRK); // set <BRK>
uint16_t timeoutcnt = EMSUART_TX_TIMEOUT;
// wait until BRK detected...
while (!(USIR(EMSUART_UART) & (1 << UIBD)) && (--timeoutcnt > 0)) {
delayMicroseconds(EMSUART_TX_BUSY_WAIT);
}
USC0(EMSUART_UART) &= ~(1 << UCBRK); // clear <BRK>
}
ETS_UART_INTR_ENABLE(); // open up the FIFO again to start receiving
return EMS_TX_STATUS_OK; // send the Tx ok status back
}
} // namespace emsesp
#endif

View File

@@ -0,0 +1,94 @@
/*
* 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/>.
*/
#if defined(ESP8266)
#ifndef EMSESP_EMSUART_H
#define EMSESP_EMSUART_H
#include <Arduino.h>
#include <user_interface.h>
#define EMSUART_UART 0 // UART 0
#define EMSUART_CONFIG 0x1C // 8N1 (8 bits, no parity, 1 stop bit)
#define EMSUART_BAUD 9600 // uart baud rate for the EMS circuit
#define EMS_MAXBUFFERS 3 // buffers for circular filling to avoid collisions
#define EMS_MAXBUFFERSIZE 33 // max size of the buffer. EMS packets are max 32 bytes, plus extra 2 for BRKs
#define EMSUART_recvTaskPrio 2 // 0, 1 or 2. 0 being the lowest
#define EMSUART_recvTaskQueueLen 10 // number of queued Rx triggers
#define EMS_TXMODE_DEFAULT 1
#define EMS_TXMODE_EMSPLUS 2
#define EMS_TXMODE_HT3 3
#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
// 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
// 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
// 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
namespace emsesp {
#define EMS_TX_STATUS_ERR 0
#define EMS_TX_STATUS_OK 1
class EMSuart {
public:
EMSuart() = default;
~EMSuart() = default;
static void ICACHE_FLASH_ATTR start(uint8_t tx_mode);
static void ICACHE_FLASH_ATTR stop();
static void ICACHE_FLASH_ATTR restart();
static void ICACHE_FLASH_ATTR send_poll(uint8_t data);
static uint16_t ICACHE_FLASH_ATTR transmit(uint8_t * buf, uint8_t len);
static bool sending() {
return sending_;
}
typedef struct {
uint8_t length;
uint8_t buffer[EMS_MAXBUFFERSIZE];
} EMSRxBuf_t;
private:
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_;
};
} // namespace emsesp
#endif
#endif

View File

@@ -1 +1 @@
#define APP_VERSION "1.9.5"
#define EMSESP_APP_VERSION "2.0.0_b1" // based off b32

View File

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
html {position: relative;overflow: scroll;overflow-x: hidden;min-height: 100% }::-webkit-scrollbar {width: 0px;background: transparent;}::-webkit-scrollbar-thumb {background: #e8e8e8;}body {background: #f1f3f6;margin-bottom: 60px }p {font-size: 1.1em;font-weight: 300;line-height: 1.7em;color: #999 }a, a:focus, a:hover {color: inherit;text-decoration: none;transition: all .3s }.navbar {padding: 15px 10px;background: #fff;border: none;border-radius: 0;margin-bottom: 40px;box-shadow: 1px 1px 3px rgba(0, 0, 0, .1) }#dismiss, #sidebar {background: #337ab7 }#content.navbar-btn {box-shadow: none;outline: 0;border: none }.line {width: 100%;height: 1px;border-bottom: 1px dashed #ddd;margin: 40px 0 }#sidebar {width: 250px;position: fixed;top: 0;left: -250px;height: 100vh;z-index: 999;color: #fff;transition: all .3s;overflow-y: auto;box-shadow: 3px 3px 3px rgba(0, 0, 0, .2) }@media screen and (min-width:768px) {#sidebar {left: 0 }.footer {margin-left: 250px }#ajaxcontent {margin-left: 250px }#dismiss, .navbar-btn {display: none }}#sidebar.active {left: 0 }#dismiss {width: 35px;height: 35px;line-height: 35px;text-align: center;position: absolute;top: 10px;right: 10px;cursor: pointer;-webkit-transition: all .3s;-o-transition: all .3s;transition: all .3s }#dismiss:hover {background: #fff;color: #337ab7 }.overlay {position: fixed;width: 100vw;height: 100vh;background: rgba(0, 0, 0, .7);z-index: 998;display: none }#sidebar .sidebar-header {padding: 20px;background: #337ab7 }#sidebar ul.components {padding: 20px 0;border-bottom: 1px solid #47748b }#content, ul.CTAs {padding: 20px }#sidebar ul p {color: #fff;padding: 10px }#sidebar ul li a {padding: 10px;font-size: 1.1em;display: block }#sidebar ul li a:hover {color: #337ab7;background: #fff }#sidebar ul li.active>a, a[aria-expanded=true] {color: #fff;background: #2e6da4 }a[data-toggle=collapse] {position: relative }a[aria-expanded=false]::before, a[aria-expanded=true]::before {content: '\e259';display: block;position: absolute;right: 20px;font-family: 'Glyphicons Halflings';font-size: .6em }#sidebar ul ul a, ul.CTAs a {font-size: .9em }a[aria-expanded=true]::before {content: '\e260' }#sidebar ul ul a {padding-left: 30px;background: #2e6da4 }ul.CTAs a {text-align: center;display: block;border-radius: 5px;margin-bottom: 5px }a.download {background: #fff;color: #337ab7 }#sidebar a.article, a.article:hover {background: #2e6da4;color: #fff }#content {width: 100%;min-height: 100vh;transition: all .3s;position: absolute;top: 0;right: 0 }.footer {position: fixed;bottom: 0;width: 100%;height: 45px;background-color: #f1f3f6 }i {margin-right: 1em }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,244 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAYFBMVEX///8zMzM1NTU0NDQ2Njby8vJ4eHhBQUGSkpJdXV36+vqbm5tpaWlPT0/Ly8uGhoZxcXHAwMBVVVVJSUm3t7fu7u6mpqY8PDzo6OiPj4/U1NR+fn5ra2tGRkbb29vExMQQm/MfAAAA4klEQVQ4jd1SbXaDMAwD2/kAElpKIRTa7v63nGPY9gjhAKt+OZGeItspik9D3Vqt9eU+nPCtC0CMsh/rHN8EwjICga5Thicof4Dk1ME/CA8EYkOPNF9FYu5dhVGJYU4MSozOzVSrzkcF2b3AxktopJ4Ni6Had6L5BfDbAC58QKMOAnLbYTwR/LrajEBcQye1unEg9HvBHWMw33I5aOli2Xcx9NKGWUZ7WyfWJZOKwWQN68Tpme6rvm6rEtDrXaSY3J+C+q8Dz+Ft4E0hvwLPOcMzZlsZY/zSZf/LmkQxTtl/jG/cCAezKUvMMwAAAABJRU5ErkJggg=="
rel="icon" type="image/x-icon" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title id="customname"></title>
<link href="css/required.css" rel="stylesheet">
</head>
<body>
<div class="wrapper">
<nav id="sidebar">
<div id="dismiss">
<i class="glyphicon glyphicon-arrow-left"></i>
</div>
<div class="sidebar-header">
<h1 id="customname2" class="text-center" onclick="home();"></h1>
<h6 id="mainver" class="text-center"></h6>
</div>
<ul class="list-unstyled components">
<li class="active">
<a href="#" id="custom_status"><i class="glyphicon glyphicon-home"></i>Dashboard</a>
</li>
<li>
<a href="#" id="status"><i class="glyphicon glyphicon-equalizer"></i>System Status</a>
</li>
<li>
<a href="#homeSubmenu" data-toggle="collapse" aria-expanded="false"><i
class="glyphicon glyphicon-cog"></i>Settings</a>
<ul class="collapse list-unstyled" id="homeSubmenu">
<li>
<a href="#" id="network"><i class="glyphicon glyphicon-signal"></i>Wireless Network</a>
</li>
<li>
<a href="#" id="general"><i class="glyphicon glyphicon-list-alt"></i>General Settings</a>
</li>
<li>
<a href="#" id="mqtt"><i class="glyphicon glyphicon-link"></i>MQTT Settings</a>
</li>
<li>
<a href="#" id="ntp"><i class="glyphicon glyphicon-hourglass"></i>Time Settings</a>
</li>
<li>
<a href="#" id="custom"><i class="glyphicon glyphicon-wrench"></i>Custom Settings</a>
</li>
</ul>
</li>
<li>
<a href="#" id="backup"><i class="glyphicon glyphicon-floppy-disk"></i>Backup & Restore</a>
</li>
<li>
<a href="#" id="reset"><i class="glyphicon glyphicon-repeat"></i>Factory Reset</a>
</li>
<li>
<a href="#" id="restart"><i class="glyphicon glyphicon-refresh"></i>Restart System</a>
</li>
<li>
<a data-toggle="modal" href="#update"><i class="glyphicon glyphicon-open"></i>Update Firmware</a>
</li>
</ul>
<ul class="list-unstyled CTAs">
<li>
<a id="helpurl" target="_blank" class="article">Help</a>
</li>
<li>
<a href="#" class="article" onclick="logout();">Logout</a>
</li>
</ul>
<div class="container-fluid">
<div class="row">
<div class="col-xs-3">
<div style="position:fixed; top:0; bottom:40px; float:none;">
<hr>
</hr>
<footer style="position:fixed; bottom: 0; height:40px;"><a id="appurl"
href="https://github.com/proddy" target="_blank">
<h6 id="appurl2"></h6>
</a></footer>
</div>
</div>
</div>
</div>
</nav>
<div id="content">
<button type="button" id="sidebarCollapse" class="btn btn-info navbar-btn">
<i class="glyphicon glyphicon-menu-hamburger"></i>
<span>Menu</span>
</button>
<div id="ajaxcontent">
</div>
<div id="destroy" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Factory Reset</h4>
</div>
<div class="alert alert-warning">
<h5><b>Warning!</b> This action <strong>cannot</strong> be undone. This will permanently
delete <strong>all
the settings and logs.</strong> Please make sure you've made a backup before
resetting!</h5>
</div>
<div class="modal-body">
<h5>Please type in the hostname of the device to confirm.</h5>
<div style="clear:both;">
<br>
</div>
<p>
<input type="text" class="form-control input-block" id="compare"
oninput="compareDestroy()">
</p>
</div>
<div class="modal-footer">
<button id="destroybtn" type="button" disabled="" onclick="destroy();"
class="btn btn-block btn-danger">I understand, reset all my settings</button>
</div>
</div>
</div>
</div>
<div id="reboot" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Restart System</h4>
</div>
<div class="alert alert-warning">
<h5><b>Are you sure you want to restart the system?</b></h5>
</div>
<div class="modal-footer">
<button id="restartbtn" type="button" onclick="restart();"
class="btn btn-block btn-danger">Restart</button>
</div>
</div>
</div>
</div>
<div id="signin" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1" role="dialog"
aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Please enter password to login to the system</h4>
</div>
<div class="row">
<br>
<div class="col-md-8 col-md-offset-2">
<div class="login-panel panel panel-default">
<div class="panel-body">
<form role="form" onsubmit="login(); return false">
<fieldset>
<div class="form-group">
<input id="password" class="form-control" placeholder="Password"
name="password" type="password" value="" required=""
title="Please enter the password">
</div>
<button type="submit"
class="btn btn-success btn-md pull-right">Login</button>
</fieldset>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="update" class="modal fade" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Update Firmware</h4>
</div>
<div class="alert alert-warning">
<strong>Warning!</strong> Please make sure you've made a backup first before updating
</div>
<div class="modal-body">
<div>
<div>
<button id="updateb" onclick="switchfirmware()" type="submit" class="btn btn-success btn-sm pull-right">Release</button>
</div>
<div id="onlineupdate">
<h5 id=releasehead></h5>
<div style="clear:both;">
<br>
</div>
<pre id="releasebody">Getting update information from GitHub...</pre>
<div class="pull-right">
<a class="pull-right" id="downloadupdate">
<button type="button" class="btn btn-primary">Download</button>
</a>
</div>
</div>
</div>
<div style="clear:both;">
<br>
</div>
<div>
<div class="form-group">
<input id="binform" onchange="allowUpload();" type="file" name="update"
accept=".bin">
</div>
<div class="pull-right">
<button onclick="upload();" class="btn btn-primary" id="upbtn"
disabled="">Update</button>
</div>
</div>
</div>
<div style="clear:both;">
<br>
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</div>
<footer class="footer">
<div id="commit" class="container"></div>
</footer>
<div class="overlay"></div>
<script src="js/required.js"></script>
<script src="js/myesp.js"></script>
<script>start();</script>
</body>
</html>

View File

@@ -1,519 +0,0 @@
<div id="backupcontent">
<br>
<br>
<div class="row">
<div class="col-sm-6">
<legend>Backup</legend>
<h6 class="text-muted">Please make sure that you make regular backups.</h6>
<div>
<button class="btn btn-link btn-sm" onclick="backupset();">Backup System Settings</button>
<a id="downloadSet" style="display:none"></a>
<button class="btn btn-link btn-sm" onclick="backupCustomSet();">Backup Custom Settings</button>
<a id="downloadCustomSet" style="display:none"></a>
</div>
<br>
<div>
<legend>Restore</legend>
<h6 class="text-muted">Restore system and custom settings.</h6>
<label for="restoreSet" class="btn btn-link btn-sm">Restore System Settings</label>
<input id="restoreSet" type="file" accept="text/json" onchange="restoreSet();" style="display:none;">
<label for="restoreCustomSet" class="btn btn-link btn-sm">Restore Custom Settings</label>
<input id="restoreCustomSet" type="file" accept="text/json" onchange="restoreCustomSet();"
style="display:none;">
</div>
<br>
</div>
</div>
<br>
<br>
</div>
<div id="progresscontent">
<br>
<br>
<div class="container">
<div class="row">
<br>
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Please wait for about 10 seconds while the system restarts...</h3>
</div>
<div class="panel-body">
<div class="progress">
<div id="updateprog" class="progress-bar progress-bar-striped active" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:0%">0%
</div>
</div>
</div>
<div class="panel-footer text-center" id="reconnect" style="display:none;"></div>
</div>
</div>
</div>
</div>
</div>
<div id="progressupload">
<br>
<br>
<div class="container">
<div class="row">
<br>
<div class="col-md-8 col-md-offset-2">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">Please wait for about a minute while the firmware uploads...</h3>
</div>
<div class="panel-body">
<div class="progress">
<div id="updateprog" class="progress-bar progress-bar-striped active" role="progressbar"
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width:0%">0%
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="generalcontent">
<br>
<legend>General Settings</legend>
<h6 class="text-muted">Setup general system settings</h6>
<br>
<div class="row form-group">
<label class="col-xs-3">Admin Password<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Password for logging into the web console"></i></label>
<span class="col-xs-9 col-md-5">
<input class="form-control input-sm" placeholder="Administrator Password" id="adminpwd" type="password">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Host Name<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Unique name of the device, used in MQTT, AP SSID and web/telnet hostname"></i></label>
<span class="col-xs-9 col-md-5">
<input class="form-control input-sm" placeholder="Hostname" id="hostname" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Serial Port<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Enabling Serial port will echo Telnet output to the monitoring console via USB"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="serialenabled">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="serialenabled" checked>Disabled</label>
</form>
</div>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Event Logging<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Enabling logging of all events to a remote SysLog"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="logeventsenabled">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="logeventsenabled" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Event Log Server IP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="IP address of the SysLog server running on UDP port 514"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" value="" style="display:inline;max-width:185px" id="log_ip"
type="text">
</span>
</div>
<br>
<br>
<div class="col-xs-9 col-md-8">
<button onclick="savegeneral()" class="btn btn-primary btn-sm pull-right">Save</button>
</div>
</div>
<div style="clear:both;">
<br>
<br>
</div>
</div>
<br>
</div>
<div id="mqttcontent">
<br>
<legend>MQTT Settings</legend>
<h6 class="text-muted">Set up access to an MQTT broker</h6>
<br>
<div class="row form-group">
<label class="col-xs-3">MQTT<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Enable or Disable MQTT support"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="mqttenabled">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="mqttenabled" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">IP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign" aria-hidden="true"
data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Server IP Address"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="IP address" style="display:inline;max-width:185px"
id="mqttip" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Port<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Server port number (default 1883)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttport" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">QOS<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT QOS 0, 1 or 2 (default 0)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttqos" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Keep Alive<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Keep Alive time in seconds (default 60)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttkeepalive" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Retain<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Retain Flag"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="mqttretain">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="mqttretain" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">Username<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Server username (can be blank)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttuser" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Password<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT Server password (can be blank)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttpwd" type="password">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Base<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="MQTT topic prefix (&lt;mqtt base&gt;/&lt;host name&gt;/)"></i></label>
<span class="col-xs-9">
<input class="form-control input-sm" placeholder="" value="" style="display:inline;max-width:185px"
id="mqttbase" type="text">
</span>
</div>
<div class="row form-group">
<label class="col-xs-3">Heartbeat<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Enable or Disable an automatic MQTT topic publish with system status"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="mqttheartbeat">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" name="mqttheartbeat" checked>Disabled</label>
</form>
</div>
</div>
<br>
<div class="col-xs-9 col-md-8">
<button onclick="savemqtt()" class="btn btn-primary btn-sm pull-right">Save</button>
</div>
</div>
<div id="networkcontent">
<br>
<legend>Wireless Settings</legend>
<h6 class="text-muted">Enter the wireless network settings here, or use Scan to find nearby networks to join
</h6>
<br>
<div class="row form-group">
<label class="col-xs-3">Mode<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="You can run your ESP in AP Mode (non-captive) or join an existing wireless network"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" name="wmode" id="wmodeap" onclick="handleAP();" checked>Access
Point
</label>
<label class="radio-inline">
<input type="radio" value="0" name="wmode" id="wmodesta" onclick="handleSTA();">Client</label>
</form>
</div>
</div>
<div class="row form-group" style="display:none" id="hideclient">
<label class="col-xs-3">SSID<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="WiFi Network Name"></i></label>
<span class="col-xs-7 col-md-5">
<input class="form-control input-sm" id="inputtohide" type="text" name="ap_ssid">
<select class="form-control input-sm" style="display:none;" id="ssid" onchange="listBSSID();"></select>
</span>
<span class="col-xs-2">
<button id="scanb" type="button" class="btn btn-info btn-xs" style="display:none;"
onclick="scanWifi()">Scan...</button>
</span>
<br><br>
<label class="col-xs-3">Password<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="WiFi Password"></i></label>
<span class="col-xs-9 col-md-5">
<input id="wifipass" class="form-control input-sm" name="ap_passwd" type="password">
</span>
<br><br>
<h6 style="margin-left: 10px;" class="text-muted">If you're not using DHCP and want a fixed IP address enter the
values below:</h6>
<br>
<label class="col-xs-3">Static IP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="static IP address"></i></label>
<span class="col-xs-9 col-md-5">
<input type="text" class="form-control input-sm" id="staticip" placeholder="">
</span>
<br><br>
<label class="col-xs-3">Gateway IP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="gateway IP address"></i></label>
<span class="col-xs-9 col-md-5">
<input type="text" class="form-control input-sm" id="gatewayip" placeholder="">
</span>
<br><br>
<label class="col-xs-3">Network Mask<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="network mask"></i></label>
<span class="col-xs-9 col-md-5">
<input type="text" class="form-control input-sm" id="nmask" placeholder="255.255.255.0">
</span>
<br><br>
<label class="col-xs-3">DNS IP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="DNS IP address"></i></label>
<span class="col-xs-9 col-md-5">
<input type="text" class="form-control input-sm" id="dnsip" placeholder="">
</span>
</div>
<br><br>
<div class="col-xs-9 col-md-8">
<button onclick="savenetwork()" class="btn btn-primary btn-sm pull-right">Save</button>
</div>
</div>
<div id="ntpcontent">
<br>
<legend>Time Settings</legend>
<h6 class="text-muted">With Network Time Protocol (NTP) enabled, all times are adjusted to the local timezone
and
respect daylight saving time (DST)</h6>
<br>
<div class="row form-group">
<label class="col-xs-3">Device Time</label>
<span id="utc" class="col-xs-9 col-md-5"></span>
<div class="col-xs-3">
<button onclick="forcentp()" id="forcentp" class="btn btn-info btn-sm">Sync Device with Internet
Time</button>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">NTP<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Enable NTP - requires an internet connection"></i></label>
<div class="col-xs-9">
<form>
<label class="radio-inline">
<input type="radio" value="1" onclick="handleNTPON();" name="ntpenabled">Enabled</label>
<label class="radio-inline">
<input type="radio" value="0" onclick="handleNTPOFF();" name="ntpenabled" checked>Disabled</label>
</form>
</div>
</div>
<div class="row form-group">
<label class="col-xs-3">NTP Server Address<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="The URL or IP of the NTP server. Choose the nearest server for better accuracy (see https://www.ntppool.org)"></i></label>
<span class="col-xs-9 col-md-5">
<input class="form-control input-sm" placeholder="eg. pool.ntp.org" value="pool.ntp.org" id="ntpserver"
type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Recalibrate Time<i style="margin-left: 10px;" class="glyphicon glyphicon-info-sign"
aria-hidden="true" data-toggle="popover" data-trigger="hover" data-placement="right"
data-content="Time in minutes between scheduled NTP refreshes"></i></label>
<span class="col-xs-9 col-md-5">
<input class="form-control input-sm" placeholder="in Minutes" value="30" id="interval" type="text">
</span>
<br>
</div>
<div class="row form-group">
<label class="col-xs-3">Time Zone</label>
<span class="col-xs-9 col-md-5">
<select class="form-control input-sm" name="timzeone" id="timezone">
<option value="0">Australia Eastern (Sydney, Melbourne)</option>
<option value="1">Moscow (MSK, does not observe DST)</option>
<option selected="selected" value="2">Central European Time (Frankfurt, Paris, Amsterdam)</option>
<option value="3">United Kingdom (London, Belfast)</option>
<option value="4">UTC</option>
<option value="5">US Eastern (New York, Detroit)</option>
<option value="6">US Central (Chicago, Houston)</option>
<option value="7">US Mountain (Denver, Salt Lake City)</option>
<option value="8">Arizona (no DST)</option>
<option value="9">US Pacific (Las Vegas, Los Angeles)</option>
<option value="10">Turkey</option>
</select>
</span>
</div>
<br>
<div class="col-xs-9 col-md-8">
<button onclick="saventp()" class="btn btn-primary btn-sm pull-right">Save</button>
</div>
</div>
<div id="statuscontent">
<br>
<div class="row text-left">
<div class="col-md-8 col-md-offset-2">
<h2>System Status</h2>
<h6>Current health of the ESP is displayed here</h6>
<div class="panel panel-default table-responsive">
<table class="table table-hover table-striped table-condensed">
<caption>General</caption>
<tr>
<th>Uptime</th>
<td id="uptime"></td>
</tr>
<tr>
<th>System Load</th>
<td id="systemload"> %</td>
</tr>
</table>
</div>
<div class="panel panel-default table-responsive">
<table class="table table-hover table-striped table-condensed">
<caption>Storage</caption>
<tr>
<th>Free Heap</th>
<td>
<div class="progress" style="margin-bottom: 0 !important;">
<div id="heap" class="progress-bar progress-bar-primary" role="progressbar">
Progress
</div>
</div>
</td>
</tr>
<tr>
<th>Free Flash</th>
<td>
<div class='progress' style="margin-bottom: 0 !important;">
<div id="flash" class="progress-bar progress-bar-primary" role="progressbar">
Progress
</div>
</div>
</td>
</tr>
<tr>
<th>Free SPIFFS</th>
<td>
<div class='progress' style="margin-bottom: 0 !important;">
<div id="spiffs" class="progress-bar progress-bar-primary" role="progressbar">
Progress
</div>
</div>
</td>
</tr>
</table>
</div>
<div class="panel panel-default table-responsive">
<table class="table table-hover table-striped table-condensed">
<caption>Wireless Network</caption>
<tr>
<th>SSID</th>
<td id="ssidstat"></td>
</tr>
<tr>
<th>IP Address</th>
<td id="ip"></td>
</tr>
<tr>
<th>MAC Address</th>
<td id="mac"></td>
</tr>
<tr>
<th>Signal Strength</th>
<td id="signalstr"> %</td>
</tr>
</table>
</div>
<div class="panel panel-default table-responsive">
<table class="table table-hover table-striped table-condensed" border=1>
<caption>MQTT&nbsp;&nbsp;<div id="mqttconnected"></div>&nbsp;<div id="mqttheartbeat"></div>
</caption>
</table>
</div>
</div>
</div>
<div class="row form-group" style="text-align: center;">
<button onclick="refreshStatus()" class="btn btn-info">Refresh</button>
</div>
<br>
</div>

View File

@@ -1,786 +0,0 @@
var version = "";
var websock = null;
var wsUri = "ws://" + window.location.host + "/ws";
var ntpSeconds;
var ajaxobj;
var custom_config = {};
var xDown = null;
var yDown = null;
var file = {};
var backupstarted = false;
var updateurl = "";
var updateurl_dev = "";
var use_beta_firmware = false;
var myespcontent;
var formData = new FormData();
var nextIsNotJson = false;
var config = {};
function deviceTime() {
var t = new Date(0); // The 0 there is the key, which sets the date to the epoch
t.setUTCSeconds(ntpSeconds);
document.getElementById("utc").innerHTML = t.toUTCString().slice(0, -3);
}
function handleNTPON() {
document.getElementById("forcentp").style.display = "block";
}
function handleNTPOFF() {
document.getElementById("forcentp").style.display = "none";
}
function listntp() {
websock.send("{\"command\":\"gettime\"}");
document.getElementById("ntpserver").value = config.ntp.server;
document.getElementById("interval").value = config.ntp.interval;
document.getElementById("timezone").value = config.ntp.timezone;
if (config.ntp.enabled) {
$("input[name=\"ntpenabled\"][value=\"1\"]").prop("checked", true);
handleNTPON();
} else {
handleNTPOFF();
}
browserTime();
deviceTime();
}
function home() {
window.location = '/';
}
function restart_alert() {
$("#commit").fadeOut(200, function () {
$(this).css("background", "gold").fadeIn(1000);
});
document.getElementById("commit").innerHTML = "<h6>Settings have changed. It's recommended to reboot the system. Click here to restart.</h6>";
$("#commit").click(function () {
$("#reboot").modal("show");
return false;
});
}
function saveconfig() {
websock.send(JSON.stringify(config));
restart_alert();
}
function custom_saveconfig() {
websock.send(JSON.stringify(custom_config));
restart_alert();
}
function saventp() {
config.ntp.server = document.getElementById("ntpserver").value;
config.ntp.interval = parseInt(document.getElementById("interval").value);
config.ntp.timezone = parseInt(document.getElementById("timezone").value);
config.ntp.enabled = false;
if (parseInt($("input[name=\"ntpenabled\"]:checked").val()) === 1) {
config.ntp.enabled = true;
}
saveconfig();
}
function forcentp() {
websock.send("{\"command\":\"forcentp\"}");
}
function savegeneral() {
var a = document.getElementById("adminpwd").value;
if (a === null || a === "") {
alert("Administrator password cannot be empty");
return;
}
config.general.password = a;
config.general.hostname = document.getElementById("hostname").value;
config.general.serial = false;
if (parseInt($("input[name=\"serialenabled\"]:checked").val()) === 1) {
config.general.serial = true;
}
config.general.log_events = false;
if (parseInt($("input[name=\"logeventsenabled\"]:checked").val()) === 1) {
config.general.log_events = true;
}
config.general.log_ip = document.getElementById("log_ip").value;
saveconfig();
}
function savemqtt() {
config.mqtt.enabled = false;
if (parseInt($("input[name=\"mqttenabled\"]:checked").val()) === 1) {
config.mqtt.enabled = true;
}
config.mqtt.heartbeat = false;
if (parseInt($("input[name=\"mqttheartbeat\"]:checked").val()) === 1) {
config.mqtt.heartbeat = true;
}
config.mqtt.retain = false;
if (parseInt($("input[name=\"mqttretain\"]:checked").val()) === 1) {
config.mqtt.retain = true;
}
config.mqtt.ip = document.getElementById("mqttip").value;
config.mqtt.port = parseInt(document.getElementById("mqttport").value);
config.mqtt.qos = parseInt(document.getElementById("mqttqos").value);
config.mqtt.keepalive = parseInt(document.getElementById("mqttkeepalive").value);
config.mqtt.base = document.getElementById("mqttbase").value;
config.mqtt.user = document.getElementById("mqttuser").value;
config.mqtt.password = document.getElementById("mqttpwd").value;
saveconfig();
}
function savenetwork() {
var wmode = 0;
if (document.getElementById("inputtohide").style.display === "none") {
var b = document.getElementById("ssid");
config.network.ssid = b.options[b.selectedIndex].value;
} else {
config.network.ssid = document.getElementById("inputtohide").value;
}
if (document.getElementById("wmodeap").checked) {
wmode = 1;
}
config.network.wmode = wmode;
config.network.password = document.getElementById("wifipass").value;
config.network.staticip = document.getElementById("staticip").value;
config.network.gatewayip = document.getElementById("gatewayip").value;
config.network.nmask = document.getElementById("nmask").value;
config.network.dnsip = document.getElementById("dnsip").value;
saveconfig();
}
function inProgress(callback) {
$("body").load("myesp.html #progresscontent", function (responseTxt, statusTxt, xhr) {
if (statusTxt === "success") {
$(".progress").css("height", "40");
$(".progress").css("font-size", "xx-large");
var i = 0;
var prg = setInterval(function () {
$(".progress-bar").css("width", i + "%").attr("aria-valuenow", i).html(i + "%");
i = i + 5;
if (i === 105) {
clearInterval(prg);
var a = document.createElement("a");
a.href = "http://" + config.general.hostname + ".local";
a.innerText = "Re-connect...";
document.getElementById("reconnect").appendChild(a);
document.getElementById("reconnect").style.display = "block";
document.getElementById("updateprog").className = "progress-bar progress-bar-success";
document.getElementById("updateprog").innerHTML = "Completed";
}
}, 500);
switch (callback) {
case "upload":
$.ajax({
url: "/update",
type: "POST",
data: formData,
processData: false,
contentType: false
});
break;
case "destroy":
websock.send("{\"command\":\"destroy\"}");
break;
case "restart":
websock.send("{\"command\":\"restart\"}");
break;
default:
break;
}
}
}).hide().fadeIn();
}
function inProgressUpload() {
$("body").load("myesp.html #progressupload", function (responseTxt, statusTxt, xhr) {
if (statusTxt === "success") {
$(".progress").css("height", "40");
$(".progress").css("font-size", "xx-large");
var i = 0;
var prg = setInterval(function () {
$(".progress-bar").css("width", i + "%").attr("aria-valuenow", i).html(i + "%");
i = i + 1;
if (i === 101) {
clearInterval(prg);
document.getElementById("updateprog").className = "progress-bar progress-bar-success";
document.getElementById("updateprog").innerHTML = "Completed";
}
}, 500);
$.ajax({
url: "/update",
type: "POST",
data: formData,
processData: false,
contentType: false
});
}
}).hide().fadeIn();
}
function handleSTA() {
document.getElementById("scanb").style.display = "block";
document.getElementById("hideclient").style.display = "block";
}
function handleAP() {
document.getElementById("ssid").style.display = "none";
document.getElementById("scanb").style.display = "none";
document.getElementById("hideclient").style.display = "none";
document.getElementById("inputtohide").style.display = "block";
}
function listnetwork() {
document.getElementById("inputtohide").value = config.network.ssid;
document.getElementById("wifipass").value = config.network.password;
if (config.network.wmode === 1) {
document.getElementById("wmodeap").checked = true;
handleAP();
} else {
document.getElementById("wmodesta").checked = true;
handleSTA();
}
document.getElementById("staticip").value = config.network.staticip;
document.getElementById("gatewayip").value = config.network.gatewayip;
document.getElementById("nmask").value = config.network.nmask;
document.getElementById("dnsip").value = config.network.dnsip;
}
function listgeneral() {
document.getElementById("adminpwd").value = config.general.password;
document.getElementById("hostname").value = config.general.hostname;
if (config.general.serial) {
$("input[name=\"serialenabled\"][value=\"1\"]").prop("checked", true);
}
if (config.general.log_events) {
$("input[name=\"logeventsenabled\"][value=\"1\"]").prop("checked", true);
}
document.getElementById("log_ip").value = config.general.log_ip;
}
function listmqtt() {
if (config.mqtt.enabled) {
$("input[name=\"mqttenabled\"][value=\"1\"]").prop("checked", true);
}
if (config.mqtt.heartbeat) {
$("input[name=\"mqttheartbeat\"][value=\"1\"]").prop("checked", true);
}
if (config.mqtt.retain) {
$("input[name=\"mqttretain\"][value=\"1\"]").prop("checked", true);
}
document.getElementById("mqttip").value = config.mqtt.ip;
document.getElementById("mqttport").value = config.mqtt.port;
document.getElementById("mqttqos").value = config.mqtt.qos;
document.getElementById("mqttkeepalive").value = config.mqtt.keepalive;
document.getElementById("mqttbase").value = config.mqtt.base;
document.getElementById("mqttuser").value = config.mqtt.user;
document.getElementById("mqttpwd").value = config.mqtt.password;
}
function listBSSID() {
var select = document.getElementById("ssid");
document.getElementById("wifibssid").value = select.options[select.selectedIndex].bssidvalue;
}
function listSSID(obj) {
var select = document.getElementById("ssid");
for (var i = 0; i < obj.list.length; i++) {
var x = parseInt(obj.list[i].rssi);
var percentage = Math.min(Math.max(2 * (x + 100), 0), 100);
var opt = document.createElement("option");
opt.value = obj.list[i].ssid;
opt.bssidvalue = obj.list[i].bssid;
opt.innerHTML = "BSSID: " + obj.list[i].bssid + ", Signal Strength: %" + percentage + ", Network: " + obj.list[i].ssid;
select.appendChild(opt);
}
document.getElementById("scanb").innerHTML = "Re-scan...";
listBSSID();
}
function scanWifi() {
websock.send("{\"command\":\"scan\"}");
document.getElementById("scanb").innerHTML = "...";
document.getElementById("inputtohide").style.display = "none";
var node = document.getElementById("ssid");
node.style.display = "inline";
while (node.hasChildNodes()) {
node.removeChild(node.lastChild);
}
}
function isVisible(e) {
return !!(e.offsetWidth || e.offsetHeight || e.getClientRects().length);
}
function colorStatusbar(ref) {
var percentage = ref.style.width.slice(0, -1);
if (percentage > 50) { ref.className = "progress-bar progress-bar-success"; } else if (percentage > 25) { ref.className = "progress-bar progress-bar-warning"; } else { ref.class = "progress-bar progress-bar-danger"; }
}
function listStats() {
document.getElementById("uptime").innerHTML = ajaxobj.uptime;
document.getElementById("heap").innerHTML = ajaxobj.heap + " bytes";
document.getElementById("heap").style.width = (ajaxobj.heap * 100) / ajaxobj.initheap + "%";
colorStatusbar(document.getElementById("heap"));
document.getElementById("flash").innerHTML = ajaxobj.availsize + " KB";
document.getElementById("flash").style.width = (ajaxobj.availsize * 100) / (ajaxobj.availsize + ajaxobj.sketchsize) + "%";
colorStatusbar(document.getElementById("flash"));
document.getElementById("spiffs").innerHTML = ajaxobj.availspiffs + " KB";
document.getElementById("spiffs").style.width = (ajaxobj.availspiffs * 100) / ajaxobj.spiffssize + "%";
colorStatusbar(document.getElementById("spiffs"));
document.getElementById("ssidstat").innerHTML = ajaxobj.ssid;
document.getElementById("ip").innerHTML = ajaxobj.ip;
document.getElementById("mac").innerHTML = ajaxobj.mac;
document.getElementById("signalstr").innerHTML = ajaxobj.signalstr + " %";
document.getElementById("systemload").innerHTML = ajaxobj.systemload + " %";
if (ajaxobj.mqttconnected) {
document.getElementById("mqttconnected").innerHTML = "MQTT is connected";
document.getElementById("mqttconnected").className = "label label-success";
} else {
document.getElementById("mqttconnected").innerHTML = "MQTT is not connected";
document.getElementById("mqttconnected").className = "label label-danger";
}
if (ajaxobj.mqttheartbeat) {
document.getElementById("mqttheartbeat").innerHTML = "MQTT hearbeat is enabled";
document.getElementById("mqttheartbeat").className = "label label-success";
} else {
document.getElementById("mqttheartbeat").innerHTML = "MQTT hearbeat is disabled";
document.getElementById("mqttheartbeat").className = "label label-primary";
}
}
function getContent(contentname) {
$("#dismiss").click();
$(".overlay").fadeOut().promise().done(function () {
var content = $(contentname).html();
$("#ajaxcontent").html(content).promise().done(function () {
switch (contentname) {
case "#statuscontent":
listStats();
break;
case "#backupcontent":
break;
case "#ntpcontent":
listntp();
break;
case "#mqttcontent":
listmqtt();
break;
case "#generalcontent":
listgeneral();
break;
case "#networkcontent":
listnetwork();
break;
case "#customcontent":
listcustom();
break;
case "#custom_statuscontent":
var version = "version " + ajaxobj.version;
$("#mainver").text(version);
$("#customname").text(ajaxobj.customname);
var customname2 = " " + ajaxobj.customname;
$("#customname2").text(customname2);
var elem;
if (config.network.wmode === 0) {
elem = document.getElementById("helpurl");
var helpurl = ajaxobj.appurl + "/wiki";
elem.setAttribute("href", helpurl);
document.getElementById("helpurl").style.display = "block";
} else {
document.getElementById("helpurl").style.display = "none";
}
elem = document.getElementById("appurl");
elem.setAttribute("href", ajaxobj.appurl);
$("#appurl2").text(ajaxobj.appurl);
updateurl = ajaxobj.updateurl;
updateurl_dev = ajaxobj.updateurl_dev;
listCustomStats();
break;
default:
break;
}
$("[data-toggle=\"popover\"]").popover({
container: "body"
});
$(this).hide().fadeIn();
});
});
}
function backupset() {
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(config, null, 2));
var dlAnchorElem = document.getElementById("downloadSet");
dlAnchorElem.setAttribute("href", dataStr);
dlAnchorElem.setAttribute("download", "system_config.json");
dlAnchorElem.click();
}
function backupCustomSet() {
var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(custom_config, null, 2));
var dlAnchorElem = document.getElementById("downloadCustomSet");
dlAnchorElem.setAttribute("href", dataStr);
dlAnchorElem.setAttribute("download", "custom_config.json");
dlAnchorElem.click();
}
function restoreSet() {
var input = document.getElementById("restoreSet");
var reader = new FileReader();
if ("files" in input) {
if (input.files.length === 0) {
alert("You did not select file to restore!");
} else {
reader.onload = function () {
var json;
try {
json = JSON.parse(reader.result);
} catch (e) {
alert("Not a valid backup file!");
return;
}
if (json.command === "configfile") {
var x = confirm("System Config file seems to be valid, do you wish to continue?");
if (x) {
config = json;
saveconfig();
}
}
};
reader.readAsText(input.files[0]);
}
}
}
function restoreCustomSet() {
var input = document.getElementById("restoreCustomSet");
var reader = new FileReader();
if ("files" in input) {
if (input.files.length === 0) {
alert("You did not select file to restore!");
} else {
reader.onload = function () {
var json;
try {
json = JSON.parse(reader.result);
} catch (e) {
alert("Not a valid backup file!");
return;
}
if (json.command === "custom_configfile") {
var x = confirm("Custom Config file seems to be valid, do you wish to continue?");
if (x) {
custom_config = json;
custom_saveconfig();
}
}
};
reader.readAsText(input.files[0]);
}
}
}
function twoDigits(value) {
if (value < 10) {
return "0" + value;
}
return value;
}
function socketMessageListener(evt) {
var obj = JSON.parse(evt.data);
if (obj.hasOwnProperty("command")) {
switch (obj.command) {
case "status":
ajaxobj = obj;
getContent("#statuscontent");
break;
case "custom_settings":
ajaxobj = obj;
break;
case "custom_status":
ajaxobj = obj;
getContent("#custom_statuscontent");
break;
case "gettime":
ntpSeconds = obj.epoch;
deviceTime();
break;
case "ssidlist":
listSSID(obj);
break;
case "configfile":
config = obj;
break;
case "custom_configfile":
custom_config = obj;
break;
default:
break;
}
}
}
function compareDestroy() {
if (config.general.hostname === document.getElementById("compare").value) {
$("#destroybtn").prop("disabled", false);
} else { $("#destroybtn").prop("disabled", true); }
}
function destroy() {
inProgress("destroy");
}
function restart() {
inProgress("restart");
}
function handleTouchStart(evt) {
xDown = evt.touches[0].clientX;
yDown = evt.touches[0].clientY;
}
function handleTouchMove(evt) {
if (!xDown || !yDown) {
return;
}
var xUp = evt.touches[0].clientX;
var yUp = evt.touches[0].clientY;
var xDiff = xDown - xUp;
var yDiff = yDown - yUp;
if (Math.abs(xDiff) > Math.abs(yDiff)) { /*most significant*/
if (xDiff > 0) {
$("#dismiss").click();
} else {
$("#sidebarCollapse").click();
/* right swipe */
}
} else {
if (yDiff > 0) {
/* up swipe */
} else {
/* down swipe */
}
}
/* reset values */
xDown = null;
yDown = null;
}
function logout() {
jQuery.ajax({
type: "GET",
url: "/login",
async: false,
username: "logmeout",
password: "logmeout",
})
.done(function () {
// If we don"t get an error, we actually got an error as we expect an 401!
})
.fail(function () {
// We expect to get an 401 Unauthorized error! In this case we are successfully
// logged out and we redirect the user.
document.location = "index.html";
});
return false;
}
function connectWS() {
if (window.location.protocol === "https:") {
wsUri = "wss://" + window.location.host + "/ws";
} else if (window.location.protocol === "file:") {
wsUri = "ws://" + "localhost" + "/ws";
}
websock = new WebSocket(wsUri);
websock.addEventListener("message", socketMessageListener);
websock.onopen = function (evt) {
websock.send("{\"command\":\"getconf\"}");
websock.send("{\"command\":\"custom_status\"}");
};
}
function upload() {
formData.append("bin", $("#binform")[0].files[0]);
inProgressUpload();
}
function login() {
if (document.getElementById("password").value === "neo") {
$("#signin").modal("hide");
connectWS();
} else {
var username = "admin";
var password = document.getElementById("password").value;
var url = "/login";
var xhr = new XMLHttpRequest();
xhr.open("get", url, true, username, password);
xhr.onload = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
$("#signin").modal("hide");
connectWS();
} else {
alert("Incorrect password!");
}
}
};
xhr.send(null);
}
}
function getfirmware() {
if (use_beta_firmware) {
use_beta_firmware = false;
document.getElementById("updateb").innerHTML = "Switch to Development build";
} else {
use_beta_firmware = true;
document.getElementById("updateb").innerHTML = "Switch to Stable release";
}
getLatestReleaseInfo();
}
function getLatestReleaseInfo() {
if (use_beta_firmware) {
var url = updateurl_dev;
} else {
var url = updateurl;
}
$.getJSON(url).done(function (release) {
var asset = release.assets[0];
var downloadCount = 0;
for (var i = 0; i < release.assets.length; i++) {
downloadCount += release.assets[i].download_count;
}
var oneHour = 60 * 60 * 1000;
var oneDay = 24 * oneHour;
var dateDiff = new Date() - new Date(release.published_at);
var timeAgo;
if (dateDiff < oneDay) {
timeAgo = (dateDiff / oneHour).toFixed(1) + " hours ago";
} else {
timeAgo = (dateDiff / oneDay).toFixed(1) + " days ago";
}
var releaseInfo = release.name + " was updated " + timeAgo + " and downloaded " + downloadCount.toLocaleString() + " times.";
$("#downloadupdate").attr("href", asset.browser_download_url);
$("#releasehead").text(releaseInfo);
$("#releasebody").text(release.body);
$("#releaseinfo").fadeIn("slow");
}).error(function () { $("#onlineupdate").html("<h5>Couldn't get release details. Make sure there is an Internet connection.</h5>"); });
}
function allowUpload() {
$("#upbtn").prop("disabled", false);
}
function start() {
myespcontent = document.createElement("div");
myespcontent.id = "mastercontent";
myespcontent.style.display = "none";
document.body.appendChild(myespcontent);
$("#signin").on("shown.bs.modal", function () {
$("#password").focus().select();
});
$("#mastercontent").load("myesp.html", function (responseTxt, statusTxt, xhr) {
if (statusTxt === "success") {
$("#signin").modal({ backdrop: "static", keyboard: false });
$("[data-toggle=\"popover\"]").popover({
container: "body"
});
}
});
}
function refreshCustomStatus() {
websock.send("{\"command\":\"custom_status\"}");
}
function refreshStatus() {
websock.send("{\"command\":\"status\"}");
}
$("#dismiss, .overlay").on("click", function () {
$("#sidebar").removeClass("active");
$(".overlay").fadeOut();
});
$("#sidebarCollapse").on("click", function () {
$("#sidebar").addClass("active");
$(".overlay").fadeIn();
$(".collapse.in").toggleClass("in");
$("a[aria-expanded=true]").attr("aria-expanded", "false");
});
$("#custom_status").click(function () { websock.send("{\"command\":\"custom_status\"}"); return false; });
$("#status").click(function () { websock.send("{\"command\":\"status\"}"); return false; });
$("#custom").click(function () { getContent("#customcontent"); return false; });
$("#network").on("click", (function () { getContent("#networkcontent"); return false; }));
$("#general").click(function () { getContent("#generalcontent"); return false; });
$("#mqtt").click(function () { getContent("#mqttcontent"); return false; });
$("#ntp").click(function () { getContent("#ntpcontent"); return false; });
$("#backup").click(function () { getContent("#backupcontent"); return false; });
$("#reset").click(function () { $("#destroy").modal("show"); return false; });
$("#restart").click(function () { $("#reboot").modal("show"); return false; });
$(".noimp").on("click", function () { $("#noimp").modal("show"); });
$("#update").on("shown.bs.modal", function (e) { getfirmware(); });
document.addEventListener("touchstart", handleTouchStart, false);
document.addEventListener("touchmove", handleTouchMove, false);