refactor device value rendering (to Web, Console or MQTT) to base class #632

This commit is contained in:
proddy
2020-12-13 22:52:34 +01:00
parent f72e549850
commit ffa313ebe4
60 changed files with 2579 additions and 3367 deletions

View File

@@ -12,6 +12,7 @@
- WM10 switch telegrams
- boiler information (#633)
- maintenance message and command
- thermostat program, reducemode, controlmode
### Fixed
- mixer IPM pumpstatus

View File

@@ -106,17 +106,6 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
onChange={handleValueChange('max_topic_length')}
margin="normal"
/>
<SelectValidator name="mqtt_format"
label="Format"
value={data.mqtt_format}
fullWidth
variant="outlined"
onChange={handleValueChange('mqtt_format')}
margin="normal">
<MenuItem value={1}>Single</MenuItem>
<MenuItem value={2}>Nested</MenuItem>
<MenuItem value={3}>Home Assistant</MenuItem>
</SelectValidator>
<SelectValidator name="mqtt_qos"
label="QoS"
value={data.mqtt_qos}
@@ -149,6 +138,43 @@ class MqttSettingsForm extends React.Component<MqttSettingsFormProps> {
label="Retain Flag"
/>
<br></br>
<Typography variant="h6" color="primary" >
Formatting
</Typography>
<SelectValidator name="dallas_format"
label="Dallas Sensor Payload Grouping"
value={data.dallas_format}
fullWidth
variant="outlined"
onChange={handleValueChange('dallas_format')}
margin="normal">
<MenuItem value={1}>by Sensor ID</MenuItem>
<MenuItem value={2}>by Number</MenuItem>
</SelectValidator>
<BlockFormControlLabel
control={
<Checkbox
checked={data.ha_enabled}
onChange={handleValueChange('ha_enabled')}
value="ha_enabled"
/>
}
label="Home Assistant MQTT Discovery"
/>
{ data.ha_enabled &&
<SelectValidator name="ha_climate_format"
label="Thermostat Room Temperature"
value={data.ha_climate_format}
fullWidth
variant="outlined"
onChange={handleValueChange('ha_climate_format')}
margin="normal">
<MenuItem value={1}>use Current temperature (default)</MenuItem>
<MenuItem value={2}>use Setpoint temperature</MenuItem>
<MenuItem value={3}>Fix to 0</MenuItem>
</SelectValidator>
}
<br></br>
<Typography variant="h6" color="primary" >
Publish Intervals
</Typography>

View File

@@ -33,7 +33,9 @@ export interface MqttSettings {
publish_time_mixer: number;
publish_time_other: number;
publish_time_sensor: number;
mqtt_format: number;
dallas_format: number;
mqtt_qos: number;
mqtt_retain: boolean;
ha_enabled: boolean;
ha_climate_format: number;
}

View File

@@ -342,13 +342,13 @@ class EMSESPDevicesForm extends Component<
<TableHead></TableHead>
<TableBody>
{deviceData.data.map((item, i) => {
if (i % 2) {
if (i % 3) {
return null;
} else {
return (
<TableRow key={i}>
<TableCell component="th" scope="row">{deviceData.data[i]}</TableCell>
<TableCell align="right">{deviceData.data[i + 1]}</TableCell>
<TableCell component="th" scope="row">{deviceData.data[i+2]}</TableCell>
<TableCell align="right">{deviceData.data[i]}{deviceData.data[i + 1]}</TableCell>
</TableRow>
);
}

View File

@@ -60,30 +60,16 @@ class EMSESPStatusForm extends Component<EMSESPStatusFormProps> {
<TableBody>
<TableRow>
<TableCell>
Received telegrams
# Telegrams Received
</TableCell>
<TableCell align="right">{formatNumber(data.rx_received)}
<TableCell align="right">{formatNumber(data.rx_received)}&nbsp;({data.rx_quality}%)
</TableCell>
</TableRow>
<TableRow>
<TableCell >
Rx line quality
</TableCell>
<TableCell align="right">{data.rx_quality}&nbsp;%
</TableCell>
</TableRow>
<TableRow>
<TableCell >
Sent telegrams
# Telegrams Sent
</TableCell >
<TableCell align="right">{formatNumber(data.tx_sent)}
</TableCell>
</TableRow>
<TableRow>
<TableCell >
Tx line quality
</TableCell>
<TableCell align="right">{data.tx_quality}&nbsp;%
<TableCell align="right">{formatNumber(data.tx_sent)}&nbsp;({data.tx_quality}%)
</TableCell>
</TableRow>
</TableBody>

View File

@@ -3,7 +3,7 @@ import { Redirect, Switch } from 'react-router';
import { AuthenticatedRoute } from '../authentication';
import EMSESP from './EMSESP';
import EMSESPDashboard from './EMSESPDashboard';
import EMSESPSettings from './EMSESPSettings';
class ProjectRouting extends Component {
@@ -11,9 +11,9 @@ class ProjectRouting extends Component {
render() {
return (
<Switch>
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESP} />
<AuthenticatedRoute exact path="/ems-esp/status/*" component={EMSESPDashboard} />
<AuthenticatedRoute exact path="/ems-esp/settings" component={EMSESPSettings} />
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESP} />
<AuthenticatedRoute exact path="/ems-esp/*" component={EMSESPDashboard} />
{
/*
* The redirect below caters for the default project route and redirecting invalid paths.

View File

@@ -188,9 +188,11 @@ void MqttSettings::read(MqttSettings & settings, JsonObject & root) {
root["publish_time_mixer"] = settings.publish_time_mixer;
root["publish_time_other"] = settings.publish_time_other;
root["publish_time_sensor"] = settings.publish_time_sensor;
root["mqtt_format"] = settings.mqtt_format;
root["mqtt_qos"] = settings.mqtt_qos;
root["mqtt_retain"] = settings.mqtt_retain;
root["dallas_format"] = settings.dallas_format;
root["ha_climate_format"] = settings.ha_climate_format;
root["ha_enabled"] = settings.ha_enabled;
}
StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & settings) {
@@ -205,6 +207,8 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
newSettings.keepAlive = root["keep_alive"] | FACTORY_MQTT_KEEP_ALIVE;
newSettings.cleanSession = root["clean_session"] | FACTORY_MQTT_CLEAN_SESSION;
newSettings.maxTopicLength = root["max_topic_length"] | FACTORY_MQTT_MAX_TOPIC_LENGTH;
newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS;
newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN;
newSettings.publish_time_boiler = root["publish_time_boiler"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_thermostat = root["publish_time_thermostat"] | EMSESP_DEFAULT_PUBLISH_TIME;
@@ -212,16 +216,25 @@ StateUpdateResult MqttSettings::update(JsonObject & root, MqttSettings & setting
newSettings.publish_time_mixer = root["publish_time_mixer"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_other = root["publish_time_other"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.publish_time_sensor = root["publish_time_sensor"] | EMSESP_DEFAULT_PUBLISH_TIME;
newSettings.mqtt_format = root["mqtt_format"] | EMSESP_DEFAULT_MQTT_FORMAT;
newSettings.mqtt_qos = root["mqtt_qos"] | EMSESP_DEFAULT_MQTT_QOS;
newSettings.mqtt_retain = root["mqtt_retain"] | EMSESP_DEFAULT_MQTT_RETAIN;
newSettings.dallas_format = root["dallas_format"] | EMSESP_DEFAULT_DALLAS_FORMAT;
newSettings.ha_climate_format = root["ha_climate_format"] | EMSESP_DEFAULT_HA_CLIMATE_FORMAT;
newSettings.ha_enabled = root["ha_enabled"] | EMSESP_DEFAULT_HA_ENABLED;
if (newSettings.mqtt_qos != settings.mqtt_qos) {
emsesp::EMSESP::mqtt_.set_qos(newSettings.mqtt_qos);
}
if (newSettings.mqtt_format != settings.mqtt_format) {
emsesp::EMSESP::mqtt_.set_format(newSettings.mqtt_format);
if (newSettings.dallas_format != settings.dallas_format) {
emsesp::EMSESP::mqtt_.dallas_format(newSettings.dallas_format);
}
if (newSettings.ha_climate_format != settings.ha_climate_format) {
emsesp::EMSESP::mqtt_.ha_climate_format(newSettings.ha_climate_format);
}
if (newSettings.ha_enabled != settings.ha_enabled) {
emsesp::EMSESP::mqtt_.ha_enabled(newSettings.ha_enabled);
}
if (newSettings.mqtt_retain != settings.mqtt_retain) {

View File

@@ -60,9 +60,11 @@ static String generateClientId() {
#define FACTORY_MQTT_MAX_TOPIC_LENGTH 128
#endif
#define EMSESP_DEFAULT_MQTT_FORMAT 2 // nested
#define EMSESP_DEFAULT_DALLAS_FORMAT 1 // sensorid
#define EMSESP_DEFAULT_HA_CLIMATE_FORMAT 1 // current temp
#define EMSESP_DEFAULT_MQTT_QOS 0
#define EMSESP_DEFAULT_MQTT_RETAIN false
#define EMSESP_DEFAULT_HA_ENABLED false
#define EMSESP_DEFAULT_PUBLISH_TIME 10
class MqttSettings {
@@ -91,9 +93,11 @@ class MqttSettings {
uint16_t publish_time_mixer;
uint16_t publish_time_other;
uint16_t publish_time_sensor;
uint8_t mqtt_format; // 1=single, 2=nested, 3=ha, 4=custom
uint8_t mqtt_qos;
bool mqtt_retain;
uint8_t dallas_format;
uint8_t ha_climate_format;
bool ha_enabled;
static void read(MqttSettings & settings, JsonObject & root);
static StateUpdateResult update(JsonObject & root, MqttSettings & settings);

View File

@@ -0,0 +1,71 @@
/*
* EMS-ESP - https://github.com/proddy/EMS-ESP
* Copyright 2020 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 <uuid/common.h>
// #ifdef ESP8266
// #include <pgmspace.h>
// #else
// #include <avr/pgmspace.h>
// #endif
#include <Arduino.h>
namespace uuid {
// On ESP8266, pgm_read_byte() already takes care of 4-byte alignment, and
// memcpy_P(s, p, 4) makes 4 calls to pgm_read_byte() anyway, so don't bother
// optimizing for 4-byte alignment here.
// class __FlashStringHelper;
int compare_flash_string(const __FlashStringHelper * a, const __FlashStringHelper * b) {
const char * aa = reinterpret_cast<const char *>(a);
const char * bb = reinterpret_cast<const char *>(b);
while (true) {
uint8_t ca = pgm_read_byte(aa);
uint8_t cb = pgm_read_byte(bb);
if (ca != cb)
return (int)ca - (int)cb;
if (ca == 0)
return 0;
aa++;
bb++;
}
}
int compare_flash_string(const __FlashStringHelper * a, const __FlashStringHelper * b, size_t n) {
const char * aa = reinterpret_cast<const char *>(a);
const char * bb = reinterpret_cast<const char *>(b);
while (n > 0) {
uint8_t ca = pgm_read_byte(aa);
uint8_t cb = pgm_read_byte(bb);
if (ca != cb)
return (int)ca - (int)cb;
if (ca == 0)
return 0;
aa++;
bb++;
n--;
}
return 0;
}
} // namespace uuid

View File

@@ -16,6 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// compare_flash_string added by Proddy
#ifndef UUID_COMMON_H_
#define UUID_COMMON_H_
@@ -32,6 +34,21 @@
*/
namespace uuid {
/**
* String compare two flash strings
*
* The flash string must be stored with appropriate alignment for
* reading it on the platform.
*
* @param[in] a Pointer to string stored in flash.
* @param[in] b Pointer to string stored in flash.
* @param[in] n optional max length
* @return 0 for match, otherwise diff
* @since 1.0.0
*/
int compare_flash_string(const __FlashStringHelper * a, const __FlashStringHelper * b);
int compare_flash_string(const __FlashStringHelper * a, const __FlashStringHelper * b, size_t n);
/**
* Read a string from flash and convert it to a std::string.
*

View File

@@ -13,22 +13,27 @@
class DummySettings {
public:
uint8_t tx_mode = 1;
uint8_t ems_bus_id = 0x0B;
bool syslog_enabled = false;
int8_t syslog_level = 3; // uuid::log::Level
uint32_t syslog_mark_interval = 0;
String syslog_host = "192.168.1.4";
uint8_t master_thermostat = 0;
bool shower_timer = false;
bool shower_alert = false;
bool hide_led = false;
bool api_enabled = true;
uint16_t publish_time = 10; // seconds
uint8_t mqtt_format = 3; // 1=single, 2=nested, 3=ha, 4=custom
uint8_t mqtt_qos = 0;
bool mqtt_retain = false;
bool enabled = true; // MQTT
uint8_t tx_mode = 1;
uint8_t ems_bus_id = 0x0B;
bool syslog_enabled = false;
int8_t syslog_level = 3; // uuid::log::Level
uint32_t syslog_mark_interval = 0;
String syslog_host = "192.168.1.4";
uint8_t master_thermostat = 0;
bool shower_timer = true;
bool shower_alert = false;
bool hide_led = false;
bool api_enabled = true;
// MQTT
uint16_t publish_time = 10; // seconds
uint8_t mqtt_qos = 0;
bool mqtt_retain = false;
bool enabled = true;
uint8_t dallas_format = 1;
uint8_t ha_climate_format = 1;
bool ha_enabled = false;
String hostname = "ems-esp";
String jwtSecret = "ems-esp";
String ssid = "ems-esp";

View File

@@ -28,7 +28,7 @@ WebAPIService::WebAPIService(AsyncWebServer * server) {
server->on(EMSESP_API_SERVICE_PATH, HTTP_GET, std::bind(&WebAPIService::webAPIService, this, std::placeholders::_1));
}
// http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1
// e.g. http://ems-esp/api?device=boiler&cmd=wwtemp&data=20&id=1
void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
// see if the API is enabled
bool api_enabled;
@@ -71,9 +71,9 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
id = "-1";
}
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_MEDIUM_DYN);
JsonObject json = doc.to<JsonObject>();
bool ok = false;
DynamicJsonDocument doc(EMSESP_JSON_SIZE_LARGE_DYN);
JsonObject json = doc.to<JsonObject>();
bool ok = false;
// execute the command
if (data.isEmpty()) {
@@ -82,39 +82,20 @@ void WebAPIService::webAPIService(AsyncWebServerRequest * request) {
if (api_enabled) {
// we only allow commands with parameters if the API is enabled
ok = Command::call(device_type, cmd.c_str(), data.c_str(), id.toInt(), json); // has cmd, data and id
if (ok && json.size()) {
// send json output back to web
std::string buffer;
serializeJsonPretty(doc, buffer);
request->send(200, "text/plain", buffer.c_str());
return;
}
} else {
request->send(401, "text/plain", F("Unauthorized"));
return;
}
}
// debug
#if defined(EMSESP_DEBUG)
std::string debug(200, '\0');
snprintf_P(&debug[0],
debug.capacity() + 1,
PSTR("[DEBUG] API: device=%s cmd=%s data=%s id=%s [%s]"),
device.c_str(),
cmd.c_str(),
data.c_str(),
id.c_str(),
ok ? PSTR("OK") : PSTR("Invalid"));
EMSESP::logger().debug(debug.c_str());
if (json.size()) {
std::string buffer2;
serializeJson(doc, buffer2);
EMSESP::logger().debug("json (max 255 chars): %s", buffer2.c_str());
}
#endif
// if we have returned data in JSON format, send this to the WEB
if (json.size()) {
std::string buffer;
serializeJsonPretty(doc, buffer);
request->send(200, "text/plain", buffer.c_str());
} else {
request->send(200, "text/plain", ok ? F("OK") : F("Invalid"));
}
request->send(200, "text/plain", ok ? F("OK") : F("Invalid"));
}
} // namespace emsesp

View File

@@ -45,7 +45,7 @@ void WebDevicesService::scan_devices(AsyncWebServerRequest * request) {
}
void WebDevicesService::all_devices(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_MAX_JSON_SIZE_LARGE_DYN);
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_LARGE_DYN);
JsonObject root = response->getRoot();
JsonArray devices = root.createNestedArray("devices");
@@ -78,19 +78,28 @@ void WebDevicesService::all_devices(AsyncWebServerRequest * request) {
request->send(response);
}
// The unique_id is the unique record ID from the Web table to identify which device to load
void WebDevicesService::device_data(AsyncWebServerRequest * request, JsonVariant & json) {
if (json.is<JsonObject>()) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_MAX_JSON_SIZE_MAX_DYN);
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_XLARGE_DYN);
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
if (emsdevice->unique_id() == json["id"]) {
#ifndef EMSESP_STANDALONE
uint8_t id = json["id"]; // get id from selected table row
EMSESP::device_info_web(id, (JsonObject &)response->getRoot());
JsonObject root = response->getRoot();
emsdevice->generate_values_json_web(root);
#endif
response->setLength();
request->send(response);
} else {
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
response->setLength();
request->send(response);
return;
}
}
}
}
// invalid
AsyncWebServerResponse * response = request->beginResponse(200);
request->send(response);
}
} // namespace emsesp

View File

@@ -44,7 +44,7 @@ void WebStatusService::onStationModeDisconnected(WiFiEvent_t event, WiFiEventInf
}
void WebStatusService::onStationModeGotIP(WiFiEvent_t event, WiFiEventInfo_t info) {
EMSESP::logger().info(F("WiFi Connected with IP=%s, hostname=%s"), WiFi.localIP().toString().c_str(), WiFi.getHostname());
EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection
EMSESP::system_.reset_system_check(); // send out heartbeat MQTT as soon as we have a connection
}
#elif defined(ESP8266)
void WebStatusService::onStationModeDisconnected(const WiFiEventStationModeDisconnected & event) {
@@ -52,12 +52,12 @@ void WebStatusService::onStationModeDisconnected(const WiFiEventStationModeDisco
}
void WebStatusService::onStationModeGotIP(const WiFiEventStationModeGotIP & event) {
EMSESP::logger().info(F("WiFi Connected with IP=%s, hostname=%s"), event.ip.toString().c_str(), WiFi.hostname().c_str());
EMSESP::system_.send_heartbeat(); // send out heartbeat MQTT as soon as we have a connection
EMSESP::system_.reset_system_check(); // send out heartbeat MQTT as soon as we have a connection
}
#endif
void WebStatusService::webStatusService(AsyncWebServerRequest * request) {
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_MAX_JSON_SIZE_MEDIUM_DYN);
AsyncJsonResponse * response = new AsyncJsonResponse(false, EMSESP_JSON_SIZE_MEDIUM_DYN);
JsonObject root = response->getRoot();
root["status"] = EMSESP::bus_status(); // 0, 1 or 2

View File

@@ -26,7 +26,7 @@ uuid::log::Logger Command::logger_{F_(command), uuid::log::Facility::DAEMON};
std::vector<Command::CmdFunction> Command::cmdfunctions_;
// calls a command, context is the device_type
// calls a command
// id may be used to represent a heating circuit for example
// returns false if error or not found
bool Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id) {
@@ -49,7 +49,7 @@ bool Command::call(const uint8_t device_type, const char * cmd, const char * val
return ((cf->cmdfunction_)(value, id));
}
// calls a command, context is the device_type. Takes a json object for output.
// calls a command. Takes a json object for output.
// id may be used to represent a heating circuit for example
// returns false if error or not found
bool Command::call(const uint8_t device_type, const char * cmd, const char * value, const int8_t id, JsonObject & json) {
@@ -92,7 +92,7 @@ void Command::add(const uint8_t device_type, const uint8_t device_id, const __Fl
// see if we need to subscribe
if (Mqtt::enabled()) {
Mqtt::register_command(device_type, device_id, cmd, cb);
Mqtt::register_command(device_type, cmd, cb);
}
}

View File

@@ -387,7 +387,7 @@ void EMSESPShell::add_console_commands() {
return;
}
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_MEDIUM_DYN);
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = doc.to<JsonObject>();
bool ok = false;

View File

@@ -54,7 +54,7 @@ void DallasSensor::reload() {
parasite_ = settings.dallas_parasite;
});
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
if (Mqtt::ha_enabled()) {
for (uint8_t i = 0; i < MAX_SENSORS; registered_ha_[i++] = false)
;
}
@@ -282,14 +282,10 @@ bool DallasSensor::updated_values() {
return false;
}
bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & json) {
return (export_values(json));
}
// creates JSON doc from values
// returns false if empty
// e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03-5F","temp":23.30},"sensor2":{"id":"28-233D-9497-0C03-8B","temp":24.0}}
bool DallasSensor::export_values(JsonObject & json) {
bool DallasSensor::command_info(const char * value, const int8_t id, JsonObject & json) {
if (sensors_.size() == 0) {
return false;
}
@@ -317,18 +313,20 @@ void DallasSensor::publish_values(const bool force) {
}
DynamicJsonDocument doc(100 * num_sensors);
uint8_t sensor_no = 1;
uint8_t mqtt_format_ = Mqtt::mqtt_format();
uint8_t sensor_no = 1;
// dallas format is overriden when using Home Assistant
uint8_t dallas_format = Mqtt::ha_enabled() ? Mqtt::Dallas_Format::SENSORID : Mqtt::dallas_format();
for (const auto & sensor : sensors_) {
char sensorID[10]; // sensor{1-n}
snprintf_P(sensorID, 10, PSTR("sensor%d"), sensor_no);
if (mqtt_format_ == Mqtt::Format::SINGLE) {
if (dallas_format == Mqtt::Dallas_Format::NUMBER) {
// e.g. dallassensor_data = {"28-EA41-9497-0E03":23.3,"28-233D-9497-0C03":24.0}
if (Helpers::hasValue(sensor.temperature_c)) {
doc[sensor.to_string()] = (float)(sensor.temperature_c) / 10;
}
} else {
} else if (dallas_format == Mqtt::Dallas_Format::SENSORID) {
// e.g. dallassensor_data = {"sensor1":{"id":"28-EA41-9497-0E03","temp":23.3},"sensor2":{"id":"28-233D-9497-0C03","temp":24.0}}
JsonObject dataSensor = doc.createNestedObject(sensorID);
dataSensor["id"] = sensor.to_string();
@@ -339,9 +337,9 @@ void DallasSensor::publish_values(const bool force) {
// create the HA MQTT config
// to e.g. homeassistant/sensor/ems-esp/dallas_28-233D-9497-0C03/config
if (mqtt_format_ == Mqtt::Format::HA) {
if (Mqtt::ha_enabled()) {
if (!(registered_ha_[sensor_no - 1]) || force) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> config;
StaticJsonDocument<EMSESP_JSON_SIZE_MEDIUM> config;
config["dev_cla"] = FJSON("temperature");
char stat_t[50];
@@ -365,8 +363,8 @@ void DallasSensor::publish_values(const bool force) {
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
std::string topic(100, '\0');
snprintf_P(&topic[0], 100, PSTR("homeassistant/sensor/ems-esp/dallas_%s/config"), sensor.to_string().c_str());
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/dallas_%s/config"), System::hostname().c_str(), sensor.to_string().c_str());
Mqtt::publish_ha(topic, config.as<JsonObject>());
registered_ha_[sensor_no - 1] = true;

View File

@@ -102,7 +102,6 @@ class DallasSensor {
uint64_t get_id(const uint8_t addr[]);
bool command_info(const char * value, const int8_t id, JsonObject & json);
bool export_values(JsonObject & doc);
uint32_t last_activity_ = uuid::get_uptime();
uint32_t last_publish_ = uuid::get_uptime();

View File

@@ -63,7 +63,7 @@
{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 - Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18 / 0x19
// Thermostat - Buderus/Nefit/Bosch specific - 0x17 / 0x10 / 0x18 / 0x19 / 0x38
{ 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
@@ -91,7 +91,7 @@
{191, DeviceType::THERMOSTAT, F("FR120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS | DeviceFlags::EMS_DEVICE_FLAG_JUNKERS_OLD}, // older model
{192, DeviceType::THERMOSTAT, F("FW120"), DeviceFlags::EMS_DEVICE_FLAG_JUNKERS},
// Solar Modules - 0x30
// Solar Modules - 0x30, 0x2A (for ww)
{ 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},

File diff suppressed because it is too large Load Diff

View File

@@ -38,23 +38,14 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
void register_mqtt_ha_config();
void register_mqtt_ha_config_ww();
void check_active(const bool force = false);
bool export_values_main(JsonObject & doc, const bool textformat = false);
bool export_values_ww(JsonObject & doc, const bool textformat = false);
bool changed_ = false;
bool mqtt_ha_config_ = false; // HA MQTT Discovery
bool mqtt_ha_config_ww_ = false; // HA MQTT Discovery
uint8_t boilerState_ = EMS_VALUE_UINT_NOTSET; // Boiler state flag - FOR INTERNAL USE
static constexpr uint8_t EMS_TYPE_UBAParameterWW = 0x33;
static constexpr uint8_t EMS_TYPE_UBAFunctionTest = 0x1D;
@@ -68,120 +59,99 @@ class Boiler : public EMSdevice {
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 wWChargeType_ = EMS_VALUE_BOOL_NOTSET; // Warm Water charge type (pump or 3-way-valve)
uint8_t wWDisinfectionTemp_ = EMS_VALUE_UINT_NOTSET; // Warm Water disinfection temperature to prevent infection
uint8_t wWComfort_ = EMS_VALUE_UINT_NOTSET; // WW comfort mode
// ww
uint8_t wWSetTemp_; // Warm Water set temperature
uint8_t wWSelTemp_; // Warm Water selected temperature
uint8_t wWType_; // 0-off, 1-flow, 2-flowbuffer, 3-buffer, 4-layered buffer
uint8_t wWComfort_; // WW comfort mode
uint8_t wWCircPump_; // Warm Water circulation pump available
uint8_t wWChargeType_; // Warm Water charge type (pump or 3-way-valve)
uint8_t wWDisinfectionTemp_; // Warm Water disinfection temperature to prevent infection
uint8_t wWCircPumpMode_; // Warm Water circulation pump mode
uint8_t wWCirc_; // Circulation on/off
uint16_t wWCurTemp_; // Warm Water current temperature
uint16_t wWCurTemp2_; // Warm Water current temperature storage
uint8_t wWCurFlow_; // Warm Water current flow temp in l/min
uint16_t wwStorageTemp1_; // warm water storage temp 1
uint16_t wwStorageTemp2_; // warm water storage temp 2
uint8_t wWActivated_; // Warm Water activated
uint8_t wWOneTime_; // Warm Water one time function on/off
uint8_t wWDisinfecting_; // Warm Water disinfection on/off
uint8_t wWCharging_; // Warm Water charging on/off
uint8_t wWRecharging_; // Warm Water recharge on/off
uint8_t wWTempOK_; // Warm Water temperature ok on/off
uint8_t wWActive_;
uint8_t wWHeat_; // 3-way valve on WW
uint8_t wWSetPumpPower_; // ww pump speed/power?
uint16_t wwMixTemperature_; // mixing temperature
uint16_t wwBufferTemperature_; // buffertemperature
uint32_t wWStarts_; // Warm Water # starts
uint32_t wWStarts2_; // Warm water starts (control)
uint32_t wWWorkM_; // Warm Water # minutes
// MC10Status
uint16_t wwMixTemperature_ = EMS_VALUE_USHORT_NOTSET; // mengertemperatuur
uint16_t wwBufferTemperature_ = EMS_VALUE_USHORT_NOTSET; // buffertemperature
// 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 heatPump_ = 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 serviceCode_[4] = {'\0'}; // 3 character status/service code
uint16_t serviceCodeNumber_ = EMS_VALUE_USHORT_NOTSET; // error/service code
uint8_t boilerState_ = EMS_VALUE_UINT_NOTSET; // Boiler state flag
char lastCode_[30] = {'\0'};
uint32_t lastCodeDate_ = 0;
// UBAMonitorSlow - 0x19 on EMS1
int16_t outdoorTemp_ = 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 wWSetTemp_ = EMS_VALUE_UINT_NOTSET; // Warm Water set temperature
uint16_t wWCurTemp_ = EMS_VALUE_USHORT_NOTSET; // Warm Water current temperature
uint16_t wWCurTemp2_ = 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 wWDisinfecting_ = EMS_VALUE_BOOL_NOTSET; // Warm Water disinfection on/off
uint8_t wWCharging_ = EMS_VALUE_BOOL_NOTSET; // Warm Water charging on/off
uint8_t wWRecharging_ = EMS_VALUE_BOOL_NOTSET; // Warm Water recharge on/off
uint8_t wWTempOK_ = 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
uint8_t wWType_ = EMS_VALUE_UINT_NOTSET; // 0-off, 1-flow, 2-flowbuffer, 3-buffer, 4-layered buffer
uint8_t wWActive_ = EMS_VALUE_BOOL_NOTSET;
// UBATotalUptime
uint32_t UBAuptime_ = EMS_VALUE_ULONG_NOTSET; // Total UBA working hours
// UBAParameters
uint8_t heatingActivated_ = EMS_VALUE_BOOL_NOTSET; // Heating activated on the boiler
uint8_t heatingTemp_ = EMS_VALUE_UINT_NOTSET; // Heating temperature setting on the boiler
uint8_t pumpModMax_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation max. power %
uint8_t pumpModMin_ = EMS_VALUE_UINT_NOTSET; // Boiler circuit pump modulation min. power
uint8_t burnMinPower_ = EMS_VALUE_UINT_NOTSET;
uint8_t burnMaxPower_ = EMS_VALUE_UINT_NOTSET;
int8_t boilHystOff_ = EMS_VALUE_INT_NOTSET;
int8_t boilHystOn_ = EMS_VALUE_INT_NOTSET;
uint8_t burnMinPeriod_ = EMS_VALUE_UINT_NOTSET;
uint8_t pumpDelay_ = EMS_VALUE_UINT_NOTSET;
// UBASetPoint
uint8_t setFlowTemp_ = EMS_VALUE_UINT_NOTSET; // boiler setpoint temp
uint8_t setBurnPow_ = EMS_VALUE_UINT_NOTSET; // max output power in %
uint8_t wWSetPumpPower_ = EMS_VALUE_UINT_NOTSET; // ww pump speed/power?
// other internal calculated params
uint8_t tapwaterActive_ = EMS_VALUE_BOOL_NOTSET; // Hot tap water is on/off
uint8_t heatingActive_ = EMS_VALUE_BOOL_NOTSET; // Central heating is on/off
uint8_t pumpMod2_ = EMS_VALUE_UINT_NOTSET; // heatpump modulation from 0xE3 (heatpumps)
// UBAInformation
uint32_t upTimeControl_ = EMS_VALUE_ULONG_NOTSET; // Operating time control
uint32_t upTimeCompHeating_ = EMS_VALUE_ULONG_NOTSET; // Operating time compressor heating
uint32_t upTimeCompCooling_ = EMS_VALUE_ULONG_NOTSET; // Operating time compressor cooling
uint32_t upTimeCompWw_ = EMS_VALUE_ULONG_NOTSET; // Operating time compressor warm water
uint32_t heatingStarts_ = EMS_VALUE_ULONG_NOTSET; // Heating starts (control)
uint32_t coolingStarts_ = EMS_VALUE_ULONG_NOTSET; // Cooling starts (control)
uint32_t wWStarts2_ = EMS_VALUE_ULONG_NOTSET; // Warm water starts (control)
uint32_t nrgConsTotal_ = EMS_VALUE_ULONG_NOTSET; // Energy consumption total
uint32_t auxElecHeatNrgConsTotal_ = EMS_VALUE_ULONG_NOTSET; // Auxiliary electrical heater energy consumption total
uint32_t auxElecHeatNrgConsHeating_ = EMS_VALUE_ULONG_NOTSET; // Auxiliary electrical heater energy consumption heating
uint32_t auxElecHeatNrgConsDHW_ = EMS_VALUE_ULONG_NOTSET; // Auxiliary electrical heater energ consumption DHW
uint32_t nrgConsCompTotal_ = EMS_VALUE_ULONG_NOTSET; // Energy consumption compressor total
uint32_t nrgConsCompHeating_ = EMS_VALUE_ULONG_NOTSET; // Energy consumption compressor heating
uint32_t nrgConsCompWw_ = EMS_VALUE_ULONG_NOTSET; // Energy consumption compressor warm water
uint32_t nrgConsCompCooling_ = EMS_VALUE_ULONG_NOTSET; // Energy consumption compressor cooling
// UBAEnergySupplied
uint32_t nrgSuppTotal_ = EMS_VALUE_ULONG_NOTSET; // Energy supplied total
uint32_t nrgSuppHeating_ = EMS_VALUE_ULONG_NOTSET; // Energy supplied heating
uint32_t nrgSuppWw_ = EMS_VALUE_ULONG_NOTSET; // Energy supplied warm water
uint32_t nrgSuppCooling_ = EMS_VALUE_ULONG_NOTSET; // Energy supplied cooling
// _UBAMaintenanceData
uint8_t maintenanceMessage_ = EMS_VALUE_UINT_NOTSET;
uint8_t maintenanceType_ = EMS_VALUE_UINT_NOTSET;
uint8_t maintenanceTime_ = EMS_VALUE_UINT_NOTSET;
char maintenanceDate_[12] = {'\0'};
// main
uint8_t heatingActive_; // Central heating is on/off
uint8_t tapwaterActive_; // Hot tap water is on/off
uint8_t selFlowTemp_; // Selected flow temperature
uint8_t selBurnPow_; // Burner max power %
uint8_t pumpMod2_; // heatpump modulation from 0xE3 (heatpumps)
uint8_t pumpMod_; // Pump modulation %
int16_t outdoorTemp_; // Outside temperature
uint16_t curFlowTemp_; // Current flow temperature
uint16_t retTemp_; // Return temperature
uint16_t switchTemp_; // Switch temperature
uint8_t sysPress_; // System pressure
uint16_t boilTemp_; // Boiler temperature
uint16_t exhaustTemp_; // Exhaust temperature
uint8_t burnGas_; // Gas on/off
uint16_t flameCurr_; // Flame current in micro amps
uint8_t heatPump_; // Boiler pump on/off
uint8_t fanWork_; // Fan on/off
uint8_t ignWork_; // Ignition on/off
uint8_t heatingActivated_; // Heating activated on the boiler
uint8_t heatingTemp_; // Heating temperature setting on the boiler
uint8_t pumpModMax_; // Boiler circuit pump modulation max. power %
uint8_t pumpModMin_; // Boiler circuit pump modulation min. power
uint8_t pumpDelay_;
uint8_t burnMinPeriod_;
uint8_t burnMinPower_;
uint8_t burnMaxPower_;
int8_t boilHystOn_;
int8_t boilHystOff_;
uint8_t setFlowTemp_; // boiler setpoint temp
uint8_t curBurnPow_; // Burner current power %
uint8_t setBurnPow_; // max output power in %
uint32_t burnStarts_; // # burner restarts
uint32_t burnWorkMin_; // Total burner operating time
uint32_t heatWorkMin_; // Total heat operating time
uint32_t UBAuptime_; // Total UBA working hours
char lastCode_[30]; // last error code
char serviceCode_[4]; // 3 character status/service code
uint16_t serviceCodeNumber_; // error/service code
// info
uint32_t upTimeControl_; // Operating time control
uint32_t upTimeCompHeating_; // Operating time compressor heating
uint32_t upTimeCompCooling_; // Operating time compressor cooling
uint32_t upTimeCompWw_; // Operating time compressor warm water
uint32_t heatingStarts_; // Heating starts (control)
uint32_t coolingStarts_; // Cooling starts (control)
uint32_t nrgConsTotal_; // Energy consumption total
uint32_t nrgConsCompTotal_; // Energy consumption compressor total
uint32_t nrgConsCompHeating_; // Energy consumption compressor heating
uint32_t nrgConsCompWw_; // Energy consumption compressor warm water
uint32_t nrgConsCompCooling_; // Energy consumption compressor cooling
uint32_t nrgSuppTotal_; // Energy supplied total
uint32_t nrgSuppHeating_; // Energy supplied heating
uint32_t nrgSuppWw_; // Energy supplied warm water
uint32_t nrgSuppCooling_; // Energy supplied cooling
uint32_t auxElecHeatNrgConsTotal_; // Auxiliary electrical heater energy consumption total
uint32_t auxElecHeatNrgConsHeating_; // Auxiliary electrical heater energy consumption heating
uint32_t auxElecHeatNrgConsDHW_; // Auxiliary electrical heater energy consumption DHW
char maintenanceMessage_[4];
char maintenanceDate_[12];
uint8_t maintenanceType_;
uint8_t maintenanceTime_;
void process_UBAParameterWW(std::shared_ptr<const Telegram> telegram);
void process_UBAMonitorFast(std::shared_ptr<const Telegram> telegram);

View File

@@ -28,21 +28,9 @@ Connect::Connect(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
}
void Connect::device_info_web(JsonArray & root) {
}
// publish values via MQTT
void Connect::publish_values(JsonObject & json, bool force) {
}
// export values to JSON
bool Connect::export_values(JsonObject & json) {
// publish HA config
bool Connect::publish_ha_config() {
return true;
}
// check to see if values have been updated
bool Connect::updated_values() {
return false;
}
} // namespace emsesp

View File

@@ -35,10 +35,7 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;

View File

@@ -28,21 +28,9 @@ Controller::Controller(uint8_t device_type, uint8_t device_id, uint8_t product_i
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
}
void Controller::device_info_web(JsonArray & root) {
}
// publish values via MQTT
void Controller::publish_values(JsonObject & json, bool force) {
}
// export values to JSON
bool Controller::export_values(JsonObject & json) {
return true;
}
// check to see if values have been updated
bool Controller::updated_values() {
return false;
// publish HA config
bool Controller::publish_ha_config() {
return true;
}
} // namespace emsesp

View File

@@ -35,10 +35,7 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;

View File

@@ -28,21 +28,9 @@ Gateway::Gateway(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
}
void Gateway::device_info_web(JsonArray & root) {
}
// publish values via MQTT
void Gateway::publish_values(JsonObject & json, bool force) {
}
// export values to JSON
bool Gateway::export_values(JsonObject & json) {
return true;
}
// check to see if values have been updated
bool Gateway::updated_values() {
return false;
// publish HA config
bool Gateway::publish_ha_config() {
return true;
}
} // namespace emsesp

View File

@@ -35,10 +35,7 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;

View File

@@ -28,21 +28,9 @@ Generic::Generic(uint8_t device_type, uint8_t device_id, uint8_t product_id, con
: EMSdevice(device_type, device_id, product_id, version, name, flags, brand) {
}
void Generic::device_info_web(JsonArray & root) {
}
// publish values via MQTT
void Generic::publish_values(JsonObject & json, bool force) {
}
// export values to JSON
bool Generic::export_values(JsonObject & json) {
return true;
}
// check to see if values have been updated
bool Generic::updated_values() {
return false;
// publish HA config
bool Generic::publish_ha_config() {
return true;
}
} // namespace emsesp

View File

@@ -35,10 +35,7 @@ class Generic : public EMSdevice {
public:
Generic(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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;

View File

@@ -31,66 +31,23 @@ Heatpump::Heatpump(uint8_t device_type, uint8_t device_id, uint8_t product_id, c
// telegram handlers
register_telegram_type(0x042B, F("HP1"), true, [&](std::shared_ptr<const Telegram> t) { process_HPMonitor1(t); });
register_telegram_type(0x047B, F("HP2"), true, [&](std::shared_ptr<const Telegram> t) { process_HPMonitor2(t); });
std::string empty("");
register_device_value(empty, &airHumidity_, DeviceValueType::UINT, flash_string_vector{F("2")}, F("airHumidity"), F("Relative air humidity"), DeviceValueUOM::NONE);
register_device_value(empty, &dewTemperature_, DeviceValueType::UINT, {}, F("dewTemperature"), F("Dew point temperature"), DeviceValueUOM::NONE);
}
// creates JSON doc from values
// returns false if empty
bool Heatpump::export_values(JsonObject & json) {
if (Helpers::hasValue(airHumidity_)) {
json["airHumidity"] = (float)airHumidity_ / 2;
}
if (Helpers::hasValue(dewTemperature_)) {
json["dewTemperature"] = dewTemperature_;
}
return json.size();
}
void Heatpump::device_info_web(JsonArray & root) {
// fetch the values into a JSON document
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json = doc.to<JsonObject>();
if (!export_values(json)) {
return; // empty
}
create_value_json(root, F("airHumidity"), nullptr, F_(airHumidity), F_(percent), json);
create_value_json(root, F("dewTemperature"), nullptr, F_(dewTemperature), F_(degrees), json);
}
// publish values via MQTT
void Heatpump::publish_values(JsonObject & json, bool force) {
// handle HA first
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
if (!mqtt_ha_config_ || force) {
register_mqtt_ha_config();
return;
}
}
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json_data = doc.to<JsonObject>();
if (export_values(json_data)) {
Mqtt::publish(F("heatpump_data"), doc.as<JsonObject>());
}
}
void Heatpump::register_mqtt_ha_config() {
if (!Mqtt::connected()) {
return;
}
// Create the Master device
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
doc["name"] = F_(EMSESP);
// publish HA config
bool Heatpump::publish_ha_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(heatpump);
doc["ic"] = F_(iconheatpump);
doc["ic"] = F_(iconvalve);
char stat_t[50];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/heatpump_data"), System::hostname().c_str());
doc["stat_t"] = stat_t;
doc["name"] = FJSON("Humidity");
doc["val_tpl"] = FJSON("{{value_json.airHumidity}}");
JsonObject dev = doc.createNestedObject("dev");
@@ -100,21 +57,12 @@ void Heatpump::register_mqtt_ha_config() {
dev["mdl"] = this->name();
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp-heatpump");
Mqtt::publish_ha(F("homeassistant/sensor/ems-esp/heatpump/config"), doc.as<JsonObject>()); // publish the config payload with retain flag
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(airHumidity), device_type(), "airHumidity", F_(percent), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(dewTemperature), device_type(), "dewTemperature", F_(degrees), nullptr);
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/heatpump/config"), System::hostname().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
mqtt_ha_config_ = true; // done
}
// check to see if values have been updated
bool Heatpump::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
return true;
}
/*
@@ -122,8 +70,8 @@ bool Heatpump::updated_values() {
* e.g. "38 10 FF 00 03 7B 08 24 00 4B"
*/
void Heatpump::process_HPMonitor2(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(dewTemperature_, 0);
changed_ |= telegram->read_value(airHumidity_, 1);
has_update(telegram->read_value(dewTemperature_, 0));
has_update(telegram->read_value(airHumidity_, 1));
}
#pragma GCC diagnostic push

View File

@@ -36,21 +36,13 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
void register_mqtt_ha_config();
uint8_t airHumidity_ = EMS_VALUE_UINT_NOTSET;
uint8_t dewTemperature_ = EMS_VALUE_UINT_NOTSET;
bool changed_ = false;
bool mqtt_ha_config_ = false; // for HA MQTT Discovery
uint8_t airHumidity_;
uint8_t dewTemperature_;
void process_HPMonitor1(std::shared_ptr<const Telegram> telegram);
void process_HPMonitor2(std::shared_ptr<const Telegram> telegram);

View File

@@ -55,103 +55,50 @@ Mixer::Mixer(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
}
}
// output json to web UI
void Mixer::device_info_web(JsonArray & root) {
if (type() == Type::NONE) {
return; // don't have any values yet
// register values, depending on type (hc or wwc)
void Mixer::register_values(const Type type, uint16_t hc) {
if (type == Type::NONE) {
return; // already done
}
// fetch the values into a JSON document
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json = doc.to<JsonObject>();
// store the heating circuit and type
hc_ = hc + 1;
type_ = type;
if (!export_values_format(Mqtt::Format::SINGLE, json)) {
return; // empty
}
std::string prefix(10, '\0');
snprintf_P(&prefix[0], sizeof(prefix), PSTR("%s%d"), (type_ == Type::HC) ? "hc" : "wwc", hc + 1);
char prefix_str[10];
if (type() == Type::HC) {
snprintf_P(prefix_str, sizeof(prefix_str), PSTR("(hc %d) "), hc_);
create_value_json(root, F("flowTemp"), FPSTR(prefix_str), F_(flowTemp), F_(degrees), json);
create_value_json(root, F("flowSetTemp"), FPSTR(prefix_str), F_(flowSetTemp), F_(degrees), json);
create_value_json(root, F("pumpStatus"), FPSTR(prefix_str), F_(pumpStatus), nullptr, json);
create_value_json(root, F("valveStatus"), FPSTR(prefix_str), F_(valveStatus), F_(percent), json);
} else {
snprintf_P(prefix_str, sizeof(prefix_str), PSTR("(wwc %d) "), hc_);
create_value_json(root, F("wwTemp"), FPSTR(prefix_str), F_(wwTemp), F_(degrees), json);
create_value_json(root, F("pumpStatus"), FPSTR(prefix_str), F_(pumpStatus), nullptr, json);
create_value_json(root, F("tempStatus"), FPSTR(prefix_str), F_(tempStatus), nullptr, json);
}
register_device_value(
prefix, &flowTemp_, DeviceValueType::USHORT, flash_string_vector{F("10")}, F("flowTemp"), F("Current flow temperature"), DeviceValueUOM::DEGREES);
register_device_value(prefix, &flowSetTemp_, DeviceValueType::UINT, {}, F("flowSetTemp"), F("Setpoint flow temperature"), DeviceValueUOM::DEGREES);
register_device_value(prefix, &pumpStatus_, DeviceValueType::BOOL, {}, F("pumpStatus"), F("Pump/Valve status"), DeviceValueUOM::NONE);
register_device_value(prefix, &status_, DeviceValueType::INT, {}, F("status"), F("Current status"), DeviceValueUOM::NONE);
}
// check to see if values have been updated
bool Mixer::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
}
// publish values via MQTT
// topic is mixer_data<id>
void Mixer::publish_values(JsonObject & json, bool force) {
// handle HA first
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
if (!mqtt_ha_config_ || force) {
register_mqtt_ha_config();
return;
}
}
if (Mqtt::mqtt_format() == Mqtt::Format::SINGLE) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json_data = doc.to<JsonObject>();
if (export_values_format(Mqtt::mqtt_format(), json_data)) {
char topic[30];
if (type() == Type::HC) {
snprintf_P(topic, 30, PSTR("mixer_data_hc%d"), hc_);
} else {
snprintf_P(topic, 30, PSTR("mixer_data_wwc%d"), hc_);
}
Mqtt::publish(topic, doc.as<JsonObject>());
}
} else {
// format is HA or Nested. This is bundled together and sent in emsesp.cpp
export_values_format(Mqtt::mqtt_format(), json);
}
}
// publish config topic for HA MQTT Discovery
void Mixer::register_mqtt_ha_config() {
if (!Mqtt::connected()) {
return;
}
// publish HA config
bool Mixer::publish_ha_config() {
// if we don't have valid values for this HC don't add it ever again
if (!Helpers::hasValue(pumpStatus_)) {
return;
return false;
}
// Create the Master device
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
char name[20];
snprintf_P(name, sizeof(name), PSTR("Mixer %02X"), device_id() - 0x20 + 1);
doc["name"] = name;
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
char uniq_id[20];
snprintf_P(uniq_id, sizeof(uniq_id), PSTR("mixer%02X"), device_id() - 0x20 + 1);
snprintf_P(uniq_id, sizeof(uniq_id), PSTR("Mixer%02X"), device_id() - 0x20 + 1);
doc["uniq_id"] = uniq_id;
doc["ic"] = FJSON("mdi:home-thermometer-outline");
char stat_t[50];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/mixer_data"), System::hostname().c_str());
doc["stat_t"] = stat_t;
doc["val_tpl"] = FJSON("{{value_json.type}}"); // HA needs a single value. We take the type which is wwc or hc
char name[20];
snprintf_P(name, sizeof(name), PSTR("Mixer %02X Type"), device_id() - 0x20 + 1);
doc["name"] = name;
doc["val_tpl"] = FJSON("{{value_json.type}}"); // HA needs a single value. We take the type which is wwc or hc
JsonObject dev = doc.createNestedObject("dev");
dev["name"] = FJSON("EMS-ESP Mixer");
dev["sw"] = EMSESP_APP_VERSION;
@@ -160,127 +107,45 @@ void Mixer::register_mqtt_ha_config() {
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp-mixer");
// determine the topic, if its HC and WWC. This is determined by the incoming telegram types.
std::string topic(100, '\0');
if (type() == Type::HC) {
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/sensor/ems-esp/mixer_hc%d/config"), hc_);
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
char hc_name[10];
snprintf_P(hc_name, sizeof(hc_name), PSTR("hc%d"), hc_);
Mqtt::register_mqtt_ha_sensor(hc_name, nullptr, F_(flowTemp), device_type(), "flowTemp", F_(degrees), F_(icontemperature));
Mqtt::register_mqtt_ha_sensor(hc_name, nullptr, F_(flowSetTemp), device_type(), "flowSetTemp", F_(degrees), F_(icontemperature));
Mqtt::register_mqtt_ha_sensor(hc_name, nullptr, F_(pumpStatus), device_type(), "pumpStatus", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(hc_name, nullptr, F_(valveStatus), device_type(), "valveStatus", F_(percent), F_(iconpercent));
if (type_ == Type::HC) {
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/sensor/%s/mixer_hc%d/config"), System::hostname().c_str(), hc_);
} else {
// WWC
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/sensor/ems-esp/mixer_wwc%d/config"), hc_);
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
char wwc_name[10];
snprintf_P(wwc_name, sizeof(wwc_name), PSTR("wwc%d"), hc_);
Mqtt::register_mqtt_ha_sensor(wwc_name, nullptr, F_(wwTemp), device_type(), "wwTemp", F_(degrees), F_(icontemperature));
Mqtt::register_mqtt_ha_sensor(wwc_name, nullptr, F_(pumpStatus), device_type(), "pumpStatus", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(wwc_name, nullptr, F_(tempStatus), device_type(), "tempStatus", nullptr, nullptr);
snprintf_P(&topic[0], topic.capacity() + 1, PSTR("homeassistant/sensor/%s/mixer_wwc%d/config"), System::hostname().c_str(), hc_); // WWC
}
mqtt_ha_config_ = true; // done
}
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
bool Mixer::export_values(JsonObject & json) {
return export_values_format(Mqtt::Format::NESTED, json);
}
// creates JSON doc from values
// returns false if empty
bool Mixer::export_values_format(uint8_t mqtt_format, JsonObject & json) {
// check if there is data for the mixer unit
if (type() == Type::NONE) {
return 0;
}
JsonObject json_hc;
char hc_name[10]; // hc{1-4}
if (type() == Type::HC) {
snprintf_P(hc_name, sizeof(hc_name), PSTR("hc%d"), hc_);
if (mqtt_format == Mqtt::Format::SINGLE) {
json_hc = json;
json["type"] = FJSON("hc");
} else if (mqtt_format == Mqtt::Format::HA) {
json_hc = json.createNestedObject(hc_name);
json_hc["type"] = FJSON("hc");
} else {
json_hc = json.createNestedObject(hc_name);
}
if (Helpers::hasValue(flowTemp_)) {
json_hc["flowTemp"] = (float)flowTemp_ / 10;
}
if (Helpers::hasValue(flowSetTemp_)) {
json_hc["flowSetTemp"] = flowSetTemp_;
}
if (Helpers::hasValue(pumpStatus_)) {
char s[7];
json_hc["pumpStatus"] = Helpers::render_value(s, pumpStatus_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(status_)) {
json_hc["valveStatus"] = status_;
}
return json_hc.size();
}
// WWC
snprintf_P(hc_name, sizeof(hc_name), PSTR("wwc%d"), hc_);
if (mqtt_format == Mqtt::Format::SINGLE) {
json_hc = json;
json["type"] = FJSON("wwc");
} else if (mqtt_format == Mqtt::Format::HA) {
json_hc = json.createNestedObject(hc_name);
json_hc["type"] = FJSON("wwc");
} else {
json_hc = json.createNestedObject(hc_name);
}
if (Helpers::hasValue(flowTemp_)) {
json_hc["wwTemp"] = (float)flowTemp_ / 10;
}
if (Helpers::hasValue(pumpStatus_)) {
char s[7];
json_hc["pumpStatus"] = Helpers::render_value(s, pumpStatus_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(status_)) {
json_hc["tempStatus"] = status_;
}
return json_hc.size();
return true;
}
// 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 Mixer::process_MMPLUSStatusMessage_HC(std::shared_ptr<const Telegram> telegram) {
type(Type::HC);
hc_ = telegram->type_id - 0x02D7 + 1; // determine which circuit this is
changed_ |= telegram->read_value(flowTemp_, 3); // is * 10
changed_ |= telegram->read_value(flowSetTemp_, 5);
changed_ |= telegram->read_bitvalue(pumpStatus_, 0, 0);
changed_ |= telegram->read_value(status_, 2); // valve status
register_values(Type::HC, telegram->type_id - 0x02D7);
has_update(telegram->read_value(flowTemp_, 3)); // is * 10
has_update(telegram->read_value(flowSetTemp_, 5));
has_update(telegram->read_bitvalue(pumpStatus_, 0, 0));
has_update(telegram->read_value(status_, 2)); // valve status
}
// Mixer 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 Mixer::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.
changed_ |= telegram->read_value(flowTemp_, 0); // is * 10
changed_ |= telegram->read_bitvalue(pumpStatus_, 2, 0);
changed_ |= telegram->read_value(status_, 11); // temp status
register_values(Type::WWC, telegram->type_id - 0x0331);
has_update(telegram->read_value(flowTemp_, 0)); // is * 10
has_update(telegram->read_bitvalue(pumpStatus_, 2, 0));
has_update(telegram->read_value(status_, 11)); // temp status
}
// Mixer 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 Mixer::process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram) {
type(Type::HC);
hc_ = device_id() - 0x20 + 1;
register_values(Type::HC, device_id() - 0x20);
// check if circuit is active, 0-off, 1-unmixed, 2-mixed
uint8_t ismixed = 0;
@@ -291,28 +156,27 @@ void Mixer::process_IPMStatusMessage(std::shared_ptr<const Telegram> telegram) {
// do we have a mixed circuit
if (ismixed == 2) {
changed_ |= telegram->read_value(flowTemp_, 3); // is * 10
changed_ |= telegram->read_value(flowSetTemp_, 5);
changed_ |= telegram->read_value(status_, 2); // valve status
has_update(telegram->read_value(flowTemp_, 3)); // is * 10
has_update(telegram->read_value(flowSetTemp_, 5));
has_update(telegram->read_value(status_, 2)); // valve status
}
changed_ |= telegram->read_bitvalue(pumpStatus_, 1, 0); // pump is also in unmixed circuits
has_update(telegram->read_bitvalue(pumpStatus_, 1, 0)); // pump is also in unmixed circuits
}
// Mixer on a MM10 - 0xAB
// e.g. Mixer 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 Mixer::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;
changed_ |= telegram->read_value(flowTemp_, 1); // is * 10
changed_ |= telegram->read_bitvalue(pumpStatus_, 3, 2); // is 0 or 0x64 (100%), check only bit 2
changed_ |= telegram->read_value(flowSetTemp_, 0);
changed_ |= telegram->read_value(status_, 4); // valve status -100 to 100
register_values(Type::HC, device_id() - 0x20);
has_update(telegram->read_value(flowTemp_, 1)); // is * 10
has_update(telegram->read_bitvalue(pumpStatus_, 3, 2)); // is 0 or 0x64 (100%), check only bit 2
has_update(telegram->read_value(flowSetTemp_, 0));
has_update(telegram->read_value(status_, 4)); // valve status -100 to 100
}
#pragma GCC diagnostic push
@@ -321,7 +185,7 @@ void Mixer::process_MMStatusMessage(std::shared_ptr<const Telegram> telegram) {
// Mixer on a MM10 - 0xAA
// e.g. Thermostat -> Mixer Module, type 0xAA, telegram: 10 21 AA 00 FF 0C 0A 11 0A 32 xx
void Mixer::process_MMConfigMessage(std::shared_ptr<const Telegram> telegram) {
hc_ = device_id() - 0x20 + 1;
register_values(Type::HC, device_id() - 0x20);
// pos 0: active FF = on
// pos 1: valve runtime 0C = 120 sec in units of 10 sec
}
@@ -329,7 +193,7 @@ void Mixer::process_MMConfigMessage(std::shared_ptr<const Telegram> telegram) {
// Mixer on a MM10 - 0xAC
// e.g. Thermostat -> Mixer Module, type 0xAC, telegram: 10 21 AC 00 1E 64 01 AB
void Mixer::process_MMSetMessage(std::shared_ptr<const Telegram> telegram) {
hc_ = device_id() - 0x20 + 1;
register_values(Type::HC, device_id() - 0x20);
// pos 0: flowtemp setpoint 1E = 30°C
// pos 1: position in %
}

View File

@@ -36,17 +36,11 @@ class Mixer : public EMSdevice {
public:
Mixer(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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
bool export_values_format(uint8_t mqtt_format, JsonObject & doc);
void register_mqtt_ha_config();
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);
@@ -60,25 +54,15 @@ class Mixer : public EMSdevice {
WWC // warm water circuit
};
Type type() const {
return type_;
}
void type(Type new_type) {
type_ = new_type;
}
private:
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET;
uint8_t pumpStatus_ = EMS_VALUE_UINT_NOTSET;
int8_t status_ = EMS_VALUE_INT_NOTSET;
uint8_t flowSetTemp_ = EMS_VALUE_UINT_NOTSET;
uint16_t flowTemp_;
uint8_t pumpStatus_;
int8_t status_;
uint8_t flowSetTemp_;
Type type_ = Type::NONE;
bool changed_ = false;
bool mqtt_ha_config_ = false; // for HA MQTT Discovery
void register_values(const Type type, const uint16_t hc);
Type type_ = Type::NONE;
uint16_t hc_ = EMS_VALUE_USHORT_NOTSET;
};
} // namespace emsesp

View File

@@ -59,67 +59,62 @@ Solar::Solar(uint8_t device_type, uint8_t device_id, uint8_t product_id, const s
register_telegram_type(0x0103, F("ISM1StatusMessage"), true, [&](std::shared_ptr<const Telegram> t) { process_ISM1StatusMessage(t); });
register_telegram_type(0x0101, F("ISM1Set"), false, [&](std::shared_ptr<const Telegram> t) { process_ISM1Set(t); });
}
std::string empty("");
// special case for a device_id with 0x2A where it's not actual a solar module
if (device_id == 0x2A) {
register_device_value(empty, &type_, DeviceValueType::TEXT, {}, F("type"), F("Type"), DeviceValueUOM::NONE);
strncpy(type_, "warm water circuit", sizeof(type_));
}
register_device_value(empty,
&collectorTemp_,
DeviceValueType::SHORT,
flash_string_vector{F("10")},
F("collectorTemp"),
F("Collector temperature (TS1)"),
DeviceValueUOM::DEGREES);
register_device_value(empty,
&tankBottomTemp_,
DeviceValueType::SHORT,
flash_string_vector{F("10")},
F("tankBottomTemp"),
F("Bottom temperature (TS2)"),
DeviceValueUOM::DEGREES);
register_device_value(empty,
&tankBottomTemp2_,
DeviceValueType::SHORT,
flash_string_vector{F("10")},
F("tankBottomTemp2"),
F("Bottom temperature (TS5)"),
DeviceValueUOM::DEGREES);
register_device_value(
empty, &heatExchangerTemp_, DeviceValueType::SHORT, {F("10")}, F("heatExchangerTemp"), F("Heat exchanger temperature (TS6)"), DeviceValueUOM::DEGREES);
register_device_value(empty, &tank1MaxTempCurrent_, DeviceValueType::UINT, {}, F("tank1MaxTempCurrent"), F("Maximum Tank temperature"), DeviceValueUOM::NONE);
register_device_value(empty, &solarPumpModulation_, DeviceValueType::UINT, {}, F("solarPumpModulation"), F("Solar pump modulation (PS1)"), DeviceValueUOM::PERCENT);
register_device_value(
empty, &cylinderPumpModulation_, DeviceValueType::UINT, {}, F("cylinderPumpModulation"), F("Cylinder pump modulation (PS5)"), DeviceValueUOM::PERCENT);
register_device_value(empty, &solarPump_, DeviceValueType::BOOL, {}, F("solarPump"), F("Solar pump (PS1) active"), DeviceValueUOM::NONE);
register_device_value(empty, &valveStatus_, DeviceValueType::BOOL, {}, F("valveStatus"), F("Valve status"), DeviceValueUOM::NONE);
register_device_value(empty, &tankHeated_, DeviceValueType::BOOL, {}, F("tankHeated"), F("Tank heated"), DeviceValueUOM::NONE);
register_device_value(empty, &collectorShutdown_, DeviceValueType::BOOL, {}, F("collectorShutdown"), F("Collector shutdown"), DeviceValueUOM::NONE);
register_device_value(empty, &pumpWorkMin_, DeviceValueType::TIME, {}, F("pumpWorkMin"), F("Pump working time"), DeviceValueUOM::MINUTES);
register_device_value(
empty, &energyLastHour_, DeviceValueType::ULONG, flash_string_vector{F("10")}, F("energyLastHour"), F("Energy last hour"), DeviceValueUOM::WH);
register_device_value(empty, &energyTotal_, DeviceValueType::ULONG, flash_string_vector{F("10")}, F("energyTotal"), F("Energy total"), DeviceValueUOM::KWH);
register_device_value(empty, &energyToday_, DeviceValueType::ULONG, {}, F("energyToday"), F("Energy today"), DeviceValueUOM::WH);
}
// print to web
void Solar::device_info_web(JsonArray & root) {
// fetch the values into a JSON document
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
JsonObject json = doc.to<JsonObject>();
if (!export_values(json)) {
return; // empty
}
create_value_json(root, F("collectorTemp"), nullptr, F_(collectorTemp), F_(degrees), json);
create_value_json(root, F("tankBottomTemp"), nullptr, F_(tankBottomTemp), F_(degrees), json);
create_value_json(root, F("tankBottomTemp2"), nullptr, F_(tankBottomTemp2), F_(degrees), json);
create_value_json(root, F("tank1MaxTempCurrent"), nullptr, F_(tank1MaxTempCurrent), F_(degrees), json);
create_value_json(root, F("heatExchangerTemp"), nullptr, F_(heatExchangerTemp), F_(degrees), json);
create_value_json(root, F("solarPumpModulation"), nullptr, F_(solarPumpModulation), F_(percent), json);
create_value_json(root, F("cylinderPumpModulation"), nullptr, F_(cylinderPumpModulation), F_(percent), json);
create_value_json(root, F("valveStatus"), nullptr, F_(valveStatus), nullptr, json);
create_value_json(root, F("solarPump"), nullptr, F_(solarPump), nullptr, json);
create_value_json(root, F("tankHeated"), nullptr, F_(tankHeated), nullptr, json);
create_value_json(root, F("collectorShutdown"), nullptr, F_(collectorShutdown), nullptr, json);
create_value_json(root, F("energyLastHour"), nullptr, F_(energyLastHour), F_(wh), json);
create_value_json(root, F("energyToday"), nullptr, F_(energyToday), F_(wh), json);
create_value_json(root, F("energyTotal"), nullptr, F_(energyTotal), F_(kwh), json);
create_value_json(root, F("pumpWorkMin"), nullptr, F_(pumpWorkMin), F_(min), json);
create_value_json(root, F("pumpWorkMintxt"), nullptr, F_(pumpWorkMintxt), F_(min), json);
}
// publish values via MQTT
void Solar::publish_values(JsonObject & json, bool force) {
// handle HA first
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
if ((!mqtt_ha_config_ || force)) {
register_mqtt_ha_config();
return;
}
}
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_MEDIUM> doc;
JsonObject json_payload = doc.to<JsonObject>();
if (export_values(json_payload)) {
if (device_id() == 0x2A) {
Mqtt::publish(F("ww_data"), doc.as<JsonObject>());
} else {
Mqtt::publish(F("solar_data"), doc.as<JsonObject>());
}
}
}
// publish config topic for HA MQTT Discovery
void Solar::register_mqtt_ha_config() {
if (!Mqtt::connected()) {
return;
}
// Create the Master device
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
doc["name"] = F_(EMSESP);
// publish HA config
bool Solar::publish_ha_config() {
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["name"] = FJSON("Solar Status");
doc["uniq_id"] = F_(solar);
doc["ic"] = F_(iconthermostat);
char stat_t[50];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/solar_data"), System::hostname().c_str());
@@ -133,113 +128,21 @@ void Solar::register_mqtt_ha_config() {
dev["mdl"] = name();
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp-solar");
Mqtt::publish_ha(F("homeassistant/sensor/ems-esp/solar/config"), doc.as<JsonObject>()); // publish the config payload with retain flag
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(collectorTemp), device_type(), "collectorTemp", F_(degrees), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(tankBottomTemp), device_type(), "tankBottomTemp", F_(degrees), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(tankBottomTemp2), device_type(), "tankBottomTemp2", F_(degrees), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(tank1MaxTempCurrent), device_type(), "tank1MaxTempCurrent", F_(degrees), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(heatExchangerTemp), device_type(), "heatExchangerTemp", F_(degrees), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(solarPumpModulation), device_type(), "solarPumpModulation", F_(percent), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(cylinderPumpModulation), device_type(), "cylinderPumpModulation", F_(percent), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(pumpWorkMin), device_type(), "pumpWorkMin", F_(min), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(energyLastHour), device_type(), "energyLastHour", F_(wh), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(energyToday), device_type(), "energyToday", F_(wh), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(energyTotal), device_type(), "energyTotal", F_(kwh), nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(solarPump), device_type(), "solarPump", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(valveStatus), device_type(), "valveStatus", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(tankHeated), device_type(), "tankHeated", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(collectorShutdown), device_type(), "collectorShutdown", nullptr, nullptr);
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/solar/config"), System::hostname().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
mqtt_ha_config_ = true; // done
}
// creates JSON doc from values
// returns false if empty
bool Solar::export_values(JsonObject & json) {
char s[10]; // for formatting strings
if (Helpers::hasValue(collectorTemp_)) {
json["collectorTemp"] = (float)collectorTemp_ / 10;
}
if (Helpers::hasValue(tankBottomTemp_)) {
json["tankBottomTemp"] = (float)tankBottomTemp_ / 10;
}
if (Helpers::hasValue(tankBottomTemp2_)) {
json["tankBottomTemp2"] = (float)tankBottomTemp2_ / 10;
}
if (Helpers::hasValue(tank1MaxTempCurrent_)) {
json["tank1MaxTempCurrent"] = tank1MaxTempCurrent_;
}
if (Helpers::hasValue(heatExchangerTemp_)) {
json["heatExchangerTemp"] = (float)heatExchangerTemp_ / 10;
}
if (Helpers::hasValue(solarPumpModulation_)) {
json["solarPumpModulation"] = solarPumpModulation_;
}
if (Helpers::hasValue(cylinderPumpModulation_)) {
json["cylinderPumpModulation"] = cylinderPumpModulation_;
}
if (Helpers::hasValue(solarPump_, EMS_VALUE_BOOL)) {
json["solarPump"] = Helpers::render_value(s, solarPump_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(valveStatus_, EMS_VALUE_BOOL)) {
json["valveStatus"] = Helpers::render_value(s, valveStatus_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(pumpWorkMin_)) {
json["pumpWorkMin"] = pumpWorkMin_;
char slong[40];
json["pumpWorkMintxt"] = Helpers::render_value(slong, pumpWorkMin_, EMS_VALUE_TIME);
}
if (Helpers::hasValue(tankHeated_, EMS_VALUE_BOOL)) {
json["tankHeated"] = Helpers::render_value(s, tankHeated_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(collectorShutdown_, EMS_VALUE_BOOL)) {
json["collectorShutdown"] = Helpers::render_value(s, collectorShutdown_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(energyLastHour_)) {
json["energyLastHour"] = (float)energyLastHour_ / 10;
}
if (Helpers::hasValue(energyToday_)) {
json["energyToday"] = energyToday_;
}
if (Helpers::hasValue(energyTotal_)) {
json["energyTotal"] = (float)energyTotal_ / 10;
}
return json.size();
}
// check to see if values have been updated
bool Solar::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
return true;
}
// SM10Monitor - type 0x97
void Solar::process_SM10Monitor(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(collectorTemp_, 2); // collector temp from SM10, is *10
changed_ |= telegram->read_value(tankBottomTemp_, 5); // bottom temp from SM10, is *10
changed_ |= telegram->read_value(solarPumpModulation_, 4); // modulation solar pump
changed_ |= telegram->read_bitvalue(solarPump_, 7, 1);
changed_ |= telegram->read_value(pumpWorkMin_, 8, 3);
has_update(telegram->read_value(collectorTemp_, 2)); // collector temp from SM10, is *10
has_update(telegram->read_value(tankBottomTemp_, 5)); // bottom temp from SM10, is *10
has_update(telegram->read_value(solarPumpModulation_, 4)); // modulation solar pump
has_update(telegram->read_bitvalue(solarPump_, 7, 1));
has_update(telegram->read_value(pumpWorkMin_, 8, 3));
}
/*
@@ -247,11 +150,11 @@ void Solar::process_SM10Monitor(std::shared_ptr<const Telegram> telegram) {
* e.g. B0 0B FF 00 02 58 FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 00 FF 01 00 00
*/
void Solar::process_SM100SystemConfig(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(heatTransferSystem_, 5, 1);
changed_ |= telegram->read_value(externalTank_, 9, 1);
changed_ |= telegram->read_value(thermalDisinfect_, 10, 1);
changed_ |= telegram->read_value(heatMetering_, 14, 1);
changed_ |= telegram->read_value(solarIsEnabled_, 19, 1);
has_update(telegram->read_value(heatTransferSystem_, 5, 1));
has_update(telegram->read_value(externalTank_, 9, 1));
has_update(telegram->read_value(thermalDisinfect_, 10, 1));
has_update(telegram->read_value(heatMetering_, 14, 1));
has_update(telegram->read_value(solarIsEnabled_, 19, 1));
}
/*
@@ -259,16 +162,16 @@ void Solar::process_SM100SystemConfig(std::shared_ptr<const Telegram> telegram)
* e.g. B0 0B FF 00 02 5A 64 05 00 58 14 01 01 32 64 00 00 00 5A 0C
*/
void Solar::process_SM100SolarCircuitConfig(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(collectorTempMax_, 0, 1);
changed_ |= telegram->read_value(tank1MaxTempCurrent_, 3, 1);
changed_ |= telegram->read_value(collectorTempMin_, 4, 1);
changed_ |= telegram->read_value(solarPumpMode_, 5, 1);
changed_ |= telegram->read_value(solarPumpMinRPM_, 6, 1);
changed_ |= telegram->read_value(solarPumpTurnoffDiff_, 7, 1);
changed_ |= telegram->read_value(solarPumpTurnonDiff_, 8, 1);
changed_ |= telegram->read_value(solarPumpKick_, 9, 1);
changed_ |= telegram->read_value(plainWaterMode_, 10, 1);
changed_ |= telegram->read_value(doubleMatchFlow_, 11, 1);
has_update(telegram->read_value(collectorTempMax_, 0, 1));
has_update(telegram->read_value(tank1MaxTempCurrent_, 3, 1));
has_update(telegram->read_value(collectorTempMin_, 4, 1));
has_update(telegram->read_value(solarPumpMode_, 5, 1));
has_update(telegram->read_value(solarPumpMinRPM_, 6, 1));
has_update(telegram->read_value(solarPumpTurnoffDiff_, 7, 1));
has_update(telegram->read_value(solarPumpTurnonDiff_, 8, 1));
has_update(telegram->read_value(solarPumpKick_, 9, 1));
has_update(telegram->read_value(plainWaterMode_, 10, 1));
has_update(telegram->read_value(doubleMatchFlow_, 11, 1));
}
/* process_SM100ParamCfg - type 0xF9 EMS 1.0
@@ -290,14 +193,14 @@ void Solar::process_SM100ParamCfg(std::shared_ptr<const Telegram> telegram) {
uint16_t t_id;
uint8_t of;
int32_t min, def, max, cur;
telegram->read_value(t_id, 1);
telegram->read_value(of, 3);
telegram->read_value(min, 5);
telegram->read_value(def, 9);
telegram->read_value(max, 13);
telegram->read_value(cur, 17);
has_update(telegram->read_value(t_id, 1));
has_update(telegram->read_value(of, 3));
has_update(telegram->read_value(min, 5));
has_update(telegram->read_value(def, 9));
has_update(telegram->read_value(max, 13));
has_update(telegram->read_value(cur, 17));
// LOG_DEBUG(F("SM100ParamCfg param=0x%04X, offset=%d, min=%d, default=%d, max=%d, current=%d"), t_id, of, min, def, max, cur);
// LOG_DEBUG(F("SM100ParamCfg param=0x%04X, offset=%d, min=%d, default=%d, max=%d, current=%d"), t_id, of, min, def, max, cur));
}
/*
@@ -312,10 +215,10 @@ void Solar::process_SM100ParamCfg(std::shared_ptr<const Telegram> telegram) {
* bytes 20+21 = TS6 Temperature sensor external heat exchanger
*/
void Solar::process_SM100Monitor(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(collectorTemp_, 0); // is *10 - TS1: Temperature sensor for collector array 1
changed_ |= telegram->read_value(tankBottomTemp_, 2); // is *10 - TS2: Temperature sensor 1 cylinder, bottom
changed_ |= telegram->read_value(tankBottomTemp2_, 16); // is *10 - TS5: Temperature sensor 2 cylinder, bottom, or swimming pool
changed_ |= telegram->read_value(heatExchangerTemp_, 20); // is *10 - TS6: Heat exchanger temperature sensor
has_update(telegram->read_value(collectorTemp_, 0)); // is *10 - TS1: Temperature sensor for collector array 1
has_update(telegram->read_value(tankBottomTemp_, 2)); // is *10 - TS2: Temperature sensor 1 cylinder, bottom
has_update(telegram->read_value(tankBottomTemp2_, 16)); // is *10 - TS5: Temperature sensor 2 cylinder, bottom, or swimming pool
has_update(telegram->read_value(heatExchangerTemp_, 20)); // is *10 - TS6: Heat exchanger temperature sensor
}
#pragma GCC diagnostic push
@@ -330,17 +233,17 @@ void Solar::process_SM100Monitor2(std::shared_ptr<const Telegram> telegram) {
// SM100wwTemperatur - 0x07D6
// Solar Module(0x2A) -> (0x00), (0x7D6), data: 01 C1 00 00 02 5B 01 AF 01 AD 80 00 01 90
void Solar::process_SM100wwTemperature(std::shared_ptr<const Telegram> telegram) {
// changed_ |= telegram->read_value(wwTemp_1_, 0);
// changed_ |= telegram->read_value(wwTemp_3_, 4);
// changed_ |= telegram->read_value(wwTemp_4_, 6);
// changed_ |= telegram->read_value(wwTemp_5_, 8);
// changed_ |= telegram->read_value(wwTemp_7_, 12);
// has_update(telegram->read_value(wwTemp_1_, 0));
// has_update(telegram->read_value(wwTemp_3_, 4));
// has_update(telegram->read_value(wwTemp_4_, 6));
// has_update(telegram->read_value(wwTemp_5_, 8));
// has_update(telegram->read_value(wwTemp_7_, 12));
}
// SM100wwStatus - 0x07AA
// Solar Module(0x2A) -> (0x00), (0x7AA), data: 64 00 04 00 03 00 28 01 0F
void Solar::process_SM100wwStatus(std::shared_ptr<const Telegram> telegram) {
// changed_ |= telegram->read_value(wwPump_, 0);
// has_update(telegram->read_value(wwPump_, 0));
}
// SM100wwCommand - 0x07AB
@@ -349,15 +252,14 @@ void Solar::process_SM100wwCommand(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) {
changed_ |= telegram->read_value(availabilityFlag_, 0);
changed_ |= telegram->read_value(configFlag_, 1);
changed_ |= telegram->read_value(userFlag_, 2);
has_update(telegram->read_value(availabilityFlag_, 0));
has_update(telegram->read_value(configFlag_, 1));
has_update(telegram->read_value(userFlag_, 2));
}
/*
@@ -369,8 +271,8 @@ void Solar::process_SM100Config(std::shared_ptr<const Telegram> telegram) {
void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
uint8_t solarpumpmod = solarPumpModulation_;
uint8_t cylinderpumpmod = cylinderPumpModulation_;
changed_ |= telegram->read_value(cylinderPumpModulation_, 8);
changed_ |= telegram->read_value(solarPumpModulation_, 9);
has_update(telegram->read_value(cylinderPumpModulation_, 8));
has_update(telegram->read_value(solarPumpModulation_, 9));
if (solarpumpmod == 0 && solarPumpModulation_ == 100) { // mask out boosts
solarPumpModulation_ = 15; // set to minimum
@@ -379,8 +281,8 @@ void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
if (cylinderpumpmod == 0 && cylinderPumpModulation_ == 100) { // mask out boosts
cylinderPumpModulation_ = 15; // set to minimum
}
changed_ |= telegram->read_bitvalue(tankHeated_, 3, 1); // issue #422
changed_ |= telegram->read_bitvalue(collectorShutdown_, 3, 0); // collector shutdown
has_update(telegram->read_bitvalue(tankHeated_, 3, 1)); // issue #422
has_update(telegram->read_bitvalue(collectorShutdown_, 3, 0)); // collector shutdown
}
/*
@@ -390,8 +292,8 @@ void Solar::process_SM100Status(std::shared_ptr<const Telegram> telegram) {
* 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) {
changed_ |= telegram->read_bitvalue(valveStatus_, 4, 2); // on if bit 2 set
changed_ |= telegram->read_bitvalue(solarPump_, 10, 2); // on if bit 2 set
has_update(telegram->read_bitvalue(valveStatus_, 4, 2)); // on if bit 2 set
has_update(telegram->read_bitvalue(solarPump_, 10, 2)); // on if bit 2 set
}
/*
@@ -399,9 +301,9 @@ void Solar::process_SM100Status2(std::shared_ptr<const Telegram> telegram) {
* e.g. B0 0B FF 00 02 80 50 64 00 00 29 01 00 00 01
*/
void Solar::process_SM100CollectorConfig(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(climateZone_, 0, 1);
changed_ |= telegram->read_value(collector1Area_, 3, 2);
changed_ |= telegram->read_value(collector1Type_, 5, 1);
has_update(telegram->read_value(climateZone_, 0, 1));
has_update(telegram->read_value(collector1Area_, 3, 2));
has_update(telegram->read_value(collector1Type_, 5, 1));
}
/*
@@ -409,16 +311,16 @@ void Solar::process_SM100CollectorConfig(std::shared_ptr<const Telegram> telegra
* 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) {
changed_ |= telegram->read_value(energyLastHour_, 0); // last hour / 10 in Wh
changed_ |= telegram->read_value(energyToday_, 4); // todays in Wh
changed_ |= telegram->read_value(energyTotal_, 8); // total / 10 in kWh
has_update(telegram->read_value(energyLastHour_, 0)); // last hour / 10 in Wh
has_update(telegram->read_value(energyToday_, 4)); // todays in Wh
has_update(telegram->read_value(energyTotal_, 8)); // total / 10 in kWh
}
/*
* SM100Time - type 0x0391 EMS+ for pump working time
*/
void Solar::process_SM100Time(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(pumpWorkMin_, 1, 3);
has_update(telegram->read_value(pumpWorkMin_, 1, 3));
}
/*
@@ -426,26 +328,26 @@ void Solar::process_SM100Time(std::shared_ptr<const Telegram> telegram) {
* 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) {
changed_ |= telegram->read_value(collectorTemp_, 4); // Collector Temperature
changed_ |= telegram->read_value(tankBottomTemp_, 6); // Temperature Bottom of Solar Boiler
has_update(telegram->read_value(collectorTemp_, 4)); // Collector Temperature
has_update(telegram->read_value(tankBottomTemp_, 6)); // Temperature Bottom of Solar Boiler
uint16_t Wh = 0xFFFF;
changed_ |= telegram->read_value(Wh, 2); // Solar Energy produced in last hour only ushort, is not * 10
has_update(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
}
changed_ |= telegram->read_bitvalue(solarPump_, 8, 0); // PS1 Solar pump on (1) or off (0)
changed_ |= telegram->read_value(pumpWorkMin_, 10, 3); // force to 3 bytes
changed_ |= telegram->read_bitvalue(collectorShutdown_, 9, 0); // collector shutdown on/off
changed_ |= telegram->read_bitvalue(tankHeated_, 9, 2); // tank full
has_update(telegram->read_bitvalue(solarPump_, 8, 0)); // PS1 Solar pump on (1) or off (0)
has_update(telegram->read_value(pumpWorkMin_, 10, 3)); // force to 3 bytes
has_update(telegram->read_bitvalue(collectorShutdown_, 9, 0)); // collector shutdown on/off
has_update(telegram->read_bitvalue(tankHeated_, 9, 2)); // tank full
}
/*
* Junkers ISM1 Solar Module - type 0x0101 EMS+ for setting values
*/
void Solar::process_ISM1Set(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(setpoint_maxBottomTemp_, 6);
has_update(telegram->read_value(setpoint_maxBottomTemp_, 6));
}
// set temperature for tank

View File

@@ -36,61 +36,56 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
void register_mqtt_ha_config();
int16_t collectorTemp_ = EMS_VALUE_SHORT_NOTSET; // TS1: Temperature sensor for collector array 1
int16_t tankBottomTemp_ = EMS_VALUE_SHORT_NOTSET; // TS2: Temperature sensor 1 cylinder, bottom (solar thermal system)
int16_t tankBottomTemp2_ = EMS_VALUE_SHORT_NOTSET; // TS5: Temperature sensor 2 cylinder, bottom, or swimming pool (solar thermal system)
int16_t heatExchangerTemp_ = EMS_VALUE_SHORT_NOTSET; // TS6: Heat exchanger temperature sensor
uint8_t solarPumpModulation_ = EMS_VALUE_UINT_NOTSET; // PS1: modulation solar pump
uint8_t cylinderPumpModulation_ = EMS_VALUE_UINT_NOTSET; // PS5: modulation cylinder pump
uint8_t solarPump_ = EMS_VALUE_BOOL_NOTSET; // PS1: solar pump active
uint8_t valveStatus_ = EMS_VALUE_BOOL_NOTSET; // VS2: status 3-way valve for cylinder 2 (solar thermal system) with valve
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 tankHeated_ = EMS_VALUE_BOOL_NOTSET;
uint8_t collectorShutdown_ = EMS_VALUE_BOOL_NOTSET; // Collector shutdown on/off
int16_t collectorTemp_; // TS1: Temperature sensor for collector array 1
int16_t tankBottomTemp_; // TS2: Temperature sensor 1 cylinder, bottom (solar thermal system)
int16_t tankBottomTemp2_; // TS5: Temperature sensor 2 cylinder, bottom, or swimming pool (solar thermal system)
int16_t heatExchangerTemp_; // TS6: Heat exchanger temperature sensor
uint8_t solarPumpModulation_; // PS1: modulation solar pump
uint8_t cylinderPumpModulation_; // PS5: modulation cylinder pump
uint8_t solarPump_; // PS1: solar pump active
uint8_t valveStatus_; // VS2: status 3-way valve for cylinder 2 (solar thermal system) with valve
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
uint8_t tankHeated_;
uint8_t collectorShutdown_; // Collector shutdown on/off
uint8_t availabilityFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t configFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t userFlag_ = EMS_VALUE_BOOL_NOTSET;
uint8_t availabilityFlag_;
uint8_t configFlag_;
uint8_t userFlag_;
// telegram 0x0358
uint8_t heatTransferSystem_ = EMS_VALUE_UINT_NOTSET; // Umladesystem, 00=no
uint8_t externalTank_ = EMS_VALUE_UINT_NOTSET; // Heat exchanger, 00=no
uint8_t thermalDisinfect_ = EMS_VALUE_UINT_NOTSET; // Daily heatup for disinfection, 00=no
uint8_t heatMetering_ = EMS_VALUE_UINT_NOTSET; // Wärmemengenzählung, 00=no
uint8_t solarIsEnabled_ = EMS_VALUE_UINT_NOTSET; // System enable, 00=no
uint8_t heatTransferSystem_; // Umladesystem, 00=no
uint8_t externalTank_; // Heat exchanger, 00=no
uint8_t thermalDisinfect_; // Daily heatup for disinfection, 00=no
uint8_t heatMetering_; // Wärmemengenzählung, 00=no
uint8_t solarIsEnabled_; // System enable, 00=no
// telegram 0x035A
uint8_t collectorTempMax_ = EMS_VALUE_UINT_NOTSET; // maximum allowable temperature for collector
uint8_t tank1MaxTempCurrent_ = EMS_VALUE_UINT_NOTSET; // Current value for max tank temp
uint8_t collectorTempMin_ = EMS_VALUE_UINT_NOTSET; // minimum allowable temperature for collector
uint8_t solarPumpMode_ = EMS_VALUE_UINT_NOTSET; // 00=off, 01=PWM, 02=10V
uint8_t solarPumpMinRPM_ = EMS_VALUE_UINT_NOTSET; // minimum RPM setting, *5 %
uint8_t solarPumpTurnoffDiff_ = EMS_VALUE_UINT_NOTSET; // solar pump turnoff collector/tank diff
uint8_t solarPumpTurnonDiff_ = EMS_VALUE_UINT_NOTSET; // solar pump turnon collector/tank diff
uint8_t solarPumpKick_ = EMS_VALUE_UINT_NOTSET; // pump kick for vacuum collector, 00=off
uint8_t plainWaterMode_ = EMS_VALUE_UINT_NOTSET; // system does not use antifreeze, 00=off
uint8_t doubleMatchFlow_ = EMS_VALUE_UINT_NOTSET; // double Match Flow, 00=off
uint8_t collectorTempMax_; // maximum allowable temperature for collector
uint8_t tank1MaxTempCurrent_; // Current value for max tank temp
uint8_t collectorTempMin_; // minimum allowable temperature for collector
uint8_t solarPumpMode_; // 00=off, 01=PWM, 02=10V
uint8_t solarPumpMinRPM_; // minimum RPM setting, *5 %
uint8_t solarPumpTurnoffDiff_; // solar pump turnoff collector/tank diff
uint8_t solarPumpTurnonDiff_; // solar pump turnon collector/tank diff
uint8_t solarPumpKick_; // pump kick for vacuum collector, 00=off
uint8_t plainWaterMode_; // system does not use antifreeze, 00=off
uint8_t doubleMatchFlow_; // double Match Flow, 00=off
// telegram 0x380
uint8_t climateZone_ = EMS_VALUE_UINT_NOTSET; // climate zone identifier
uint16_t collector1Area_ = EMS_VALUE_USHORT_NOTSET; // Area of collector field 1
uint8_t collector1Type_ = EMS_VALUE_UINT_NOTSET; // Type of collector field 1, 01=flat, 02=vacuum
uint8_t climateZone_; // climate zone identifier
uint16_t collector1Area_; // Area of collector field 1
uint8_t collector1Type_; // Type of collector field 1, 01=flat, 02=vacuum
bool changed_ = false;
bool mqtt_ha_config_ = false; // for HA MQTT Discovery
char type_[20]; // Solar of WWC
void process_SM10Monitor(std::shared_ptr<const Telegram> telegram);
void process_SM100SystemConfig(std::shared_ptr<const Telegram> telegram);

View File

@@ -32,92 +32,30 @@ Switch::Switch(uint8_t device_type, uint8_t device_id, uint8_t product_id, const
register_telegram_type(0x9C, F("WM10MonitorMessage"), false, [&](std::shared_ptr<const Telegram> t) { process_WM10MonitorMessage(t); });
register_telegram_type(0x9B, F("WM10SetMessage"), false, [&](std::shared_ptr<const Telegram> t) { process_WM10SetMessage(t); });
std::string empty("");
register_device_value(empty, &activated_, DeviceValueType::BOOL, {}, F("activated"), F("Activated"), DeviceValueUOM::NONE);
register_device_value(
empty, &flowTemp_, DeviceValueType::USHORT, flash_string_vector{F("10")}, F("flowTemp"), F("Current flow temperature"), DeviceValueUOM::DEGREES);
register_device_value(empty, &status_, DeviceValueType::INT, {}, F("status"), F("Status"), DeviceValueUOM::NONE);
}
// fetch the values into a JSON document for display in the web
void Switch::device_info_web(JsonArray & root) {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json = doc.to<JsonObject>();
if (export_values(json)) {
create_value_json(root, F("activated"), nullptr, F_(activated), nullptr, json);
create_value_json(root, F("flowTemp"), nullptr, F_(flowTemp), F_(degrees), json);
create_value_json(root, F("status"), nullptr, F_(status), nullptr, json);
}
}
// publish values via MQTT
void Switch::publish_values(JsonObject & json, bool force) {
if (Mqtt::mqtt_format() == Mqtt::Format::HA) {
if (!mqtt_ha_config_ || force) {
register_mqtt_ha_config();
return;
}
}
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
JsonObject json_data = doc.to<JsonObject>();
if (export_values(json_data)) {
Mqtt::publish(F("switch_data"), doc.as<JsonObject>());
}
}
// export values to JSON
bool Switch::export_values(JsonObject & json) {
if (Helpers::hasValue(flowTemp_)) {
char s[7];
json["activated"] = Helpers::render_value(s, activated_, EMS_VALUE_BOOL);
}
if (Helpers::hasValue(flowTemp_)) {
json["flowTemp"] = (float)flowTemp_ / 10;
}
if (Helpers::hasValue(flowTemp_)) {
json["status"] = status_;
}
return true;
}
// check to see if values have been updated
bool Switch::updated_values() {
if (changed_) {
changed_ = false;
return true;
}
return false;
}
// publish config topic for HA MQTT Discovery
void Switch::register_mqtt_ha_config() {
if (!Mqtt::connected()) {
return;
}
// if we don't have valid values for this HC don't add it ever again
// publish HA config
bool Switch::publish_ha_config() {
// if we don't have valid values don't add it ever again
if (!Helpers::hasValue(flowTemp_)) {
return;
return false;
}
// Create the Master device
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
char name[10];
snprintf_P(name, sizeof(name), PSTR("Switch"));
doc["name"] = name;
char uniq_id[10];
snprintf_P(uniq_id, sizeof(uniq_id), PSTR("switch"));
doc["uniq_id"] = uniq_id;
doc["ic"] = FJSON("mdi:home-thermometer-outline");
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["uniq_id"] = F_(switch);
char stat_t[50];
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/switch_data"), System::hostname().c_str());
doc["stat_t"] = stat_t;
doc["name"] = FJSON("Type");
doc["val_tpl"] = FJSON("{{value_json.type}}"); // HA needs a single value. We take the type which is wwc or hc
JsonObject dev = doc.createNestedObject("dev");
dev["name"] = FJSON("EMS-ESP Switch");
dev["sw"] = EMSESP_APP_VERSION;
@@ -126,24 +64,22 @@ void Switch::register_mqtt_ha_config() {
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp-switch");
Mqtt::publish_ha(F("homeassistant/sensor/ems-esp/switch/config"), doc.as<JsonObject>()); // publish the config payload with retain flag
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/switch/config"), System::hostname().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(activated), device_type(), "activated", nullptr, nullptr);
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(flowTemp), device_type(), "flowTemp", F_(degrees), F_(icontemperature));
Mqtt::register_mqtt_ha_sensor(nullptr, nullptr, F_(status), device_type(), "status", nullptr, nullptr);
mqtt_ha_config_ = true; // done
return true;
}
// message 0x9B switch on/off
void Switch::process_WM10SetMessage(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(activated_, 0);
has_update(telegram->read_value(activated_, 0));
}
// message 0x9C holds flowtemp and unknown status value
void Switch::process_WM10MonitorMessage(std::shared_ptr<const Telegram> telegram) {
changed_ |= telegram->read_value(flowTemp_, 0); // is * 10
changed_ |= telegram->read_value(status_, 2);
has_update(telegram->read_value(flowTemp_, 0)); // is * 10
has_update(telegram->read_value(status_, 2));
}
} // namespace emsesp

View File

@@ -36,23 +36,17 @@ 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 publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
void process_WM10SetMessage(std::shared_ptr<const Telegram> telegram);
void process_WM10MonitorMessage(std::shared_ptr<const Telegram> telegram);
void register_mqtt_ha_config();
uint16_t flowTemp_ = EMS_VALUE_USHORT_NOTSET;
uint8_t status_ = EMS_VALUE_UINT_NOTSET;
uint8_t activated_ = EMS_VALUE_BOOL_NOTSET;
bool changed_ = false;
bool mqtt_ha_config_ = false; // for HA MQTT Discovery
uint16_t flowTemp_;
uint8_t status_;
uint8_t activated_;
};
} // namespace emsesp

File diff suppressed because it is too large Load Diff

View File

@@ -40,44 +40,43 @@ class Thermostat : public EMSdevice {
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)
HeatingCircuit(const uint8_t hc_num, const uint8_t model)
: hc_num_(hc_num)
, ha_registered_(false) {
, model_(model) {
}
~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;
int8_t nofrosttemp = EMS_VALUE_INT_NOTSET; // signed -20°C to +10°C
uint8_t designtemp = EMS_VALUE_UINT_NOTSET; // heating curve design temp at MinExtTemp
int8_t offsettemp = EMS_VALUE_INT_NOTSET; // heating curve offest temp at roomtemp signed!
uint8_t manualtemp = EMS_VALUE_UINT_NOTSET;
uint8_t summer_setmode = EMS_VALUE_UINT_NOTSET;
uint8_t roominfluence = EMS_VALUE_UINT_NOTSET;
uint8_t flowtempoffset = EMS_VALUE_UINT_NOTSET;
uint8_t minflowtemp = EMS_VALUE_UINT_NOTSET;
uint8_t maxflowtemp = EMS_VALUE_UINT_NOTSET;
int16_t setpoint_roomTemp;
int16_t curr_roomTemp;
uint8_t mode;
uint8_t modetype;
uint8_t summermode;
uint8_t holidaymode;
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 targetflowtemp;
uint8_t summertemp;
int8_t nofrosttemp; // signed -20°C to +10°C
uint8_t designtemp; // heating curve design temp at MinExtTemp
int8_t offsettemp; // heating curve offest temp at roomtemp signed!
uint8_t manualtemp;
uint8_t summer_setmode;
uint8_t roominfluence;
uint8_t flowtempoffset;
uint8_t minflowtemp;
uint8_t maxflowtemp;
uint8_t reducemode;
uint8_t program;
uint8_t controlmode;
uint8_t hc_num() const {
return hc_num_;
}
bool ha_registered() const {
return ha_registered_;
}
void ha_registered(bool b) {
ha_registered_ = b;
uint8_t get_model() const {
return model_;
}
// determines if the heating circuit is actually present and has data
@@ -85,11 +84,10 @@ class Thermostat : public EMSdevice {
return Helpers::hasValue(setpoint_roomTemp);
}
uint8_t get_mode(uint8_t model) const;
uint8_t get_mode_type(uint8_t model) const;
uint8_t get_mode() const;
uint8_t get_mode_type() const;
enum Mode : uint8_t {
UNKNOWN,
OFF,
MANUAL,
AUTO,
@@ -106,7 +104,8 @@ class Thermostat : public EMSdevice {
FLOWOFFSET,
MINFLOW,
MAXFLOW,
ROOMINFLUENCE
ROOMINFLUENCE,
UNKNOWN
};
// for sorting based on hc number
@@ -115,31 +114,21 @@ class Thermostat : public EMSdevice {
}
private:
uint8_t hc_num_; // heating circuit number 1..10
bool ha_registered_; // whether it has been registered for HA MQTT Discovery
uint8_t hc_num_; // heating circuit number 1..10
uint8_t model_; // the model type
};
static std::string mode_tostring(uint8_t mode);
virtual void publish_values(JsonObject & json, bool force);
virtual bool export_values(JsonObject & json);
virtual void device_info_web(JsonArray & root);
virtual bool updated_values();
virtual bool publish_ha_config();
private:
static uuid::log::Logger logger_;
void add_commands();
bool export_values_main(JsonObject & doc);
bool export_values_hc(uint8_t mqtt_format, JsonObject & doc);
bool ha_registered() const {
return ha_registered_;
}
void ha_registered(bool b) {
ha_registered_ = b;
}
void register_device_values();
void register_device_values(uint8_t hc_num);
// specific thermostat characteristics, stripping the top 4 bits
inline uint8_t model() const {
@@ -153,40 +142,38 @@ class Thermostat : public EMSdevice {
std::vector<uint16_t> summer_typeids;
std::vector<uint16_t> curve_typeids;
std::string datetime_; // date and time stamp
std::string errorCode_; // code from 0xA2 as string i.e. "A22(816)"
bool changed_ = false;
bool ha_registered_ = false;
char dateTime_[25]; // date and time stamp
char errorCode_[15]; // code from 0xA2 as string i.e. "A22(816)"
// 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
uint8_t ibaMainDisplay_; // 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_; // language on Thermostat: 0 german, 1 dutch, 2 french, 3 italian
int8_t ibaCalIntTemperature_; // offset int. temperature sensor, by * 0.1 Kelvin (-5.0 to 5.0K)
int8_t ibaMinExtTemperature_; // min ext temp for heating curve, in deg., 0xF6=-10, 0x0 = 0, 0xFF=-1
uint8_t ibaBuildingType_; // building type: 0 = light, 1 = medium, 2 = heavy
uint8_t ibaClockOffset_; // offset (in sec) to clock, 0xff = -1 s, 0x02 = 2 s
uint16_t errorNumber_ = EMS_VALUE_USHORT_NOTSET;
char lastCode_[30] = {'\0'};
int8_t dampedoutdoortemp_ = EMS_VALUE_INT_NOTSET;
uint16_t tempsensor1_ = EMS_VALUE_USHORT_NOTSET;
uint16_t tempsensor2_ = EMS_VALUE_USHORT_NOTSET;
int16_t dampedoutdoortemp2_ = EMS_VALUE_SHORT_NOTSET;
uint8_t floordrystatus_ = EMS_VALUE_UINT_NOTSET;
uint8_t floordrytemp_ = EMS_VALUE_UINT_NOTSET;
uint16_t errorNumber_;
char lastCode_[30];
int8_t dampedoutdoortemp_;
uint16_t tempsensor1_;
uint16_t tempsensor2_;
int16_t dampedoutdoortemp2_;
uint8_t floordrystatus_;
uint8_t floordrytemp_;
uint8_t wwExtra1_ = EMS_VALUE_UINT_NOTSET; // wwExtra active for wwSystem 1
uint8_t wwExtra2_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwMode_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwCircPump_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwCircMode_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwTemp_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwTempLow_ = EMS_VALUE_UINT_NOTSET;
uint8_t wwExtra1_; // wwExtra active for wwSystem 1
uint8_t wwExtra2_;
uint8_t wwMode_;
uint8_t wwCircPump_;
uint8_t wwCircMode_;
uint8_t wwTemp_;
uint8_t wwTempLow_;
std::vector<std::shared_ptr<HeatingCircuit>> heating_circuits_; // each thermostat can have multiple heating circuits
uint8_t zero_value_ = 0; // for fixing current room temperature to 0 for HA
// 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
@@ -266,9 +253,9 @@ class Thermostat : public EMSdevice {
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 register_mqtt_ha_config();
void register_mqtt_ha_config(uint8_t hc_num);
bool ha_config(bool force = false);
void register_mqtt_ha_config_hc(uint8_t hc_num);
void register_device_values_hc(std::shared_ptr<emsesp::Thermostat::HeatingCircuit> hc);
bool thermostat_ha_cmd(const char * message, uint8_t hc_num);
void process_RCOutdoorTemp(std::shared_ptr<const Telegram> telegram);
@@ -279,6 +266,7 @@ class Thermostat : public EMSdevice {
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_RC35Timer(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);
@@ -335,6 +323,9 @@ class Thermostat : public EMSdevice {
bool set_flowtempoffset(const char * value, const int8_t id);
bool set_minflowtemp(const char * value, const int8_t id);
bool set_maxflowtemp(const char * value, const int8_t id);
bool set_reducemode(const char * value, const int8_t id);
bool set_program(const char * value, const int8_t id);
bool set_controlmode(const char * value, const int8_t id);
// set functions - these don't use the id/hc, the parameters are ignored
bool set_wwmode(const char * value, const int8_t id);
@@ -353,4 +344,4 @@ class Thermostat : public EMSdevice {
} // namespace emsesp
#endif
#endif

View File

@@ -21,6 +21,32 @@
namespace emsesp {
// mapping of UOM, to match order in DeviceValueUOM enum
static const __FlashStringHelper * DeviceValueUOM_s[(uint8_t)10] __attribute__((__aligned__(sizeof(int)))) PROGMEM = {
F_(degrees),
F_(percent),
F_(lmin),
F_(kwh),
F_(wh),
F_(hours),
F_(minutes),
F_(ua),
F_(bar)
};
const __FlashStringHelper * EMSdevice::uom_to_string(uint8_t uom) {
if (uom == DeviceValueUOM::NONE) {
return nullptr;
}
return DeviceValueUOM_s[uom];
}
const std::vector<EMSdevice::DeviceValue> EMSdevice::devicevalues() const {
return devicevalues_;
}
std::string EMSdevice::brand_to_string() const {
switch (brand_) {
case EMSdevice::Brand::BOSCH:
@@ -276,7 +302,6 @@ char * EMSdevice::show_telegram_handlers(char * result) {
return result;
}
// list all the mqtt handlers for this device
void EMSdevice::show_mqtt_handlers(uuid::console::Shell & shell) {
Mqtt::show_topic_handlers(shell, device_type_);
@@ -296,6 +321,317 @@ void EMSdevice::register_telegram_type(const uint16_t telegram_type_id, const __
telegram_functions_.emplace_back(telegram_type_id, telegram_type_name, fetch, f);
}
// add to device value library
// arguments are:
// tag: to be used to group mqtt together, either as separate topics as a nested object
// value: pointer to the value from the .h file
// type: one of DeviceValueType
// options: options for enum or a divider for int (e.g. F("10"))
// short_name: used in Mqtt as keys
// full name: used in Web and Console
// uom: unit of measure from DeviceValueUOM
// icon (optional): the HA mdi icon to use, from locale_*.h file
void EMSdevice::register_device_value(std::string & tag,
void * value_p,
uint8_t type,
const flash_string_vector & options,
const __FlashStringHelper * short_name,
const __FlashStringHelper * full_name,
uint8_t uom,
const __FlashStringHelper * icon) {
// init the value depending on it's type
if (type == DeviceValueType::TEXT) {
*(char *)(value_p) = {'\0'};
} else if (type == DeviceValueType::INT) {
*(int8_t *)(value_p) = EMS_VALUE_INT_NOTSET;
} else if (type == DeviceValueType::SHORT) {
*(int16_t *)(value_p) = EMS_VALUE_SHORT_NOTSET;
} else if (type == DeviceValueType::USHORT) {
*(uint16_t *)(value_p) = EMS_VALUE_USHORT_NOTSET;
} else if ((type == DeviceValueType::ULONG) || (type == DeviceValueType::TIME)) {
*(uint32_t *)(value_p) = EMS_VALUE_ULONG_NOTSET;
} else {
// enums, uint8_t, bool behave as uint8_t
*(uint8_t *)(value_p) = EMS_VALUE_UINT_NOTSET;
}
// add to our library
devicevalues_.emplace_back(device_type_, tag, value_p, type, options, short_name, full_name, uom, icon);
}
// looks up the uom (suffix) for a given key from the device value table
std::string EMSdevice::get_value_uom(const char * key) {
// the key may have a suffix at the start which is between brackets. remove it.
char new_key[80];
strncpy(new_key, key, sizeof(new_key));
char * p = new_key;
if (key[0] == '(') {
while ((*p++ != ')') && (*p != '\0'))
;
p++;
}
for (const auto & dv : devicevalues_) {
if (dv.full_name != nullptr) {
if (uuid::read_flash_string(dv.full_name) == p) {
// ignore TIME since "minutes" is already included
if ((dv.uom == DeviceValueUOM::NONE) || (dv.uom == DeviceValueUOM::MINUTES)) {
break;
}
return uuid::read_flash_string(EMSdevice::uom_to_string(dv.uom));
}
}
}
return {}; // not found
}
// prepare array of device values, as 3 elements serialized (name, value, uom) in array to send to Web UI
// returns number of elements
bool EMSdevice::generate_values_json_web(JsonObject & json) {
json["name"] = to_string_short();
JsonArray data = json.createNestedArray("data");
uint8_t num_elements = 0;
for (const auto & dv : devicevalues_) {
// ignore if full_name empty
if (dv.full_name != nullptr) {
// handle Booleans (true, false)
if ((dv.type == DeviceValueType::BOOL) && Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) {
// see if we have options for the bool's
if (dv.options.size() == 2) {
data.add(*(uint8_t *)(dv.value_p) ? dv.options[0] : dv.options[1]);
} else {
// see how to render the value depending on the setting
if (Helpers::bool_format() == BOOL_FORMAT_ONOFF) {
// on or off as strings
data.add(*(uint8_t *)(dv.value_p) ? F_(on) : F_(off));
} else if (Helpers::bool_format() == BOOL_FORMAT_TRUEFALSE) {
// true or false values (not strings)
data.add((bool)(*(uint8_t *)(dv.value_p)) ? true : false);
} else {
// 1 or 0
data.add((uint8_t)(*(uint8_t *)(dv.value_p)) ? 1 : 0);
}
}
}
// handle TEXT strings
else if ((dv.type == DeviceValueType::TEXT) && (Helpers::hasValue((char *)(dv.value_p)))) {
data.add((char *)(dv.value_p));
}
// handle ENUMs
else if ((dv.type == DeviceValueType::ENUM) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
if (*(uint8_t *)(dv.value_p) < dv.options.size()) {
data.add(dv.options[*(uint8_t *)(dv.value_p)]);
}
}
else {
// handle Integers and Floats
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as an integer whole
// the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler
uint8_t divider = (dv.options.size() == 1) ? Helpers::atoint(uuid::read_flash_string(dv.options[0]).c_str()) : 0;
// INT
if ((dv.type == DeviceValueType::INT) && Helpers::hasValue(*(int8_t *)(dv.value_p))) {
if (divider) {
data.add(Helpers::round2(*(int8_t *)(dv.value_p), divider));
} else {
data.add(*(int8_t *)(dv.value_p));
}
} else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
if (divider) {
data.add(Helpers::round2(*(uint8_t *)(dv.value_p), divider));
} else {
data.add(*(uint8_t *)(dv.value_p));
}
} else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) {
if (divider) {
data.add(Helpers::round2(*(int16_t *)(dv.value_p), divider));
} else {
data.add(*(int16_t *)(dv.value_p));
}
} else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
if (divider) {
data.add(Helpers::round2(*(uint16_t *)(dv.value_p), divider));
} else {
data.add(*(uint16_t *)(dv.value_p));
}
} else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
if (divider) {
data.add(Helpers::round2(*(uint32_t *)(dv.value_p), divider));
} else {
data.add(*(uint32_t *)(dv.value_p));
}
} else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
uint32_t time_value = *(uint32_t *)(dv.value_p);
time_value = (divider) ? time_value / divider : time_value; // sometimes we need to divide by 60
char time_s[40];
snprintf_P(time_s, 40, PSTR("%d days %d hours %d minutes"), (time_value / 1440), ((time_value % 1440) / 60), (time_value % 60));
data.add(time_s);
}
}
// check if we've added a data element by comparing the size
// then add the remaining elements
uint8_t sz = data.size();
if (sz > num_elements) {
// add the unit of measure (uom)
if (dv.uom == DeviceValueUOM::MINUTES) {
data.add(nullptr);
} else {
data.add(uom_to_string(dv.uom));
}
// add name, prefixing the tag if it exists
// if we're a boiler, ignore the tag
if (dv.tag.empty() || (device_type_ == DeviceType::BOILER)) {
data.add(dv.full_name);
} else {
char name[50];
snprintf_P(name, sizeof(name), "(%s) %s", dv.tag.c_str(), uuid::read_flash_string(dv.full_name).c_str());
data.add(name);
}
num_elements = sz + 2;
}
}
}
return (num_elements != 0);
}
// For each value in the device create the json object pair and add it to given json
// return false if empty
bool EMSdevice::generate_values_json(JsonObject & root, const std::string & tag_filter, const bool verbose) {
bool has_value = false; // to see if we've added a value. it's faster than doing a json.size() at the end
std::string old_tag(40, '\0');
JsonObject json = root;
for (const auto & dv : devicevalues_) {
// only show if tag is either empty or matches a value, and don't show if full_name is empty unless we're outputing for mqtt payloads
if (((tag_filter.empty()) || (tag_filter == dv.tag)) && (dv.full_name != nullptr || !verbose)) {
bool have_tag = (!dv.tag.empty() && (dv.device_type != DeviceType::BOILER));
char name[80];
if (verbose) {
// prefix the tag in brackets, unless it's Boiler because we're naughty and use tag for the MQTT topic
if (have_tag) {
snprintf_P(name, 80, "(%s) %s", dv.tag.c_str(), uuid::read_flash_string(dv.full_name).c_str());
} else {
strcpy(name, uuid::read_flash_string(dv.full_name).c_str()); // use full name
}
} else {
strcpy(name, uuid::read_flash_string(dv.short_name).c_str()); // use short name
// if we have a tag, and its different to the last one create a nested object
if (have_tag && (dv.tag != old_tag)) {
old_tag = dv.tag;
json = root.createNestedObject(dv.tag);
}
}
// handle Booleans (true, false)
if ((dv.type == DeviceValueType::BOOL) && Helpers::hasValue(*(uint8_t *)(dv.value_p), EMS_VALUE_BOOL)) {
// see if we have options for the bool's
if (dv.options.size() == 2) {
json[name] = *(uint8_t *)(dv.value_p) ? dv.options[0] : dv.options[1];
has_value = true;
} else {
// see how to render the value depending on the setting
if (Helpers::bool_format() == BOOL_FORMAT_ONOFF) {
// on or off as strings
json[name] = *(uint8_t *)(dv.value_p) ? F_(on) : F_(off);
has_value = true;
} else if (Helpers::bool_format() == BOOL_FORMAT_TRUEFALSE) {
// true or false values (not strings)
json[name] = (bool)(*(uint8_t *)(dv.value_p)) ? true : false;
has_value = true;
} else {
// 1 or 0
json[name] = (uint8_t)(*(uint8_t *)(dv.value_p)) ? 1 : 0;
has_value = true;
}
}
}
// handle TEXT strings
else if ((dv.type == DeviceValueType::TEXT) && (Helpers::hasValue((char *)(dv.value_p)))) {
json[name] = (char *)(dv.value_p);
has_value = true;
}
// handle ENUMs
else if ((dv.type == DeviceValueType::ENUM) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
if (*(uint8_t *)(dv.value_p) < dv.options.size()) {
json[name] = dv.options[*(uint8_t *)(dv.value_p)];
has_value = true;
}
}
// handle Integers and Floats
else {
// If a divider is specified, do the division to 2 decimals places and send back as double/float
// otherwise force as an integer whole
// the nested if's is necessary due to the way the ArduinoJson templates are pre-processed by the compiler
uint8_t divider = (dv.options.size() == 1) ? Helpers::atoint(uuid::read_flash_string(dv.options[0]).c_str()) : 0;
// INT
if ((dv.type == DeviceValueType::INT) && Helpers::hasValue(*(int8_t *)(dv.value_p))) {
if (divider) {
json[name] = Helpers::round2(*(int8_t *)(dv.value_p), divider);
} else {
json[name] = *(int8_t *)(dv.value_p);
}
has_value = true;
} else if ((dv.type == DeviceValueType::UINT) && Helpers::hasValue(*(uint8_t *)(dv.value_p))) {
if (divider) {
json[name] = Helpers::round2(*(uint8_t *)(dv.value_p), divider);
} else {
json[name] = *(uint8_t *)(dv.value_p);
}
has_value = true;
} else if ((dv.type == DeviceValueType::SHORT) && Helpers::hasValue(*(int16_t *)(dv.value_p))) {
if (divider) {
json[name] = Helpers::round2(*(int16_t *)(dv.value_p), divider);
} else {
json[name] = *(int16_t *)(dv.value_p);
}
has_value = true;
} else if ((dv.type == DeviceValueType::USHORT) && Helpers::hasValue(*(uint16_t *)(dv.value_p))) {
if (divider) {
json[name] = Helpers::round2(*(uint16_t *)(dv.value_p), divider);
} else {
json[name] = *(uint16_t *)(dv.value_p);
}
has_value = true;
} else if ((dv.type == DeviceValueType::ULONG) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
if (divider) {
json[name] = Helpers::round2(*(uint32_t *)(dv.value_p), divider);
} else {
json[name] = *(uint32_t *)(dv.value_p);
}
has_value = true;
} else if ((dv.type == DeviceValueType::TIME) && Helpers::hasValue(*(uint32_t *)(dv.value_p))) {
uint32_t time_value = *(uint32_t *)(dv.value_p);
time_value = (divider) ? time_value / divider : time_value; // sometimes we need to divide by 60
if (verbose) {
char time_s[40];
snprintf_P(time_s, sizeof(time_s), PSTR("%d days %d hours %d minutes"), (time_value / 1440), ((time_value % 1440) / 60), (time_value % 60));
json[name] = time_s;
} else {
json[name] = time_value;
}
has_value = true;
}
}
}
}
return has_value;
}
// 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
@@ -354,50 +690,4 @@ void EMSdevice::read_command(const uint16_t type_id) {
EMSESP::send_read_request(type_id, device_id());
}
// create json key/value pair
void EMSdevice::create_value_json(JsonArray & root,
const __FlashStringHelper * key,
const __FlashStringHelper * prefix,
const __FlashStringHelper * name,
const __FlashStringHelper * suffix,
JsonObject & json) {
JsonVariant data = json[uuid::read_flash_string(key)];
if (data == nullptr) {
return; // doesn't exist
}
// add prefix to name
if (prefix != nullptr) {
char name_text[100];
snprintf_P(name_text, sizeof(name_text), PSTR("%s%s"), uuid::read_flash_string(prefix).c_str(), uuid::read_flash_string(name).c_str());
root.add(name_text);
} else {
root.add(name);
}
// convert to string and add the suffix, this is to save space when sending to the web as json
// which is why we use n and v instead of name and value
std::string suffix_string(10, '\0');
if (suffix == nullptr) {
suffix_string = "";
} else {
suffix_string = " " + uuid::read_flash_string(suffix);
}
char data_string[40];
if (data.is<char *>()) {
snprintf_P(data_string, sizeof(data_string), PSTR("%s%s"), data.as<char *>(), suffix_string.c_str());
} else if (data.is<int>()) {
snprintf_P(data_string, sizeof(data_string), PSTR("%d%s"), data.as<int>(), suffix_string.c_str());
} else if (data.is<float>()) {
char s[10];
snprintf_P(data_string, sizeof(data_string), PSTR("%s%s"), Helpers::render_value(s, (float)data.as<float>(), 1), suffix_string.c_str());
} else if (data.is<bool>()) {
char s[10];
snprintf_P(data_string, sizeof(data_string), PSTR("%s%s"), Helpers::render_boolean(s, data.as<bool>()), suffix_string.c_str());
}
root.add(data_string);
}
} // namespace emsesp

View File

@@ -30,10 +30,43 @@
namespace emsesp {
enum DeviceValueType : uint8_t {
BOOL,
INT,
UINT,
SHORT,
USHORT,
ULONG,
TIME, // same as ULONG
ENUM,
TEXT
};
// Unit Of Measurement mapping
enum DeviceValueUOM : uint8_t {
DEGREES,
PERCENT,
LMIN,
KWH,
WH,
HOURS,
MINUTES,
UA,
BAR,
NONE
};
class EMSdevice {
public:
virtual ~EMSdevice() = default; // destructor of base class must always be virtual because it's a polymorphic class
static constexpr uint8_t EMS_DEVICES_MAX_TELEGRAMS = 20;
// virtual functions overrules by derived classes
virtual bool publish_ha_config() = 0;
// 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)
@@ -45,8 +78,6 @@ class EMSdevice {
, 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_;
}
@@ -55,6 +86,8 @@ class EMSdevice {
static std::string device_type_2_device_name(const uint8_t device_type);
static uint8_t device_name_2_device_type(const char * topic);
static const __FlashStringHelper * uom_to_string(uint8_t uom);
inline uint8_t product_id() const {
return product_id_;
}
@@ -117,6 +150,14 @@ class EMSdevice {
unique_id_ = unique_id;
}
inline bool has_update() const {
return has_update_;
}
inline void has_update(bool has_update) {
has_update_ |= has_update;
}
std::string brand_to_string() const;
static uint8_t decode_brand(uint8_t value);
@@ -131,6 +172,19 @@ class EMSdevice {
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);
std::string get_value_uom(const char * key);
bool generate_values_json(JsonObject & json, const std::string & tag_filter, const bool verbose = false);
bool generate_values_json_web(JsonObject & json);
void register_device_value(std::string & tag,
void * value_p,
uint8_t type,
const flash_string_vector & options,
const __FlashStringHelper * short_name,
const __FlashStringHelper * full_name,
uint8_t uom,
const __FlashStringHelper * icon = nullptr);
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);
@@ -139,12 +193,6 @@ class EMSdevice {
void register_mqtt_topic(const std::string & topic, mqtt_subfunction_p f);
void register_mqtt_cmd(const __FlashStringHelper * cmd, cmdfunction_p f);
// virtual functions overrules by derived classes
virtual void publish_values(JsonObject & json, bool force = false) = 0;
virtual bool export_values(JsonObject & json) = 0;
virtual bool updated_values() = 0;
virtual void device_info_web(JsonArray & root) = 0;
std::string telegram_type_name(std::shared_ptr<const Telegram> telegram);
void fetch_values();
@@ -155,12 +203,13 @@ class EMSdevice {
telegram_functions_.reserve(n);
}
static void create_value_json(JsonArray & root,
const __FlashStringHelper * key,
const __FlashStringHelper * prefix,
const __FlashStringHelper * name,
const __FlashStringHelper * suffix,
JsonObject & json);
bool ha_config_done() const {
return ha_config_done_;
}
void ha_config_done(const bool v) {
ha_config_done_ = v;
}
enum Brand : uint8_t {
NO_BRAND = 0, // 0
@@ -214,14 +263,47 @@ class EMSdevice {
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_RC20_2 = 4; // Variation on RC20, Older, like ES72
static constexpr uint8_t EMS_DEVICE_FLAG_RC30_1 = 5; // variation on RC30, Newer models
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;
struct DeviceValue {
uint8_t device_type; // EMSdevice::DeviceType
const std::string tag; // MQTT topic or ID
void * value_p; // pointer to variable of any type
uint8_t type; // DeviceValueType::*
const flash_string_vector options; // list of options for ENUM, or divider
const __FlashStringHelper * short_name; // used in MQTT
const __FlashStringHelper * full_name; // used in Web and Console
uint8_t uom; // DeviceValueUOM::*
const __FlashStringHelper * icon; // HA icon
DeviceValue(uint8_t device_type,
const std::string & tag,
void * value_p,
uint8_t type,
const flash_string_vector options,
const __FlashStringHelper * short_name,
const __FlashStringHelper * full_name,
uint8_t uom,
const __FlashStringHelper * icon)
: device_type(device_type)
, tag(tag)
, value_p(value_p)
, type(type)
, options(options)
, short_name(short_name)
, full_name(full_name)
, uom(uom)
, icon(icon) {
}
};
const std::vector<DeviceValue> devicevalues() const;
private:
uint8_t unique_id_;
uint8_t device_type_ = DeviceType::SYSTEM;
@@ -232,6 +314,9 @@ class EMSdevice {
uint8_t flags_ = 0;
uint8_t brand_ = Brand::NO_BRAND;
bool ha_config_done_ = false;
bool has_update_ = false;
struct TelegramFunction {
uint16_t telegram_type_id_; // it's type_id
const __FlashStringHelper * telegram_type_name_; // e.g. RC20Message
@@ -246,6 +331,8 @@ class EMSdevice {
}
};
std::vector<TelegramFunction> telegram_functions_; // each EMS device has its own set of registered telegram types
std::vector<DeviceValue> devicevalues_;
};
} // namespace emsesp

View File

@@ -41,7 +41,7 @@ WebAPIService EMSESP::webAPIService = WebAPIService(&webServer);
using DeviceFlags = emsesp::EMSdevice;
using DeviceType = emsesp::EMSdevice::DeviceType;
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
std::vector<emsesp::EMSESP::Device_record> EMSESP::device_library_; // library of all our known EMS devices so far
uuid::log::Logger EMSESP::logger_{F_(emsesp), uuid::log::Facility::KERN};
@@ -256,7 +256,7 @@ void EMSESP::show_ems(uuid::console::Shell & shell) {
shell.println();
}
// show EMS device values
// show EMS device values to the shell console
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."));
@@ -264,8 +264,6 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
return;
}
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_MAX_DYN);
// 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) {
@@ -273,16 +271,40 @@ void EMSESP::show_device_values(uuid::console::Shell & shell) {
// print header
shell.printfln(F("%s: %s"), emsdevice->device_type_name().c_str(), emsdevice->to_string().c_str());
doc.clear(); // clear so we can re-use for each device
JsonArray root = doc.to<JsonArray>();
emsdevice->device_info_web(root); // create array
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // use max size
JsonObject json = doc.to<JsonObject>();
emsdevice->generate_values_json(json, "", true); // verbose mode
// iterate values and print to shell
uint8_t key_value = 0;
for (const JsonVariant & value : root) {
shell.printf((++key_value & 1) ? " %s: " : "%s\r\n", value.as<const char *>());
// print line
uint8_t id = 0;
for (JsonPair p : json) {
const char * key = p.key().c_str();
shell.printf(" %s: ", key);
JsonVariant data = p.value();
shell.print(COLOR_BRIGHT_GREEN);
if (data.is<char *>()) {
shell.print(data.as<char *>());
} else if (data.is<int>()) {
shell.print(data.as<int>());
} else if (data.is<float>()) {
char s[10];
shell.print(Helpers::render_value(s, (float)data.as<float>(), 1));
} else if (data.is<bool>()) {
char s[10];
shell.print(Helpers::render_boolean(s, data.as<bool>()));
}
// if there is a uom print it
std::string uom = emsdevice->get_value_uom(key);
if (!uom.empty()) {
shell.print(' ');
shell.print(uom);
}
shell.print(COLOR_RESET);
shell.println();
id++;
}
shell.println();
}
}
@@ -311,12 +333,12 @@ void EMSESP::publish_all(bool force) {
return;
}
if (Mqtt::connected()) {
publish_device_values(EMSdevice::DeviceType::BOILER, false);
publish_device_values(EMSdevice::DeviceType::THERMOSTAT, false);
publish_device_values(EMSdevice::DeviceType::SOLAR, false);
publish_device_values(EMSdevice::DeviceType::MIXER, false);
publish_device_values(EMSdevice::DeviceType::BOILER);
publish_device_values(EMSdevice::DeviceType::THERMOSTAT);
publish_device_values(EMSdevice::DeviceType::SOLAR);
publish_device_values(EMSdevice::DeviceType::MIXER);
publish_other_values();
publish_sensor_values(true, false);
publish_sensor_values(true);
system_.send_heartbeat();
}
}
@@ -327,29 +349,31 @@ void EMSESP::publish_all_loop() {
if (!Mqtt::connected() || !publish_all_idx_) {
return;
}
// every HA-sensor takes 20 ms, wait ~2 sec to finish (boiler have ~70 sensors)
// every HA-sensor takes 20 ms, wait ~2 sec to finish (boiler has ~70 sensors)
if ((uuid::get_uptime() - last < 2000)) {
return;
}
last = uuid::get_uptime();
switch (publish_all_idx_++) {
case 1:
publish_device_values(EMSdevice::DeviceType::BOILER, true);
publish_device_values(EMSdevice::DeviceType::BOILER);
break;
case 2:
publish_device_values(EMSdevice::DeviceType::THERMOSTAT, true);
publish_device_values(EMSdevice::DeviceType::THERMOSTAT);
break;
case 3:
publish_device_values(EMSdevice::DeviceType::SOLAR, true);
publish_device_values(EMSdevice::DeviceType::SOLAR);
break;
case 4:
publish_device_values(EMSdevice::DeviceType::MIXER, true);
publish_device_values(EMSdevice::DeviceType::MIXER);
break;
case 5:
publish_other_values();
break;
case 6:
publish_sensor_values(true, true);
publish_sensor_values(true);
break;
case 7:
system_.send_heartbeat();
@@ -362,37 +386,61 @@ void EMSESP::publish_all_loop() {
}
// create json doc for the devices values and add to MQTT publish queue
// special case for Mixer units, since we want to bundle all devices together into one payload
void EMSESP::publish_device_values(uint8_t device_type, bool force) {
if (device_type == EMSdevice::DeviceType::MIXER && Mqtt::mqtt_format() != Mqtt::Format::SINGLE) {
// DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_LARGE);
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_LARGE> doc;
JsonObject json = doc.to<JsonObject>();
for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type)) {
emsdevice->publish_values(json, force);
}
}
Mqtt::publish("mixer_data", doc.as<JsonObject>());
return;
}
void EMSESP::publish_device_values(uint8_t device_type) {
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN); // use max size
JsonObject json = doc.to<JsonObject>();
bool has_value = false;
for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type)) {
JsonObject dummy;
emsdevice->publish_values(dummy, force);
// if we're using HA and it's not already done, send the config topics first. only do this once
if (Mqtt::ha_enabled() && (!emsdevice->ha_config_done())) {
// create the configs for each value as a sensor
for (const auto & dv : emsdevice->devicevalues()) {
if (dv.device_type == device_type) {
Mqtt::register_mqtt_ha_sensor(dv.type, dv.tag.c_str(), dv.full_name, device_type, dv.short_name, dv.uom, dv.icon);
}
}
// create HA device
// if this is done early, it may fail for some reason
emsdevice->ha_config_done(emsdevice->publish_ha_config());
}
// if its a boiler, generate json for each group and publish it
if (device_type == DeviceType::BOILER) {
emsdevice->generate_values_json(json, "boiler_data");
Mqtt::publish("boiler_data", json);
json.clear();
emsdevice->generate_values_json(json, "boiler_data_ww");
Mqtt::publish("boiler_data_ww", json);
json.clear();
emsdevice->generate_values_json(json, "boiler_data_info");
Mqtt::publish("boiler_data_info", json);
return;
}
// for all other devices add the values to the json, without verbose mode
has_value |= emsdevice->generate_values_json(json, "");
}
}
// if there is nothing to publish, exit
if (!has_value) {
return;
}
// publish it under a single topic
char topic[20];
snprintf_P(topic, sizeof(topic), PSTR("%s_data"), EMSdevice::device_type_2_device_name(device_type).c_str());
Mqtt::publish(topic, json);
}
// call the devices that don't need special attention
void EMSESP::publish_other_values() {
for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() != EMSdevice::DeviceType::BOILER) && (emsdevice->device_type() != EMSdevice::DeviceType::THERMOSTAT)
&& (emsdevice->device_type() != EMSdevice::DeviceType::SOLAR) && (emsdevice->device_type() != EMSdevice::DeviceType::MIXER)) {
JsonObject dummy;
emsdevice->publish_values(dummy);
}
}
publish_device_values(EMSdevice::DeviceType::SWITCH);
publish_device_values(EMSdevice::DeviceType::HEATPUMP);
}
void EMSESP::publish_sensor_values(const bool time, const bool force) {
@@ -407,7 +455,7 @@ void EMSESP::publish_response(std::shared_ptr<const Telegram> telegram) {
return;
}
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
char buffer[100];
doc["src"] = Helpers::hextoa(buffer, telegram->src);
@@ -641,7 +689,7 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
// 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
// after the telegram has been processed, call see if there have been values changed and we need to do a MQTT publish
bool found = false;
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
@@ -649,10 +697,11 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
found = emsdevice->handle_telegram(telegram);
// if we correctly processes the telegram follow up with sending it via MQTT if needed
if (found && Mqtt::connected()) {
if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->updated_values()) || telegram->type_id == publish_id_) {
if ((mqtt_.get_publish_onchange(emsdevice->device_type()) && emsdevice->has_update()) || telegram->type_id == publish_id_) {
if (telegram->type_id == publish_id_) {
publish_id_ = 0;
}
emsdevice->has_update(false); // reset flag
publish_device_values(emsdevice->device_type()); // publish to MQTT if we explicitly have too
}
}
@@ -671,21 +720,6 @@ bool EMSESP::process_telegram(std::shared_ptr<const Telegram> telegram) {
return found;
}
// calls the device handler's function to populate a json doc with device info
// to be used in the Web UI. The unique_id is the unique record ID from the Web table to identify which device to load
void EMSESP::device_info_web(const uint8_t unique_id, JsonObject & root) {
for (const auto & emsdevice : emsdevices) {
if (emsdevice) {
if (emsdevice->unique_id() == unique_id) {
root["name"] = emsdevice->to_string_short(); // can't use c_str() because of scope
JsonArray data = root.createNestedArray("data");
emsdevice->device_info_web(data);
return;
}
}
}
}
// return true if we have this device already registered
bool EMSESP::device_exists(const uint8_t device_id) {
for (const auto & emsdevice : emsdevices) {
@@ -813,14 +847,14 @@ bool EMSESP::add_device(const uint8_t device_id, const uint8_t product_id, std::
// export all values to info command
// value and id are ignored
bool EMSESP::command_info(uint8_t device_type, JsonObject & json) {
bool ok = false;
bool has_value = false;
for (const auto & emsdevice : emsdevices) {
if (emsdevice && (emsdevice->device_type() == device_type)) {
ok |= emsdevice->export_values(json);
has_value |= emsdevice->generate_values_json(json, "", true); // verbose mode
}
}
return ok;
return has_value;
}
// send a read request, passing it into to the Tx Service, with offset
@@ -972,8 +1006,8 @@ void EMSESP::start() {
webSettingsService.begin(); // load EMS-ESP specific settings
}
// Load our library of known devices. Names are stored in Flash mem.
device_library_.reserve(80);
// Load our library of known devices into stack mem. Names are stored in Flash mem.
// device_library_.reserve(80);
device_library_ = {
#include "device_library.h"
};
@@ -985,13 +1019,8 @@ void EMSESP::start() {
dallassensor_.start(); // dallas external sensors
webServer.begin(); // start web server
emsdevices.reserve(5); // reserve space for initially 5 devices to avoid mem
emsdevices.reserve(5); // reserve space for initially 5 devices to avoid mem frag issues
LOG_INFO(F("EMS Device library loaded with %d records"), device_library_.size());
#if defined(EMSESP_STANDALONE)
mqtt_.on_connect(); // simulate an MQTT connection
#endif
}
// main loop calling all services

View File

@@ -50,15 +50,22 @@
#include "roomcontrol.h"
#include "command.h"
#include "devices/boiler.h"
#define WATCH_ID_NONE 0 // no watch id set
#define EMSESP_MAX_JSON_SIZE_HA_CONFIG 384 // for small HA config payloads, using StaticJsonDocument
#define EMSESP_MAX_JSON_SIZE_SMALL 256 // for smaller json docs, using StaticJsonDocument
#define EMSESP_MAX_JSON_SIZE_MEDIUM 768 // for medium json docs from ems devices, using StaticJsonDocument
#define EMSESP_MAX_JSON_SIZE_LARGE 1024 // for large json docs from ems devices, like boiler or thermostat data, using StaticJsonDocument
#define EMSESP_MAX_JSON_SIZE_MEDIUM_DYN 1024 // for large json docs, using DynamicJsonDocument
#define EMSESP_MAX_JSON_SIZE_LARGE_DYN 2048 // for very large json docs, using DynamicJsonDocument
#define EMSESP_MAX_JSON_SIZE_MAX_DYN 4096 // for very very large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_HA_CONFIG 768 // for HA config payloads, using StaticJsonDocument
#define EMSESP_JSON_SIZE_SMALL 256 // for smaller json docs, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM 768 // for medium json docs from ems devices, using StaticJsonDocument
#define EMSESP_JSON_SIZE_LARGE 1024 // for large json docs from ems devices, like boiler or thermostat data, using StaticJsonDocument
#define EMSESP_JSON_SIZE_MEDIUM_DYN 1024 // for large json docs, using DynamicJsonDocument
#define EMSESP_JSON_SIZE_LARGE_DYN 2048 // for very large json docs, using DynamicJsonDocument
#if defined(EMSESP_STANDALONE)
#define EMSESP_JSON_SIZE_XLARGE_DYN 7000 // for very very large json docs, using DynamicJsonDocument
#else
#define EMSESP_JSON_SIZE_XLARGE_DYN 4096 // for very very large json docs, using DynamicJsonDocument
#endif
namespace emsesp {
@@ -69,7 +76,7 @@ class EMSESP {
static void start();
static void loop();
static void publish_device_values(uint8_t device_type, bool force = false);
static void publish_device_values(uint8_t device_type);
static void publish_other_values();
static void publish_sensor_values(const bool time, const bool force = false);
static void publish_all(bool force = false);
@@ -98,8 +105,6 @@ class EMSESP {
static void send_raw_telegram(const char * data);
static bool device_exists(const uint8_t device_id);
static void device_info_web(const uint8_t unique_id, JsonObject & root);
static uint8_t count_devices(const uint8_t device_type);
static uint8_t actual_master_thermostat();
@@ -193,7 +198,6 @@ class EMSESP {
static void process_version(std::shared_ptr<const Telegram> telegram);
static void publish_response(std::shared_ptr<const Telegram> telegram);
static void publish_all_loop();
static bool command_info(uint8_t device_type, JsonObject & json);
static constexpr uint32_t EMS_FETCH_FREQUENCY = 60000; // check every minute
@@ -205,7 +209,6 @@ class EMSESP {
const __FlashStringHelper * name;
uint8_t flags;
};
static std::vector<Device_record> device_library_;
static uint8_t actual_master_thermostat_;

View File

@@ -136,28 +136,8 @@ char * Helpers::render_boolean(char * result, bool value) {
return result;
}
// depending on format render a number or a string
char * Helpers::render_enum(char * result, const std::vector<const __FlashStringHelper *> & value, const uint8_t no) {
if (no >= value.size()) {
return nullptr; // out of bounds
}
strcpy(result, uuid::read_flash_string(value[no]).c_str());
if (bool_format() == BOOL_FORMAT_TRUEFALSE) {
if (no == 0 && uuid::read_flash_string(value[0]) == "off") {
strlcpy(result, "false", 7);
} else if (no == 1 && uuid::read_flash_string(value[1]) == "on") {
strlcpy(result, "true", 6);
}
} else if (bool_format() == BOOL_FORMAT_NUMBERS) {
itoa(result, no);
}
return result;
}
// render for native char strings
char * Helpers::render_value(char * result, const char * value, uint8_t format) {
char * Helpers::render_value(char * result, const char * value, uint8_t format __attribute__((unused))) {
strcpy(result, value);
return result;
}
@@ -293,13 +273,6 @@ char * Helpers::render_value(char * result, const uint32_t value, const uint8_t
}
result[0] = '\0';
// check if we're converting from minutes to a time string
if (format == EMS_VALUE_TIME) {
snprintf_P(result, 40, PSTR("%d days %d hours %d minutes"), (value / 1440), ((value % 1440) / 60), (value % 60));
return result;
}
char s[20];
#ifndef EMSESP_STANDALONE
@@ -378,8 +351,8 @@ uint16_t Helpers::atoint(const char * value) {
// 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;
double Helpers::round2(double value, const uint8_t divider) {
return (int)((value / divider) * 100 + 0.5) / 100.0;
}
bool Helpers::check_abs(const int32_t i) {
@@ -398,6 +371,14 @@ bool Helpers::hasValue(const int8_t & v) {
return (v != EMS_VALUE_INT_NOTSET);
}
bool Helpers::hasValue(char * v) {
if ((v == nullptr) || (strlen(v) == 0)) {
return false;
}
return (v[0] != '\0');
}
// for short these are typically 0x8300, 0x7D00 and sometimes 0x8000
bool Helpers::hasValue(const int16_t & v) {
return (abs(v) < EMS_VALUE_USHORT_NOTSET);
@@ -471,7 +452,7 @@ bool Helpers::value2bool(const char * v, bool & value) {
}
// checks to see if a string is member of a vector and return the index, also allow true/false for on/off
bool Helpers::value2enum(const char * v, uint8_t & value, const std::vector<const __FlashStringHelper *> & strs) {
bool Helpers::value2enum(const char * v, uint8_t & value, const flash_string_vector & strs) {
if ((v == nullptr) || (strlen(v) == 0)) {
return false;
}
@@ -485,4 +466,7 @@ bool Helpers::value2enum(const char * v, uint8_t & value, const std::vector<cons
return false;
}
} // namespace emsesp

View File

@@ -20,19 +20,18 @@
#define EMSESP_HELPERS_H
#include <Arduino.h>
#include <uuid/common.h>
#include "telegram.h" // for EMS_VALUE_* settings
#define BOOL_FORMAT_ONOFF 1
#define BOOL_FORMAT_TRUEFALSE 2
#define BOOL_FORMAT_NUMBERS 3
enum { BOOL_FORMAT_ONOFF = 1, BOOL_FORMAT_TRUEFALSE, BOOL_FORMAT_NUMBERS }; // matches Web UI settings
// #define FJSON(x) x
#define FJSON(x) F(x)
namespace emsesp {
using flash_string_vector = std::vector<const __FlashStringHelper *>;
class Helpers {
public:
static char * render_value(char * result, const float value, const uint8_t format); // format is the precision
@@ -43,7 +42,6 @@ class Helpers {
static char * render_value(char * result, const int16_t value, const uint8_t format);
static char * render_value(char * result, const char * value, uint8_t format);
static char * render_boolean(char * result, bool value);
static char * render_enum(char * result, const std::vector<const __FlashStringHelper *> & value, const uint8_t no);
static char * hextoa(char * result, const uint8_t value);
static std::string data_to_hex(const uint8_t * data, const uint8_t length);
@@ -53,7 +51,7 @@ class Helpers {
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);
static double round2(double value, const uint8_t divider);
static std::string toLower(std::string const & s);
static bool hasValue(const uint8_t & v, const uint8_t isBool = 0);
@@ -61,12 +59,13 @@ class Helpers {
static bool hasValue(const int16_t & v);
static bool hasValue(const uint16_t & v);
static bool hasValue(const uint32_t & v);
static bool hasValue(char * v);
static bool value2number(const char * v, int & value);
static bool value2float(const char * v, float & value);
static bool value2bool(const char * v, bool & value);
static bool value2string(const char * v, std::string & value);
static bool value2enum(const char * v, uint8_t & value, const std::vector<const __FlashStringHelper *> & strs);
static bool value2enum(const char * v, uint8_t & value, const flash_string_vector & strs);
static void bool_format(uint8_t bool_format) {
bool_format_ = bool_format;

View File

@@ -62,10 +62,6 @@ MAKE_PSTR_WORD(users)
MAKE_PSTR_WORD(master)
MAKE_PSTR_WORD(pin)
MAKE_PSTR_WORD(publish)
MAKE_PSTR_WORD(bar)
MAKE_PSTR_WORD(min)
MAKE_PSTR_WORD(hours)
MAKE_PSTR_WORD(uA)
MAKE_PSTR_WORD(timeout)
// for commands
@@ -94,10 +90,6 @@ MAKE_PSTR_WORD(generic)
MAKE_PSTR_WORD(dallassensor)
MAKE_PSTR_WORD(unknown)
MAKE_PSTR(1space, " ")
MAKE_PSTR(2spaces, " ")
MAKE_PSTR(kwh, "kWh")
MAKE_PSTR(wh, "Wh")
MAKE_PSTR(EMSESP, "EMS-ESP")
MAKE_PSTR(master_thermostat_fmt, "Master Thermostat Device ID = %s")
MAKE_PSTR(host_fmt, "Host = %s")
@@ -115,8 +107,26 @@ MAKE_PSTR(watchid_optional, "[ID]")
MAKE_PSTR(watch_format_optional, "[off | on | raw | unknown]")
MAKE_PSTR(invalid_watch, "Invalid watch type")
MAKE_PSTR(data_mandatory, "\"XX XX ...\"")
// uom - also used with HA
MAKE_PSTR(percent, "%")
MAKE_PSTR(degrees, "°C")
MAKE_PSTR(kwh, "kWh")
MAKE_PSTR(wh, "Wh")
MAKE_PSTR(bar, "bar")
MAKE_PSTR(minutes, "minutes")
MAKE_PSTR(hours, "hours")
MAKE_PSTR(ua, "uA")
MAKE_PSTR(lmin, "l/min")
// Home Assistant icons (https://materialdesignicons.com/)
MAKE_PSTR(icontemperature, "mdi:temperature-celsius")
MAKE_PSTR(iconpercent, "mdi:percent-outline")
MAKE_PSTR(iconfire, "mdi:fire")
MAKE_PSTR(iconfan, "mdi:fan")
MAKE_PSTR(iconflame, "mdi:flash")
MAKE_PSTR(iconvalve, "mdi:valve")
MAKE_PSTR(asterisks, "********")
MAKE_PSTR(n_mandatory, "<n>")
MAKE_PSTR(id_optional, "[id|hc]")
@@ -134,193 +144,3 @@ 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>")
// boiler
MAKE_PSTR(heatingActive, "Heating active")
MAKE_PSTR(tapwaterActive, "Warm water/DHW active")
MAKE_PSTR(serviceCode, "Service code")
MAKE_PSTR(serviceCodeNumber, "Service code number")
MAKE_PSTR(lastCode, "Last error")
MAKE_PSTR(wWSelTemp, "Warm water selected temperature")
MAKE_PSTR(wWSetTemp, "Warm water set temperature")
MAKE_PSTR(wWDisinfectionTemp, "Warm water disinfection temperature")
MAKE_PSTR(selFlowTemp, "Selected flow temperature")
MAKE_PSTR(selBurnPow, "Burner selected max power")
MAKE_PSTR(curBurnPow, "Burner current power")
MAKE_PSTR(pumpMod, "Pump modulation")
MAKE_PSTR(pumpMod2, "Heat pump modulation")
MAKE_PSTR(wWType, "Warm water type")
MAKE_PSTR(wWChargeType, "Warm water charging type")
MAKE_PSTR(wWCircPump, "Warm water circulation pump available")
MAKE_PSTR(wWCircPumpMode, "Warm water circulation pump freq")
MAKE_PSTR(wWCirc, "Warm water circulation active")
MAKE_PSTR(outdoorTemp, "Outside temperature")
MAKE_PSTR(wWCurTemp, "Warm water current temperature (intern)")
MAKE_PSTR(wWCurTemp2, "Warm water current temperature (extern)")
MAKE_PSTR(wWCurFlow, "Warm water current tap water flow")
MAKE_PSTR(curFlowTemp, "Current flow temperature")
MAKE_PSTR(retTemp, "Return temperature")
MAKE_PSTR(switchTemp, "Mixer switch temperature")
MAKE_PSTR(sysPress, "System pressure")
MAKE_PSTR(boilTemp, "Max temperature")
MAKE_PSTR(wwStorageTemp1, "Warm water storage temperature (intern)")
MAKE_PSTR(wwStorageTemp2, "Warm water storage temperature (extern)")
MAKE_PSTR(exhaustTemp, "Exhaust temperature")
MAKE_PSTR(wWActivated, "Warm water activated")
MAKE_PSTR(wWOneTime, "Warm water one time charging")
MAKE_PSTR(wWDisinfecting, "Warm water disinfecting")
MAKE_PSTR(wWCharging, "Warm water charging")
MAKE_PSTR(wWRecharging, "Warm water recharging")
MAKE_PSTR(wWTempOK, "Warm water temperature ok")
MAKE_PSTR(wWActive, "Warm water active")
MAKE_PSTR(burnGas, "Gas")
MAKE_PSTR(flameCurr, "Flame current")
MAKE_PSTR(heatPump, "Pump")
MAKE_PSTR(fanWork, "Fan")
MAKE_PSTR(ignWork, "Ignition")
MAKE_PSTR(wWHeat, "Warm water heating")
MAKE_PSTR(heatingActivated, "Heating activated")
MAKE_PSTR(heatingTemp, "Heating temperature setting")
MAKE_PSTR(pumpModMax, "Circuit pump modulation max power")
MAKE_PSTR(pumpModMin, "Circuit pump modulation min power")
MAKE_PSTR(pumpDelay, "Circuit pump delay time")
MAKE_PSTR(burnMinPeriod, "Burner min period")
MAKE_PSTR(burnMinPower, "Burner min power")
MAKE_PSTR(burnMaxPower, "Burner max power")
MAKE_PSTR(boilHystOn, "Temperature hysteresis on")
MAKE_PSTR(boilHystOff, "Temperature hysteresis off")
MAKE_PSTR(setFlowTemp, "Set flow temperature")
MAKE_PSTR(wWSetPumpPower, "Warm water pump set power")
MAKE_PSTR(wwMixTemperature, "Warm water mix temperature")
MAKE_PSTR(wwBufferTemperature, "Warm water buffer temperature")
MAKE_PSTR(wWStarts, "Warm water starts")
MAKE_PSTR(wWWorkM, "Warm water active time")
MAKE_PSTR(setBurnPow, "Burner set power")
MAKE_PSTR(burnStarts, "Burner starts")
MAKE_PSTR(burnWorkMin, "Burner active time")
MAKE_PSTR(heatWorkMin, "Heating active time")
MAKE_PSTR(UBAuptime, "Boiler total uptime")
MAKE_PSTR(upTimeControl, "Operating time control")
MAKE_PSTR(upTimeCompHeating, "Operating time compressor heating")
MAKE_PSTR(upTimeCompCooling, "Operating time compressor cooling")
MAKE_PSTR(upTimeCompWw, "Operating time compressor warm water")
MAKE_PSTR(heatingStarts, "Heating starts (control)")
MAKE_PSTR(coolingStarts, "Cooling starts (control)")
MAKE_PSTR(wWStarts2, "Warm water starts (control)")
MAKE_PSTR(nrgConsTotal, "Energy consumption total")
MAKE_PSTR(auxElecHeatNrgConsTotal, "Auxiliary electrical heater energy consumption total")
MAKE_PSTR(auxElecHeatNrgConsHeating, "Auxiliary electrical heater energy consumption heating")
MAKE_PSTR(auxElecHeatNrgConsDHW, "Auxiliary electrical heater energy consumption DHW")
MAKE_PSTR(nrgConsCompTotal, "Energy consumption compressor total")
MAKE_PSTR(nrgConsCompHeating, "Energy consumption compressor heating")
MAKE_PSTR(nrgConsCompWw, "Energy consumption compressor warm water")
MAKE_PSTR(nrgConsCompCooling, "Energy consumption compressor total")
MAKE_PSTR(nrgSuppTotal, "Energy supplied total")
MAKE_PSTR(nrgSuppHeating, "Energy supplied heating")
MAKE_PSTR(nrgSuppWw, "Energy supplied warm water")
MAKE_PSTR(nrgSuppCooling, "Energy supplied cooling")
MAKE_PSTR(maintenanceMessage, "Maintenance message")
MAKE_PSTR(maintenance, "Scheduled maintenance")
MAKE_PSTR(maintenanceTime, "Next maintenance in")
MAKE_PSTR(maintenanceDate, "Next maintenance on")
// solar
MAKE_PSTR(collectorTemp, "Collector temperature (TS1)")
MAKE_PSTR(tankBottomTemp, "Bottom temperature (TS2)")
MAKE_PSTR(tankBottomTemp2, "Bottom temperature (TS5)")
MAKE_PSTR(tank1MaxTempCurrent, "Maximum Tank temperature")
MAKE_PSTR(heatExchangerTemp, "Heat exchanger temperature (TS6)")
MAKE_PSTR(solarPumpModulation, "Solar pump modulation (PS1)")
MAKE_PSTR(cylinderPumpModulation, "Cylinder pump modulation (PS5)")
MAKE_PSTR(pumpWorkMin, "Pump working time (min)")
MAKE_PSTR(pumpWorkMintxt, "Pump working time")
MAKE_PSTR(energyLastHour, "Energy last hour")
MAKE_PSTR(energyToday, "Energy today")
MAKE_PSTR(energyTotal, "Energy total")
MAKE_PSTR(solarPump, "Solar pump (PS1) active")
MAKE_PSTR(valveStatus, "Valve status")
MAKE_PSTR(tankHeated, "Tank heated")
MAKE_PSTR(collectorShutdown, "Collector shutdown")
// mixer
MAKE_PSTR(ww_hc, " Warm water circuit %d:")
MAKE_PSTR(wwTemp, "Current warm water temperature")
MAKE_PSTR(pumpStatus, "Current pump status")
MAKE_PSTR(tempStatus, "Current temperature status")
MAKE_PSTR(hc, " Heating circuit %d:")
MAKE_PSTR(flowTemp, "Current flow temperature")
MAKE_PSTR(flowSetTemp, "Setpoint flow temperature")
// thermostat
MAKE_PSTR(time, "Time")
MAKE_PSTR(error, "Error code")
MAKE_PSTR(display, "Display")
MAKE_PSTR(language, "Language")
MAKE_PSTR(offsetclock, "Offset clock")
MAKE_PSTR(dampedtemp, "Damped outdoor temperature")
MAKE_PSTR(inttemp1, "Temperature sensor 1")
MAKE_PSTR(inttemp2, "Temperature sensor 2")
MAKE_PSTR(intoffset, "Offset int. temperature")
MAKE_PSTR(minexttemp, "Min ext. temperature")
MAKE_PSTR(building, "Building")
MAKE_PSTR(floordry, "Floordrying")
MAKE_PSTR(floordrytemp, "Floordrying temperature")
MAKE_PSTR(wwmode, "Warm water mode")
MAKE_PSTR(wwtemp, "Warm water high temperature")
MAKE_PSTR(wwtemplow, "Warm water low temperature")
MAKE_PSTR(wwextra1, "Warm water circuit 1 extra")
MAKE_PSTR(wwextra2, "Warm water circuit 2 extra")
MAKE_PSTR(wwcircmode, "Warm water circulation mode")
// thermostat - per heating circuit
MAKE_PSTR(seltemp, "Setpoint room temperature")
MAKE_PSTR(currtemp, "Current room temperature")
MAKE_PSTR(heattemp, "Heat temperature")
MAKE_PSTR(comforttemp, "Comfort temperature")
MAKE_PSTR(daytemp, "Day temperature")
MAKE_PSTR(ecotemp, "Eco temperature")
MAKE_PSTR(nighttemp, "Night temperature")
MAKE_PSTR(manualtemp, "Manual temperature")
MAKE_PSTR(holidaytemp, "Holiday temperature")
MAKE_PSTR(nofrosttemp, "Nofrost temperature")
MAKE_PSTR(heatingtype, "Heating type")
MAKE_PSTR(targetflowtemp, "Target flow temperature")
MAKE_PSTR(offsettemp, "Offset temperature")
MAKE_PSTR(designtemp, "Design temperature")
MAKE_PSTR(summertemp, "Summer temperature")
MAKE_PSTR(summermode, "Summer mode")
MAKE_PSTR(roominfluence, "Room influence")
MAKE_PSTR(flowtempoffset, "Flow temperature offset")
MAKE_PSTR(minflowtemp, "Min. flow temperature")
MAKE_PSTR(maxflowtemp, "Max. flow temperature")
MAKE_PSTR(mode, "Mode")
MAKE_PSTR(modetype, "Mode type")
// heat pump
MAKE_PSTR(airHumidity, "Relative air humidity")
MAKE_PSTR(dewTemperature, "Dew point temperature")
// other
MAKE_PSTR(activated, "Switch activated")
MAKE_PSTR(status, "Switch status")
// Home Assistant icons
MAKE_PSTR(icontemperature, "mdi:coolant-temperature")
MAKE_PSTR(iconpercent, "mdi:sine-wave")
MAKE_PSTR(iconfire, "mdi:fire")
MAKE_PSTR(iconfan, "mdi:fan")
MAKE_PSTR(iconflash, "mdi:flash")
MAKE_PSTR(iconwaterpump, "mdi:water-pump")
MAKE_PSTR(iconexport, "mdi:home-export-outline")
MAKE_PSTR(iconimport, "mdi:home-import-outline")
MAKE_PSTR(iconcruise, "mdi:car-cruise-control")
MAKE_PSTR(iconvalve, "mdi:valve")
MAKE_PSTR(iconpower, "mdi:power-cycle")
MAKE_PSTR(iconthermostat, "mdi:home-thermometer-outline")
MAKE_PSTR(iconheatpump, "mdi:water-pump")
// MQTT topic suffix
MAKE_PSTR(mqtt_suffix_ww, "_ww")

View File

@@ -32,10 +32,12 @@ uint32_t Mqtt::publish_time_boiler_;
uint32_t Mqtt::publish_time_thermostat_;
uint32_t Mqtt::publish_time_solar_;
uint32_t Mqtt::publish_time_mixer_;
uint32_t Mqtt::publish_time_other_;
uint32_t Mqtt::publish_time_sensor_;
uint8_t Mqtt::mqtt_format_;
uint32_t Mqtt::publish_time_other_;
bool Mqtt::mqtt_enabled_;
uint8_t Mqtt::dallas_format_;
uint8_t Mqtt::ha_climate_format_;
bool Mqtt::ha_enabled_;
std::vector<Mqtt::MQTTSubFunction> Mqtt::mqtt_subfunctions_;
@@ -84,7 +86,7 @@ void Mqtt::subscribe(const uint8_t device_type, const std::string & topic, mqtt_
}
// subscribe to the command topic if it doesn't exist yet
void Mqtt::register_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb) {
void Mqtt::register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb) {
std::string cmd_topic = EMSdevice::device_type_2_device_name(device_type);
bool exists = false;
@@ -149,11 +151,6 @@ void Mqtt::loop() {
EMSESP::publish_device_values(EMSdevice::DeviceType::MIXER);
}
if (publish_time_other_ && (currentMillis - last_publish_other_ > publish_time_other_)) {
last_publish_other_ = currentMillis;
EMSESP::publish_other_values();
}
if (currentMillis - last_publish_sensor_ > publish_time_sensor_) {
last_publish_sensor_ = currentMillis;
EMSESP::publish_sensor_values(publish_time_sensor_ != 0);
@@ -249,8 +246,8 @@ void Mqtt::on_message(const char * topic, const char * payload, size_t len) {
}
// empty function. It's a command then. Find the command from the json and call it directly.
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
DeserializationError error = deserializeJson(doc, message);
if (error) {
LOG_ERROR(F("MQTT error: payload %s, error %s"), message, error.c_str());
return;
@@ -356,8 +353,10 @@ void Mqtt::start() {
publish_time_sensor_ = mqttSettings.publish_time_sensor * 1000;
mqtt_qos_ = mqttSettings.mqtt_qos;
mqtt_retain_ = mqttSettings.mqtt_retain;
mqtt_format_ = mqttSettings.mqtt_format;
mqtt_enabled_ = mqttSettings.enabled;
ha_enabled_ = mqttSettings.ha_enabled;
ha_climate_format_ = mqttSettings.ha_climate_format;
dallas_format_ = mqttSettings.dallas_format;
});
// if MQTT disabled, quit
@@ -406,6 +405,10 @@ void Mqtt::start() {
// create space for command buffer, to avoid heap memory fragmentation
mqtt_subfunctions_.reserve(10);
#if defined(EMSESP_STANDALONE)
on_connect(); // simulate an MQTT connection
#endif
}
void Mqtt::set_publish_time_boiler(uint16_t publish_time) {
@@ -455,19 +458,8 @@ bool Mqtt::get_publish_onchange(uint8_t device_type) {
return false;
}
void Mqtt::set_qos(uint8_t mqtt_qos) {
mqtt_qos_ = mqtt_qos;
}
void Mqtt::set_retain(bool mqtt_retain) {
mqtt_retain_ = mqtt_retain;
}
void Mqtt::set_format(uint8_t mqtt_format) {
mqtt_format_ = mqtt_format;
}
// MQTT onConnect - when a connect is established
// MQTT onConnect - when an MQTT connect is established
// send out some inital MQTT messages
void Mqtt::on_connect() {
if (connecting_) {
return;
@@ -479,7 +471,7 @@ void Mqtt::on_connect() {
// first time to connect
if (connectcount_ == 1) {
// send info topic appended with the version information as JSON
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
doc["event"] = FJSON("start");
doc["version"] = EMSESP_APP_VERSION;
#ifndef EMSESP_STANDALONE
@@ -488,9 +480,14 @@ void Mqtt::on_connect() {
publish(F_(info), doc.as<JsonObject>());
// create the EMS-ESP device in HA, which is MQTT retained
if (mqtt_format() == Format::HA) {
if (ha_enabled()) {
ha_status();
}
// send initial MQTT messages for some of our services
EMSESP::shower_.send_mqtt_stat(false); // Send shower_activated as false
EMSESP::system_.send_heartbeat(); // send heatbeat
} else {
// we doing a re-connect from a TCP break
// only re-subscribe again to all MQTT topics
@@ -504,19 +501,18 @@ void Mqtt::on_connect() {
}
// Home Assistant Discovery - the main master Device
// homeassistant/sensor/ems-esp/status/config
// e.g. homeassistant/sensor/ems-esp/status/config
// all the values from the heartbeat payload will be added as attributes to the entity state
void Mqtt::ha_status() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["name"] = FJSON("EMS-ESP status");
doc["uniq_id"] = FJSON("status");
doc["~"] = System::hostname(); // ems-esp
doc["avty_t"] = FJSON("~/status");
doc["uniq_id"] = FJSON("status");
doc["~"] = System::hostname(); // default ems-esp
// doc["avty_t"] = FJSON("~/status");
doc["json_attr_t"] = FJSON("~/heartbeat");
doc["stat_t"] = FJSON("~/heartbeat");
doc["name"] = FJSON("EMS-ESP status");
doc["val_tpl"] = FJSON("{{value_json['status']}}");
doc["ic"] = FJSON("mdi:home-thermometer-outline");
JsonObject dev = doc.createNestedObject("dev");
dev["name"] = FJSON("EMS-ESP");
@@ -526,7 +522,9 @@ void Mqtt::ha_status() {
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
Mqtt::publish_ha(F("homeassistant/sensor/ems-esp/status/config"), doc.as<JsonObject>()); // publish the config payload with retain flag
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/status/config"), System::hostname().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
}
// add sub or pub task to the queue.
@@ -560,7 +558,7 @@ std::shared_ptr<const MqttMessage> Mqtt::queue_message(const uint8_t operation,
// add MQTT message to queue, payload is a string
std::shared_ptr<const MqttMessage> Mqtt::queue_publish_message(const std::string & topic, const std::string & payload, bool retain) {
if (!enabled() || !connected()) {
if (!enabled() || !connecting_) {
return nullptr;
};
return queue_message(Operation::PUBLISH, topic, payload, retain);
@@ -625,7 +623,7 @@ void Mqtt::publish_ha(const __FlashStringHelper * topic, const JsonObject & payl
// publish a Home Assistant config topic and payload, with retain flag off.
// for ESP32 its added to the queue, for ESP8266 is sent immediatelty
void Mqtt::publish_ha(const std::string & topic, const JsonObject & payload) {
if (!enabled() || !payload.size()) {
if (!enabled()) {
return;
}
@@ -638,20 +636,14 @@ void Mqtt::publish_ha(const std::string & topic, const JsonObject & payload) {
#if defined(EMSESP_STANDALONE)
LOG_DEBUG(F("Publishing HA topic=%s, payload=%s"), topic.c_str(), payload_text.c_str());
#else
LOG_DEBUG(F("Publishing HA topic %s"), topic.c_str());
#endif
#if defined(ESP32)
bool queued = true; // queue MQTT publish
#else
bool queued = false; // publish immediately
#if defined(EMSESP_DEBUG)
LOG_DEBUG(F("[debug] Publishing HA topic=%s, payload=%s"), topic.c_str(), payload_text.c_str());
#endif
// if MQTT is not connected, then we have to queue the msg until the MQTT is online
if (!connected()) {
queued = true; // override
}
// queue messages if the MQTT connection is not yet established. to ensure we don't miss messages
bool queued = !connected();
if (queued) {
queue_publish_message(topic, payload_text, true); // with retain true
@@ -661,6 +653,7 @@ void Mqtt::publish_ha(const std::string & topic, const JsonObject & payload) {
// send immediately and then wait a while
if (!mqttClient_->publish(topic.c_str(), 0, true, payload_text.c_str())) {
LOG_ERROR(F("Failed to publish topic %s"), topic.c_str());
mqtt_publish_fails_++; // increment failure counter
}
delay(MQTT_HA_PUBLISH_DELAY); // enough time to send the short message out
@@ -734,103 +727,65 @@ void Mqtt::process_queue() {
mqtt_messages_.pop_front(); // remove the message from the queue
}
// HA config for a binary_sensor
void Mqtt::register_mqtt_ha_binary_sensor(const __FlashStringHelper * name, const uint8_t device_type, const char * entity) {
if (mqtt_format() != Format::HA) {
return;
}
// StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_HA_CONFIG);
doc["name"] = name;
doc["uniq_id"] = entity;
char state_t[50];
snprintf_P(state_t, sizeof(state_t), PSTR("%s/%s"), hostname_.c_str(), entity);
doc["stat_t"] = state_t;
EMSESP::webSettingsService.read([&](WebSettings & settings) {
if (settings.bool_format == BOOL_FORMAT_ONOFF) {
doc[F("payload_on")] = FJSON("on");
doc[F("payload_off")] = FJSON("off");
} else if (settings.bool_format == BOOL_FORMAT_TRUEFALSE) {
doc[F("payload_on")] = FJSON("true");
doc[F("payload_off")] = FJSON("false");
} else {
doc[F("payload_on")] = FJSON("1");
doc[F("payload_off")] = FJSON("0");
}
});
JsonObject dev = doc.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
char ha_device[40];
snprintf_P(ha_device, sizeof(ha_device), PSTR("ems-esp-%s"), EMSdevice::device_type_2_device_name(device_type).c_str());
ids.add(ha_device);
char topic[MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/binary_sensor/ems-esp/%s/config"), entity);
publish_ha(topic, doc.as<JsonObject>());
}
// HA config for a normal 'sensor' type
// entity must match the key/value pair in the _data topic
// HA config for a sensor and binary_sensor entity
// entity must match the key/value pair in the *_data topic
// some string copying here into chars, it looks messy but does help with heap fragmentation issues
void Mqtt::register_mqtt_ha_sensor(const char * prefix,
const __FlashStringHelper * suffix,
void Mqtt::register_mqtt_ha_sensor(uint8_t type, // device value type
const char * prefix,
const __FlashStringHelper * name,
const uint8_t device_type,
const char * entity,
const __FlashStringHelper * uom,
const __FlashStringHelper * entity,
const uint8_t uom,
const __FlashStringHelper * icon) {
if (mqtt_format() != Format::HA) {
// ignore if name (fullname) is empty
if (name == nullptr) {
return;
}
// StaticJsonDocument<EMSESP_MAX_JSON_SIZE_HA_CONFIG> doc;
DynamicJsonDocument doc(EMSESP_MAX_JSON_SIZE_HA_CONFIG);
// DynamicJsonDocument doc(EMSESP_JSON_SIZE_HA_CONFIG);
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc; // TODO see if this crashes ESP8266?
// create entity by prefixing any given prefix
bool have_prefix = ((prefix[0] != '\0') && (device_type != EMSdevice::DeviceType::BOILER));
// create entity by inserting any given prefix
// we ignore the prefix (tag) if BOILER
char new_entity[50];
if (prefix != nullptr) {
snprintf_P(new_entity, sizeof(new_entity), PSTR("%s.%s"), prefix, entity);
// special case for boiler - don't use the prefix
if (have_prefix) {
snprintf_P(new_entity, sizeof(new_entity), PSTR("%s.%s"), prefix, uuid::read_flash_string(entity).c_str());
} else {
strncpy(new_entity, entity, sizeof(new_entity));
snprintf_P(new_entity, sizeof(new_entity), PSTR("%s"), uuid::read_flash_string(entity).c_str());
}
// device name
char device_name[50];
strncpy(device_name, EMSdevice::device_type_2_device_name(device_type).c_str(), sizeof(device_name));
// build unique identifier, replacing all . with _ as not to break HA
// build unique identifier which will be used in the topic
// and replacing all . with _ as not to break HA
std::string uniq(50, '\0');
snprintf_P(&uniq[0], uniq.capacity() + 1, PSTR("%s_%s"), device_name, new_entity);
std::replace(uniq.begin(), uniq.end(), '.', '_');
// topic
char topic[MQTT_TOPIC_MAX_SIZE];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/ems-esp/%s/config"), uniq.c_str());
// state topic
// if its a boiler we use the tag
char stat_t[MQTT_TOPIC_MAX_SIZE];
if (suffix != nullptr) {
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s_data%s"), hostname_.c_str(), device_name, uuid::read_flash_string(suffix).c_str());
if (device_type == EMSdevice::DeviceType::BOILER) {
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s"), hostname_.c_str(), prefix);
} else {
snprintf_P(stat_t, sizeof(stat_t), PSTR("%s/%s_data"), hostname_.c_str(), device_name);
}
// value template
char val_tpl[50];
snprintf_P(val_tpl, sizeof(val_tpl), PSTR("{{value_json.%s}}"), new_entity);
// ha device
char ha_device[40];
snprintf_P(ha_device, sizeof(ha_device), PSTR("ems-esp-%s"), device_name);
// name
char new_name[50];
if (prefix != nullptr) {
if (have_prefix) {
snprintf_P(new_name, sizeof(new_name), PSTR("%s %s %s"), device_name, prefix, uuid::read_flash_string(name).c_str());
} else {
snprintf_P(new_name, sizeof(new_name), PSTR("%s %s"), device_name, uuid::read_flash_string(name).c_str());
@@ -839,13 +794,55 @@ void Mqtt::register_mqtt_ha_sensor(const char * prefix,
doc["name"] = new_name;
doc["uniq_id"] = uniq;
if (uom != nullptr) {
doc["unit_of_meas"] = uom;
}
doc["stat_t"] = stat_t;
doc["val_tpl"] = val_tpl;
if (icon != nullptr) {
doc["ic"] = icon;
// look at the device value type
if (type != DeviceValueType::BOOL) {
//
// normal HA sensor
//
// topic
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/sensor/%s/%s/config"), System::hostname().c_str(), uniq.c_str());
// value template
char val_tpl[50];
snprintf_P(val_tpl, sizeof(val_tpl), PSTR("{{value_json.%s}}"), new_entity);
doc["val_tpl"] = val_tpl;
// unit of measure
if (uom != DeviceValueUOM::NONE) {
doc["unit_of_meas"] = EMSdevice::uom_to_string(uom);
}
// if there was no icon supplied, resort to the default one
if (icon == nullptr) {
switch (uom) {
case DeviceValueUOM::DEGREES:
doc["ic"] = F_(icontemperature);
break;
case DeviceValueUOM::PERCENT:
doc["ic"] = F_(iconpercent);
break;
case DeviceValueUOM::NONE:
default:
break;
}
} else {
doc["ic"] = icon; // must be prefixed with mdi:
}
} else {
//
// binary sensor
//
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/binary_sensor/%s/%s/config"), System::hostname().c_str(), uniq.c_str());
// boolean
EMSESP::webSettingsService.read([&](WebSettings & settings) {
char result[10];
doc[F("payload_on")] = Helpers::render_boolean(result, true);
doc[F("payload_off")] = Helpers::render_boolean(result, false);
});
}
JsonObject dev = doc.createNestedObject("dev");

View File

@@ -71,14 +71,12 @@ class Mqtt {
void set_publish_time_mixer(uint16_t publish_time);
void set_publish_time_other(uint16_t publish_time);
void set_publish_time_sensor(uint16_t publish_time);
void set_qos(uint8_t mqtt_qos);
void set_retain(bool mqtt_retain);
void set_format(uint8_t mqtt_format);
bool get_publish_onchange(uint8_t device_type);
enum Operation { PUBLISH, SUBSCRIBE };
enum Format : uint8_t { NONE = 0, SINGLE, NESTED, HA };
enum Dallas_Format : uint8_t { SENSORID = 1, NUMBER };
enum HA_Climate_Format : uint8_t { CURRENT = 1, SETPOINT, ZERO };
static constexpr uint8_t MQTT_TOPIC_MAX_SIZE = 128; // note this should really match the user setting in mqttSettings.maxTopicLength
@@ -100,15 +98,14 @@ class Mqtt {
static void publish_ha(const std::string & topic, const JsonObject & payload);
static void publish_ha(const __FlashStringHelper * topic, const JsonObject & payload);
static void register_mqtt_ha_binary_sensor(const __FlashStringHelper * name, const uint8_t device_type, const char * entity);
static void register_mqtt_ha_sensor(const char * prefix,
const __FlashStringHelper * suffix,
static void register_mqtt_ha_sensor(uint8_t type,
const char * prefix,
const __FlashStringHelper * name,
const uint8_t device_type,
const char * entity,
const __FlashStringHelper * uom,
const __FlashStringHelper * entity,
const uint8_t uom,
const __FlashStringHelper * icon);
static void register_command(const uint8_t device_type, const uint8_t device_id, const __FlashStringHelper * cmd, cmdfunction_p cb);
static void register_command(const uint8_t device_type, const __FlashStringHelper * cmd, cmdfunction_p cb);
static void show_topic_handlers(uuid::console::Shell & shell, const uint8_t device_type);
static void show_mqtt(uuid::console::Shell & shell);
@@ -129,6 +126,10 @@ class Mqtt {
#endif
}
static AsyncMqttClient * client() {
return mqttClient_;
}
static bool enabled() {
return mqtt_enabled_;
}
@@ -141,12 +142,36 @@ class Mqtt {
mqtt_publish_fails_ = 0;
}
static uint8_t mqtt_format() {
return mqtt_format_;
static uint8_t ha_climate_format() {
return ha_climate_format_;
}
static AsyncMqttClient * client() {
return mqttClient_;
static uint8_t dallas_format() {
return dallas_format_;
}
static bool ha_enabled() {
return ha_enabled_;
}
static void ha_climate_format(uint8_t ha_climate_format) {
ha_climate_format_ = ha_climate_format;
}
static void dallas_format(uint8_t dallas_format) {
dallas_format_ = dallas_format;
}
static void ha_enabled(bool ha_enabled) {
ha_enabled_ = ha_enabled;
}
void set_qos(uint8_t mqtt_qos) {
mqtt_qos_ = mqtt_qos;
}
void set_retain(bool mqtt_retain) {
mqtt_retain_ = mqtt_retain;
}
private:
@@ -232,8 +257,10 @@ class Mqtt {
static uint32_t publish_time_mixer_;
static uint32_t publish_time_other_;
static uint32_t publish_time_sensor_;
static uint8_t mqtt_format_;
static bool mqtt_enabled_;
static uint8_t dallas_format_;
static uint8_t ha_climate_format_;
static bool ha_enabled_;
};
} // namespace emsesp

View File

@@ -27,10 +27,6 @@ void Shower::start() {
shower_timer_ = settings.shower_timer;
shower_alert_ = settings.shower_alert;
});
if (Mqtt::enabled()) {
send_mqtt_stat(false); // send first MQTT publish
}
}
void Shower::loop() {
@@ -100,14 +96,26 @@ void Shower::send_mqtt_stat(bool state) {
return;
}
// if we're in HA mode make sure we've first sent out the HA MQTT Discovery config topic
if ((Mqtt::mqtt_format() == Mqtt::Format::HA) && (!ha_config_)) {
Mqtt::register_mqtt_ha_binary_sensor(F("Shower Active"), EMSdevice::DeviceType::BOILER, "shower_active");
ha_config_ = true;
}
char s[7];
Mqtt::publish(F("shower_active"), Helpers::render_boolean(s, state));
// if we're in HA mode make sure we've first sent out the HA MQTT Discovery config topic
if ((Mqtt::ha_enabled()) && (!ha_configdone_)) {
ha_configdone_ = true;
StaticJsonDocument<EMSESP_JSON_SIZE_HA_CONFIG> doc;
doc["name"] = FJSON("Shower Active");
doc["uniq_id"] = FJSON("shower_active");
doc["~"] = System::hostname(); // default ems-esp
doc["stat_t"] = FJSON("~/shower_active");
JsonObject dev = doc.createNestedObject("dev");
JsonArray ids = dev.createNestedArray("ids");
ids.add("ems-esp");
char topic[100];
snprintf_P(topic, sizeof(topic), PSTR("homeassistant/binary_sensor/%s/shower_active/config"), System::hostname().c_str());
Mqtt::publish_ha(topic, doc.as<JsonObject>()); // publish the config payload with retain flag
}
}
// turn back on the hot water for the shower
@@ -134,7 +142,7 @@ void Shower::shower_alert_start() {
// Publish shower data
// returns true if added to MQTT queue went ok
void Shower::publish_values() {
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
char s[50];
doc["shower_timer"] = Helpers::render_boolean(s, shower_timer_);

View File

@@ -37,6 +37,8 @@ class Shower {
void start();
void loop();
void send_mqtt_stat(bool state);
bool shower_alert() const {
return shower_alert_;
}
@@ -65,11 +67,10 @@ class Shower {
void publish_values();
void shower_alert_start();
void shower_alert_stop();
void send_mqtt_stat(bool state);
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 ha_config_ = false; // for HA MQTT Discovery
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 ha_configdone_ = false; // for HA MQTT Discovery
bool shower_on_;
uint32_t timer_start_; // ms
uint32_t timer_pause_; // ms

View File

@@ -34,7 +34,7 @@ uuid::syslog::SyslogService System::syslog_;
#endif
// init statics
uint32_t System::heap_start_ = 0;
uint32_t System::heap_start_ = 1; // avoid using 0 to divide-by-zero later
bool System::upload_status_ = false;
bool System::hide_led_ = false;
uint8_t System::led_gpio_ = 0;
@@ -45,7 +45,7 @@ std::string System::hostname_;
// send on/off to a gpio pin
// value: true = HIGH, false = LOW
// http://ems-esp/api?device=system&cmd=pin&data=1&id=2
// e.g. http://ems-esp/api?device=system&cmd=pin&data=1&id=2
bool System::command_pin(const char * value, const int8_t id) {
if (id < 0) {
return false;
@@ -184,7 +184,7 @@ void System::syslog_init() {
// first call. Sets memory and starts up the UART Serial bridge
void System::start() {
// set the inital free mem
if (heap_start_ == 0) {
if (heap_start_ < 2) {
#ifndef EMSESP_STANDALONE
heap_start_ = ESP.getFreeHeap();
#else
@@ -225,7 +225,7 @@ void System::other_init() {
void System::init() {
led_init(); // init LED
other_init();
other_init(); // boolean format and analog setting
syslog_init(); // init SysLog
@@ -319,7 +319,7 @@ void System::send_heartbeat() {
uint8_t frag_memory = ESP.getHeapFragmentation();
#endif
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_SMALL> doc;
StaticJsonDocument<EMSESP_JSON_SIZE_SMALL> doc;
uint8_t ems_status = EMSESP::bus_status();
if (ems_status == EMSESP::BUS_STATUS_TX_ERRORS) {
@@ -378,10 +378,13 @@ void System::set_led_speed(uint32_t speed) {
led_monitor();
}
void System::reset_system_check() {
last_system_check_ = 0; // force the LED to go from fast flash to pulse
send_heartbeat();
}
// 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();
@@ -551,11 +554,11 @@ void System::show_system(uuid::console::Shell & shell) {
shell.printfln(F("Syslog: disabled"));
} else {
shell.printfln(F("Syslog:"));
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(host_fmt), !settings.syslog_host.isEmpty() ? settings.syslog_host.c_str() : uuid::read_flash_string(F_(unset)).c_str());
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(log_level_fmt), uuid::log::format_level_lowercase(static_cast<uuid::log::Level>(settings.syslog_level)));
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(mark_interval_fmt), settings.syslog_mark_interval);
}
});
@@ -688,18 +691,18 @@ void System::console_commands(Shell & shell, unsigned int context) {
flash_string_vector{F_(set)},
[](Shell & shell, const std::vector<std::string> & arguments __attribute__((unused))) {
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) {
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(hostname_fmt),
wifiSettings.hostname.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str()
: wifiSettings.hostname.c_str());
});
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & wifiSettings) {
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(wifi_ssid_fmt),
wifiSettings.ssid.isEmpty() ? uuid::read_flash_string(F_(unset)).c_str()
: wifiSettings.ssid.c_str());
shell.print(F_(1space));
shell.print(F(" "));
shell.printfln(F_(wifi_password_fmt), wifiSettings.ssid.isEmpty() ? F_(unset) : F_(asterisks));
});
});
@@ -724,7 +727,7 @@ bool System::check_upgrade() {
if (LittleFS.begin()) {
#if defined(EMSESP_FORCE_SERIAL)
Serial.begin(115200);
Serial.println(F("FS is Littlefs"));
Serial.println(F("FS is already LittleFS"));
Serial.end();
#endif
return false;
@@ -750,10 +753,10 @@ bool System::check_upgrade() {
Serial.begin(115200);
bool failed = false;
File file;
JsonObject network, general, mqtt, custom_settings;
StaticJsonDocument<EMSESP_MAX_JSON_SIZE_LARGE> doc;
bool failed = false;
File file;
JsonObject network, general, mqtt, custom_settings;
StaticJsonDocument<EMSESP_JSON_SIZE_LARGE> doc;
// open the system settings:
// {
@@ -816,7 +819,6 @@ bool System::check_upgrade() {
EMSESP::esp8266React.getMqttSettingsService()->update(
[&](MqttSettings & mqttSettings) {
mqttSettings.host = mqtt["ip"] | FACTORY_MQTT_HOST;
mqttSettings.mqtt_format = (mqtt["nestedjson"] ? Mqtt::Format::NESTED : Mqtt::Format::SINGLE);
mqttSettings.mqtt_qos = mqtt["qos"] | 0;
mqttSettings.mqtt_retain = mqtt["retain"];
mqttSettings.username = mqtt["user"] | "";
@@ -899,26 +901,25 @@ bool System::check_upgrade() {
Serial.end();
delay(1000);
restart();
return true;
return true; // will never get here
#else
return false;
#endif
}
// export all settings to JSON text
// http://ems-esp/api?device=system&cmd=settings
// e.g. http://ems-esp/api?device=system&cmd=settings
// value and id are ignored
bool System::command_settings(const char * value, const int8_t id, JsonObject & json) {
#ifdef EMSESP_STANDALONE
json["test"] = "testing system info command";
#else
EMSESP::esp8266React.getWiFiSettingsService()->read([&](WiFiSettings & settings) {
char s[7];
JsonObject node = json.createNestedObject("WIFI");
node["ssid"] = settings.ssid;
// node["password"] = settings.password;
node["hostname"] = settings.hostname;
node["static_ip_config"] = Helpers::render_boolean(s, settings.staticIPConfig);
node["static_ip_config"] = settings.staticIPConfig;
JsonUtils::writeIP(node, "local_ip", settings.localIP);
JsonUtils::writeIP(node, "gateway_ip", settings.gatewayIP);
JsonUtils::writeIP(node, "subnet_mask", settings.subnetMask);
@@ -954,7 +955,9 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
node["publish_time_mixer"] = settings.publish_time_mixer;
node["publish_time_other"] = settings.publish_time_other;
node["publish_time_sensor"] = settings.publish_time_sensor;
node["mqtt_format"] = settings.mqtt_format;
node["dallas_format"] = settings.dallas_format;
node["ha_climate_format"] = settings.ha_climate_format;
node["ha_enabled"] = settings.ha_enabled;
node["mqtt_qos"] = settings.mqtt_qos;
node["mqtt_retain"] = Helpers::render_boolean(s, settings.mqtt_retain);
});
@@ -1004,7 +1007,7 @@ bool System::command_settings(const char * value, const int8_t id, JsonObject &
}
// export status information including some basic settings
// http://ems-esp/api?device=system&cmd=info
// e.g. http://ems-esp/api?device=system&cmd=info
bool System::command_info(const char * value, const int8_t id, JsonObject & json) {
JsonObject node;
@@ -1028,7 +1031,9 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
node["publish_time_mixer"] = settings.publish_time_mixer;
node["publish_time_other"] = settings.publish_time_other;
node["publish_time_sensor"] = settings.publish_time_sensor;
node["mqtt_format"] = settings.mqtt_format;
node["dallas_format"] = settings.dallas_format;
node["ha_enabled"] = settings.ha_enabled;
node["ha_climate_format"] = settings.ha_climate_format;
node["mqtt_qos"] = settings.mqtt_qos;
node["mqtt_retain"] = Helpers::render_boolean(s, settings.mqtt_retain);
});
@@ -1095,10 +1100,10 @@ bool System::command_info(const char * value, const int8_t id, JsonObject & json
}
#if defined(EMSESP_TEST)
// run a test
// e.g. http://ems-esp/api?device=system&cmd=test&data=boiler
// run a test, e.g. http://ems-esp/api?device=system&cmd=test&data=boiler
bool System::command_test(const char * value, const int8_t id) {
return (Test::run_test(value, id));
Test::run_test(value, id);
return true;
}
#endif

View File

@@ -63,11 +63,11 @@ class System {
static void upload_status(bool in_progress);
static bool upload_status();
static void show_mem(const char * note);
static void init();
static void led_init();
static void syslog_init();
static void other_init();
void reset_system_check();
static void init();
static void led_init();
static void syslog_init();
static void other_init();
bool check_upgrade();
void send_heartbeat();
@@ -106,9 +106,10 @@ class System {
static void wifi_reconnect();
static int8_t wifi_quality();
bool system_healthy_ = false;
uint32_t led_flash_speed_ = LED_WARNING_BLINK_FAST; // default boot flashes quickly
uint32_t last_heartbeat_ = 0;
bool system_healthy_ = false;
uint32_t led_flash_speed_ = LED_WARNING_BLINK_FAST; // default boot flashes quickly
uint32_t last_heartbeat_ = 0;
uint32_t last_system_check_ = 0;
static bool upload_status_; // true if we're in the middle of a OTA firmware upload
static uint32_t heap_start_;

View File

@@ -73,6 +73,7 @@ Telegram::Telegram(const uint8_t operation,
, offset(offset)
, message_length(message_length) {
// copy complete telegram data over, preventing buffer overflow
// faster than using std::move()
for (uint8_t i = 0; ((i < message_length) && (i < EMS_MAX_TELEGRAM_MESSAGE_LENGTH)); i++) {
message_data[i] = data[i];
}

View File

@@ -40,8 +40,6 @@ static constexpr uint8_t EMS_VALUE_BOOL = 0xFF; // used to mark that somethi
static constexpr uint8_t EMS_VALUE_BOOL_OFF = 0x00; // boolean false
static constexpr uint8_t EMS_VALUE_BOOL_ON = 0x01; // boolean true. True can be 0x01 or 0xFF sometimes
static constexpr uint8_t EMS_VALUE_TIME = 0xFD; // for converting uint32 to time strings
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

View File

@@ -105,8 +105,8 @@ bool Test::run_test(const char * command, int8_t id) {
// add controller
add_device(0x09, 114);
add_device(0x28, 160); // MM100, WWC
add_device(0x29, 161); // MM200, WWC
add_device(0x28, 160); // MM100
add_device(0x29, 161); // MM200
add_device(0x20, 160); // MM100
// WWC1 on 0x29
@@ -115,6 +115,9 @@ bool Test::run_test(const char * command, int8_t id) {
// WWC2 on 0x28
uart_telegram({0xA8, 0x00, 0xFF, 0x00, 0x02, 0x31, 0x02, 0x35, 0x00, 0x3C, 0x00, 0x3C, 0x3C, 0x46, 0x02, 0x03, 0x03, 0x00, 0x3C});
// HC1 on 0x20
uart_telegram({0xA0, 00, 0xFF, 00, 01, 0xD7, 00, 00, 00, 0x80, 00, 00, 00, 00, 03, 0xC5});
return true;
}
@@ -149,28 +152,26 @@ bool Test::run_test(const char * command, int8_t id) {
if (strcmp(command, "solar") == 0) {
EMSESP::logger().info(F("Testing solar..."));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
add_device(0x30, 163); // SM100
// 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});
uart_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});
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});
uart_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});
rx_telegram({0xB0, 00, 0xFF, 0x18, 02, 0x62, 0x80, 00, 0xB8});
uart_telegram({0xB0, 00, 0xFF, 0x18, 02, 0x62, 0x80, 00, 0xB8});
EMSESP::send_raw_telegram("B0 00 FF 18 02 62 80 00 B8");
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
return true;
}
if (strcmp(command, "heatpump") == 0) {
EMSESP::logger().info(F("Testing heatpump..."));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
add_device(0x38, 200); // Enviline module
add_device(0x10, 192); // FW120 thermostat
@@ -189,13 +190,12 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
// switch to su
shell.add_flags(CommandFlags::ADMIN);
// change MQTT format
EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) {
// mqttSettings.mqtt_format = Mqtt::Format::SINGLE;
// mqttSettings.mqtt_format = Mqtt::Format::NESTED;
mqttSettings.mqtt_format = Mqtt::Format::HA;
return StateUpdateResult::CHANGED;
});
// init stuff
Mqtt::ha_enabled(true);
Mqtt::dallas_format(1);
Mqtt::ha_climate_format(1);
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
emsesp::EMSESP::watch(EMSESP::Watch::WATCH_RAW); // raw
std::string command(20, '\0');
if ((cmd.empty()) || (cmd == "default")) {
@@ -273,8 +273,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (command == "devices") {
shell.printfln(F("Testing devices..."));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS); // this is important otherwise nothing will be picked up!
// A fake response - UBADevices(0x07)
rx_telegram({0x08, 0x00, 0x07, 0x00, 0x0B, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00});
}
@@ -355,19 +353,58 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
uart_telegram({0x98, 0x00, 0x06, 0x00, 0x00, 0x03, 0x04, 0x0C, 0x02, 0x33, 0x06, 00, 00, 00, 00, 00, 00});
shell.invoke_command("show");
shell.invoke_command("call boiler info");
StaticJsonDocument<500> doc;
JsonObject root = doc.to<JsonObject>();
EMSESP::device_info_web(2, root); // show thermostat. use 1 for boiler
serializeJsonPretty(doc, shell);
// test call
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject json = doc.to<JsonObject>();
(void)emsesp::Command::call(EMSdevice::DeviceType::BOILER, "info", nullptr, -1, json);
// bool has_data = emsesp::Command::call(EMSdevice::DeviceType::SYSTEM, "test", "boiler", -1, json);
#if defined(EMSESP_STANDALONE)
Serial.print(COLOR_BRIGHT_MAGENTA);
if (json.size() != 0) {
serializeJson(doc, Serial);
}
shell.println();
Serial.print(COLOR_RESET);
#endif
for (const auto & emsdevice : EMSESP::emsdevices) {
if (emsdevice) {
if (emsdevice->unique_id() == 1) {
DynamicJsonDocument doc(EMSESP_JSON_SIZE_XLARGE_DYN);
JsonObject root = doc.to<JsonObject>();
emsdevice->generate_values_json_web(root);
#if defined(EMSESP_STANDALONE)
Serial.print(COLOR_BRIGHT_MAGENTA);
Serial.print("memoryUsage=");
Serial.print(doc.memoryUsage());
Serial.println();
Serial.print("measureMsgPack=");
Serial.print(measureMsgPack(doc));
Serial.println();
Serial.print("measureJson=");
Serial.print(measureJson(doc));
Serial.println();
serializeJson(doc, Serial);
Serial.print(COLOR_RESET);
Serial.println();
#endif
}
}
}
return;
}
if (command == "boiler") {
shell.printfln(F("Testing boiler..."));
run_test("boiler");
shell.invoke_command("show");
// shell.invoke_command("call boiler info");
shell.invoke_command("call boiler info");
shell.invoke_command("call system publish");
shell.invoke_command("show mqtt");
}
if (command == "fr120") {
@@ -408,11 +445,14 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (command == "solar") {
shell.printfln(F("Testing Solar"));
run_test("solar");
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
uart_telegram("30 00 FF 0A 02 6A 04"); // SM100 pump on (1)
EMSESP::show_device_values(shell);
uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off 0
uart_telegram("30 00 FF 0A 02 6A 03"); // SM100 pump off (0)
EMSESP::show_device_values(shell);
shell.invoke_command("call system publish");
// EMSESP::send_raw_telegram("B0 00 FF 18 02 62 80 00 B8");
}
if (command == "heatpump") {
@@ -425,8 +465,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (command == "solar200") {
shell.printfln(F("Testing Solar SM200"));
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
add_device(0x30, 164); // SM200
// SM100Monitor - type 0x0362 EMS+ - for SM100 and SM200
@@ -452,10 +490,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
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);
add_device(0x10, 158); // RC300
add_device(0x48, 189); // KM200
@@ -514,10 +548,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
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
add_device(0x18, 157); // Bosch CR100 - https://github.com/proddy/EMS-ESP/issues/355
// RCPLUSStatusMessage_HC1(0x01A5)
@@ -822,16 +852,6 @@ void Test::run_test(uuid::console::Shell & shell, const std::string & cmd) {
if (command == "mixer") {
shell.printfln(F("Testing Mixer..."));
// change MQTT format
EMSESP::esp8266React.getMqttSettingsService()->updateWithoutPropagation([&](MqttSettings & mqttSettings) {
// mqttSettings.mqtt_format = Mqtt::Format::SINGLE;
// mqttSettings.mqtt_format = Mqtt::Format::NESTED;
mqttSettings.mqtt_format = Mqtt::Format::HA;
return StateUpdateResult::CHANGED;
});
EMSESP::rxservice_.ems_mask(EMSbus::EMS_MASK_BUDERUS);
run_test("mixer");
// check for error "No telegram type handler found for ID 0x255 (src 0x20)"

View File

@@ -35,10 +35,16 @@
#include "telegram.h"
#include "mqtt.h"
#include "emsesp.h"
#include "command.h"
namespace emsesp {
#define EMSESP_TEST_DEFAULT "boiler"
// #define EMSESP_TEST_DEFAULT "thermostat"
// #define EMSESP_TEST_DEFAULT "solar"
// #define EMSESP_TEST_DEFAULT "mixer"
// #define EMSESP_TEST_DEFAULT "web"
#define EMSESP_TEST_DEFAULT "general"
// #define EMSESP_TEST_DEFAULT "boiler"
class Test {
public:

View File

@@ -186,6 +186,9 @@ uint16_t EMSuart::transmit(const uint8_t * buf, const uint8_t len) {
if (len == 0 || len >= EMS_MAXBUFFERSIZE) {
return EMS_TX_STATUS_ERR;
}
if (tx_mode_ == 0) {
return EMS_TX_STATUS_OK;
}
if (tx_mode_ > 5) { // timer controlled modes
for (uint8_t i = 0; i < len; i++) {

View File

@@ -41,7 +41,6 @@ bool EMSuart::sending_ = false;
// Important: must not use ICACHE_FLASH_ATTR
//
void ICACHE_RAM_ATTR EMSuart::emsuart_rx_intr_handler(void * para) {
if (USIS(EMSUART_UART) & ((1 << UIBD))) { // BREAK detection = End of EMS data block
USC0(EMSUART_UART) &= ~(1 << UCBRK); // reset tx-brk
if (sending_) { // irq tx_mode is interrupted by <brk>, should never happen
@@ -233,6 +232,9 @@ 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
}
if (tx_mode_ == 0) {
return EMS_TX_STATUS_OK;
}
// timer controlled modes with extra delay
if (tx_mode_ >= 5) {

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "2.1.1b6"
#define EMSESP_APP_VERSION "2.3.0b0"