mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-08 00:39:50 +03:00
version 1.2.3. See ChangeLog
This commit is contained in:
@@ -65,9 +65,6 @@ Ticker showerColdShotStopTimer;
|
||||
#define TOPIC_SHOWER_COLDSHOT "shower_coldshot" // used to trigger a coldshot from HA publish
|
||||
#define SHOWER_ALARM "shower_alarm" // for notifying HA that shower time has reached its limit
|
||||
|
||||
// logging - EMS_SYS_LOGGING_VERBOSE, EMS_SYS_LOGGING_NONE, EMS_SYS_LOGGING_BASIC (see ems.h)
|
||||
#define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_NONE
|
||||
|
||||
// shower settings for DEBUGGING only
|
||||
#ifdef SHOWER_TEST
|
||||
#undef SHOWER_PAUSE_TIME
|
||||
@@ -99,7 +96,7 @@ command_t PROGMEM project_cmds[] = {
|
||||
|
||||
{"l [n]", "set logging (0=none, 1=raw, 2=basic, 3=thermostat only, 4=verbose)"},
|
||||
{"s", "show statistics"},
|
||||
{"D", "auto Detect EMS connected devices"},
|
||||
{"D", "scan EMS connected Devices"},
|
||||
{"h", "list supported EMS telegram type IDs"},
|
||||
{"M", "publish to MQTT"},
|
||||
{"Q", "print Tx Queue"},
|
||||
@@ -133,9 +130,9 @@ unsigned long timestamp; // for internal timings, via millis()
|
||||
|
||||
uint8_t last_boilerActive = 0xFF; // for remembering last setting of the tap water or heating on/off
|
||||
|
||||
// logging messages with fixed strings (newline done automatically)
|
||||
// logging messages with fixed strings
|
||||
void myDebugLog(const char * s) {
|
||||
if (ems_getLogging() != EMS_SYS_LOGGING_NONE) {
|
||||
if (ems_getLogging() >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug(s);
|
||||
}
|
||||
}
|
||||
@@ -387,6 +384,7 @@ void showInfo() {
|
||||
|
||||
_renderFloatValue("Setpoint room temperature", "C", EMS_Thermostat.setpoint_roomTemp);
|
||||
_renderFloatValue("Current room temperature", "C", EMS_Thermostat.curr_roomTemp);
|
||||
|
||||
if (EMS_Thermostat.mode == 0) {
|
||||
myDebug(" Mode is set to low");
|
||||
} else if (EMS_Thermostat.mode == 1) {
|
||||
@@ -482,13 +480,22 @@ void publishValues(bool force) {
|
||||
rootThermostat[THERMOSTAT_CURRTEMP] = _float_to_char(s, EMS_Thermostat.curr_roomTemp);
|
||||
rootThermostat[THERMOSTAT_SELTEMP] = _float_to_char(s, EMS_Thermostat.setpoint_roomTemp);
|
||||
|
||||
// send mode 0=low, 1=manual, 2=auto
|
||||
if (EMS_Thermostat.mode == 0) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "low";
|
||||
} else if (EMS_Thermostat.mode == 1) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "manual";
|
||||
if (ems_getThermostatModel() == EMS_MODEL_RC20) {
|
||||
if (EMS_Thermostat.mode == 0) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "low";
|
||||
} else if (EMS_Thermostat.mode == 1) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "manual";
|
||||
} else {
|
||||
rootThermostat[THERMOSTAT_MODE] = "auto";
|
||||
}
|
||||
} else {
|
||||
rootThermostat[THERMOSTAT_MODE] = "auto";
|
||||
if (EMS_Thermostat.mode == 0) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "night";
|
||||
} else if (EMS_Thermostat.mode == 1) {
|
||||
rootThermostat[THERMOSTAT_MODE] = "day";
|
||||
} else {
|
||||
rootThermostat[THERMOSTAT_MODE] = "auto";
|
||||
}
|
||||
}
|
||||
|
||||
rlen = rootThermostat.measureLength();
|
||||
@@ -562,7 +569,7 @@ void myDebugCallback() {
|
||||
ems_printTxQueue();
|
||||
break;
|
||||
case 'D': // Auto detect EMS devices
|
||||
ems_getAllVersions();
|
||||
ems_scanDevices();
|
||||
break;
|
||||
default:
|
||||
myDebug("Unknown command. Use ? for help.");
|
||||
@@ -629,8 +636,6 @@ void MQTTcallback(unsigned int type, const char * topic, const char * message) {
|
||||
myESP.mqttSubscribe(TOPIC_THERMOSTAT_CMD_MODE);
|
||||
myESP.mqttSubscribe(TOPIC_SHOWER_TIMER);
|
||||
myESP.mqttSubscribe(TOPIC_SHOWER_ALERT);
|
||||
myESP.mqttSubscribe(TOPIC_BOILER_TAPWATER_ACTIVE);
|
||||
myESP.mqttSubscribe(TOPIC_BOILER_HEATING_ACTIVE);
|
||||
myESP.mqttSubscribe(TOPIC_SHOWER_COLDSHOT);
|
||||
|
||||
// publish to HA the status of the Shower parameters
|
||||
@@ -689,7 +694,7 @@ void MQTTcallback(unsigned int type, const char * topic, const char * message) {
|
||||
void WIFICallback() {
|
||||
// when finally we're all set up, we can fire up the uart (this will enable the UART interrupts)
|
||||
#ifdef DEBUG_SUPPORT
|
||||
myDebug("Warning, in DEBUG mode. EMS bus is disabled. See -DDEBUG_SUPPORT build option.");
|
||||
myDebug("Warning, in DEBUG mode. EMS bus has been disabled. See -DDEBUG_SUPPORT build option.");
|
||||
#else
|
||||
// Important! This is where we enable the UART service to scan the incoming serial Tx/Rx bus signals
|
||||
// This is done after we have a WiFi signal to avoid any resource conflicts
|
||||
@@ -697,26 +702,20 @@ void WIFICallback() {
|
||||
myDebug("[UART] Opened Rx/Tx connection");
|
||||
#endif
|
||||
|
||||
// now that we're connected, send a version request to see what things are on the EMS bus
|
||||
myDebug("Starting up. Finding what devices are on the EMS bus...");
|
||||
ems_getAllVersions();
|
||||
// now that we're connected, check to see if we boiler and thermostat set
|
||||
// otherwise this will initiate a self scan
|
||||
ems_setModels();
|
||||
}
|
||||
|
||||
// Initialize the boiler settings
|
||||
void initBoiler() {
|
||||
// default settings
|
||||
Boiler_Status.shower_timer = BOILER_SHOWER_TIMER;
|
||||
Boiler_Status.shower_alert = BOILER_SHOWER_ALERT;
|
||||
|
||||
// init shower
|
||||
void initShower() {
|
||||
// default showr settings
|
||||
Boiler_Status.shower_timer = BOILER_SHOWER_TIMER;
|
||||
Boiler_Status.shower_alert = BOILER_SHOWER_ALERT;
|
||||
Boiler_Shower.timerStart = 0;
|
||||
Boiler_Shower.timerPause = 0;
|
||||
Boiler_Shower.duration = 0;
|
||||
Boiler_Shower.doingColdShot = false;
|
||||
|
||||
ems_setLogging(BOILER_DEFAULT_LOGGING); // set default logging
|
||||
|
||||
ems_init(); // call ems.cpp's init function to set all the internal params
|
||||
}
|
||||
|
||||
// call PublishValues without forcing, so using CRC to see if we really need to publish
|
||||
@@ -728,7 +727,7 @@ void do_publishValues() {
|
||||
void do_ledcheck() {
|
||||
#ifndef NO_LED
|
||||
if (ems_getBusConnected()) {
|
||||
digitalWrite(BOILER_LED, (BOILER_LED == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard high=off
|
||||
digitalWrite(BOILER_LED, (BOILER_LED == LED_BUILTIN) ? LOW : HIGH); // light on. For onboard LED high=off
|
||||
} else {
|
||||
int state = digitalRead(BOILER_LED);
|
||||
digitalWrite(BOILER_LED, !state);
|
||||
@@ -761,7 +760,7 @@ void do_regularUpdates() {
|
||||
|
||||
// turn off hot water to send a shot of cold
|
||||
void _showerColdShotStart() {
|
||||
myDebugLog("Shower: doing a shot of cold");
|
||||
myDebugLog("[Shower] doing a shot of cold water");
|
||||
ems_setWarmTapWaterActivated(false);
|
||||
Boiler_Shower.doingColdShot = true;
|
||||
// start the timer for n seconds which will reset the water back to hot
|
||||
@@ -771,11 +770,10 @@ void _showerColdShotStart() {
|
||||
// turn back on the hot water for the shower
|
||||
void _showerColdShotStop() {
|
||||
if (Boiler_Shower.doingColdShot) {
|
||||
myDebugLog("Shower: finished shot of cold. hot water back on");
|
||||
myDebugLog("[Shower] finished shot of cold. hot water back on");
|
||||
ems_setWarmTapWaterActivated(true);
|
||||
Boiler_Shower.doingColdShot = false;
|
||||
// disable the timer
|
||||
showerColdShotStopTimer.detach();
|
||||
showerColdShotStopTimer.detach(); // disable the timer
|
||||
}
|
||||
}
|
||||
|
||||
@@ -800,13 +798,13 @@ void showerCheck() {
|
||||
// first check to see if hot water has been on long enough to be recognized as a Shower/Bath
|
||||
if (!Boiler_Shower.showerOn && (timestamp - Boiler_Shower.timerStart) > SHOWER_MIN_DURATION) {
|
||||
Boiler_Shower.showerOn = true;
|
||||
myDebugLog("Shower: hot water still running, starting shower timer");
|
||||
myDebugLog("[Shower] hot water still running, starting shower timer");
|
||||
}
|
||||
// check if the shower has been on too long
|
||||
else if ((((timestamp - Boiler_Shower.timerStart) > SHOWER_MAX_DURATION) && !Boiler_Shower.doingColdShot)
|
||||
&& Boiler_Status.shower_alert) {
|
||||
myESP.sendHACommand(SHOWER_ALARM);
|
||||
myDebugLog("Shower: exceeded max shower time");
|
||||
myDebugLog("[Shower] exceeded max shower time");
|
||||
_showerColdShotStart();
|
||||
}
|
||||
}
|
||||
@@ -814,7 +812,6 @@ void showerCheck() {
|
||||
// if it just turned off, record the time as it could be a short pause
|
||||
if ((Boiler_Shower.timerStart != 0) && (Boiler_Shower.timerPause == 0)) {
|
||||
Boiler_Shower.timerPause = timestamp;
|
||||
myDebugLog("Shower: hot water turned off");
|
||||
}
|
||||
|
||||
// if shower has been off for longer than the wait time
|
||||
@@ -831,14 +828,13 @@ void showerCheck() {
|
||||
strlcat(s, itoa((uint8_t)((Boiler_Shower.duration / 1000) % 60), buffer, 10), sizeof(s));
|
||||
strlcat(s, " seconds", sizeof(s));
|
||||
if (ems_getLogging() != EMS_SYS_LOGGING_NONE) {
|
||||
myDebug("Shower: finished with duration %s", s);
|
||||
myDebug("[Shower] finished with duration %s", s);
|
||||
}
|
||||
myESP.mqttPublish(TOPIC_SHOWERTIME, s); // publish to HA
|
||||
}
|
||||
}
|
||||
|
||||
// reset everything
|
||||
myDebugLog("Shower: resetting timers");
|
||||
Boiler_Shower.timerStart = 0;
|
||||
Boiler_Shower.timerPause = 0;
|
||||
Boiler_Shower.showerOn = false;
|
||||
@@ -865,13 +861,20 @@ void setup() {
|
||||
regularUpdatesTimer.attach(REGULARUPDATES_TIME, do_regularUpdates); // regular reads from the EMS
|
||||
|
||||
// set up myESP for Wifi, MQTT, MDNS and Telnet
|
||||
// with callbacks
|
||||
myESP.setup(APP_HOSTNAME, APP_NAME, APP_VERSION, WIFI_SSID, WIFI_PASSWORD, MQTT_IP, MQTT_USER, MQTT_PASS);
|
||||
myESP.consoleSetCallBackProjectCmds(project_cmds, ArraySize(project_cmds), myDebugCallback); // set up Telnet commands
|
||||
myESP.setWIFICallback(WIFICallback);
|
||||
myESP.setMQTTCallback(MQTTcallback);
|
||||
|
||||
// init Boiler specific parameters
|
||||
initBoiler();
|
||||
// init Shower specific parameters
|
||||
initShower();
|
||||
|
||||
ems_setLogging(BOILER_DEFAULT_LOGGING); // set default logging. see my_config.h
|
||||
|
||||
// init the EMS bus
|
||||
// call ems.cpp's init function to set all the internal params
|
||||
ems_init(MY_BOILER_MODELID, MY_THERMOSTAT_MODELID);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
239
src/ems.cpp
239
src/ems.cpp
@@ -62,7 +62,7 @@ const _Model_Type Model_Types[] = {
|
||||
{EMS_MODEL_ES73, 76, 0x10, "Sieger ES73"},
|
||||
{EMS_MODEL_RC20, 77, 0x17, "RC20 (Nefit Moduline 300)"},
|
||||
{EMS_MODEL_RC30, 78, 0x10, "RC30 (Nefit Moduline 400)"},
|
||||
{EMS_MODEL_RC35, 86, 0x10, "RC35"},
|
||||
{EMS_MODEL_RC35, 86, 0x10, "RC35 (or compatible"},
|
||||
{EMS_MODEL_EASY, 202, 0x18, "TC100 (Nefit Easy/CT100)"}
|
||||
|
||||
};
|
||||
@@ -165,7 +165,7 @@ uint8_t _last_TxTelgramCRC; // CRC of last Tx sent, for checking duplicates
|
||||
|
||||
// init stats and counters and buffers
|
||||
// uses -255 or 255 for values that haven't been set yet (EMS_VALUE_INT_NOTSET and EMS_VALUE_FLOAT_NOTSET)
|
||||
void ems_init() {
|
||||
void ems_init(_EMS_MODEL_ID boiler_modelid, _EMS_MODEL_ID thermostat_modelid) {
|
||||
// overall status
|
||||
EMS_Sys_Status.emsRxPgks = 0;
|
||||
EMS_Sys_Status.emsTxPkgs = 0;
|
||||
@@ -177,12 +177,6 @@ void ems_init() {
|
||||
EMS_Sys_Status.emsTxEnabled = true; // start up with Tx enabled
|
||||
EMS_Sys_Status.emsBusConnected = false;
|
||||
|
||||
// no thermostat or boiler attached yet
|
||||
EMS_Sys_Status.emsBoilerEnabled = false;
|
||||
EMS_Sys_Status.emsThermostatEnabled = false;
|
||||
|
||||
EMS_Sys_Status.emsLogging = EMS_SYS_LOGGING_NONE; // Verbose logging is off
|
||||
|
||||
// thermostat
|
||||
EMS_Thermostat.setpoint_roomTemp = EMS_VALUE_FLOAT_NOTSET;
|
||||
EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET;
|
||||
@@ -195,7 +189,6 @@ void ems_init() {
|
||||
EMS_Thermostat.mode = 255; // dummy value
|
||||
|
||||
EMS_Thermostat.type_id = EMS_ID_NONE;
|
||||
EMS_Thermostat.model_id = EMS_MODEL_NONE;
|
||||
EMS_Thermostat.read_supported = false;
|
||||
EMS_Thermostat.write_supported = false;
|
||||
|
||||
@@ -244,8 +237,11 @@ void ems_init() {
|
||||
EMS_Boiler.tapwaterActive = EMS_VALUE_INT_NOTSET; // Hot tap water is on/off
|
||||
EMS_Boiler.heatingActive = EMS_VALUE_INT_NOTSET; // Central heating is on/off
|
||||
|
||||
EMS_Boiler.type_id = EMS_ID_NONE;
|
||||
EMS_Boiler.model_id = EMS_MODEL_NONE;
|
||||
EMS_Boiler.type_id = EMS_ID_NONE;
|
||||
|
||||
// for lookup later
|
||||
EMS_Boiler.model_id = boiler_modelid;
|
||||
EMS_Thermostat.model_id = thermostat_modelid;
|
||||
|
||||
// counters
|
||||
_ems_PollCount = 0;
|
||||
@@ -281,16 +277,11 @@ void ems_setEmsRefreshed(bool b) {
|
||||
}
|
||||
|
||||
bool ems_getBoilerEnabled() {
|
||||
return EMS_Sys_Status.emsBoilerEnabled;
|
||||
}
|
||||
|
||||
void ems_setBoilerEnabled(bool b) {
|
||||
EMS_Sys_Status.emsBoilerEnabled = b;
|
||||
myDebug("Boiler set to %s", EMS_Sys_Status.emsBoilerEnabled ? "enabled" : "disabled");
|
||||
return (EMS_Boiler.model_id != EMS_MODEL_NONE);
|
||||
}
|
||||
|
||||
bool ems_getThermostatEnabled() {
|
||||
return EMS_Sys_Status.emsThermostatEnabled;
|
||||
return (EMS_Thermostat.model_id != EMS_MODEL_NONE);
|
||||
}
|
||||
|
||||
bool ems_getBusConnected() {
|
||||
@@ -300,11 +291,6 @@ bool ems_getBusConnected() {
|
||||
return EMS_Sys_Status.emsBusConnected;
|
||||
}
|
||||
|
||||
void ems_setThermostatEnabled(bool b) {
|
||||
EMS_Sys_Status.emsThermostatEnabled = b;
|
||||
myDebug("Thermostat set to %s", EMS_Sys_Status.emsThermostatEnabled ? "enabled" : "disabled");
|
||||
}
|
||||
|
||||
_EMS_SYS_LOGGING ems_getLogging() {
|
||||
return EMS_Sys_Status.emsLogging;
|
||||
}
|
||||
@@ -330,6 +316,22 @@ void ems_setLogging(_EMS_SYS_LOGGING loglevel) {
|
||||
}
|
||||
}
|
||||
|
||||
// if the thermostat or boiler models have been provided, set them up
|
||||
void ems_setModels() {
|
||||
bool found = false;
|
||||
if (ems_getThermostatModel() != EMS_MODEL_NONE) {
|
||||
found = _ems_setModel(ems_getThermostatModel());
|
||||
}
|
||||
|
||||
if (ems_getBoilerModel() != EMS_MODEL_NONE) {
|
||||
found = found && _ems_setModel(ems_getBoilerModel());
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
ems_scanDevices(); // initiate a scan
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate CRC checksum using lookup table for speed
|
||||
* len is length of data in bytes (including the CRC byte at end)
|
||||
@@ -482,6 +484,14 @@ void _ems_sendTelegram() {
|
||||
}
|
||||
|
||||
// if Tx is disabled, don't do anything and ignore the request
|
||||
// this could be because the boiler has yet to be found and the type_id is still empty
|
||||
if (EMS_TxTelegram.dest == EMS_ID_NONE) {
|
||||
EMS_Sys_Status.emsTxStatus = EMS_TX_IDLE; // finished sending
|
||||
EMS_TxQueue.shift(); // remove from queue
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is no destination, also delete it from the queue
|
||||
if (!EMS_Sys_Status.emsTxEnabled) {
|
||||
myDebug("Tx is disabled. Ignoring %s request to 0x%02X.",
|
||||
((EMS_TxTelegram.action == EMS_TX_TELEGRAM_WRITE) ? "write" : "read"),
|
||||
@@ -953,54 +963,56 @@ void _process_UBAMonitorSlow(uint8_t * data, uint8_t length) {
|
||||
}
|
||||
|
||||
/**
|
||||
* RC20StatusMessage - type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long
|
||||
* type 0x91 - data from the RC20 thermostat (0x17) - 15 bytes long
|
||||
* For reading the temp values only
|
||||
* received every 60 seconds
|
||||
*/
|
||||
void _process_RC20StatusMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(2, data);
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC20StatusMessage_setpoint]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC20StatusMessage_curr, data);
|
||||
|
||||
EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT
|
||||
}
|
||||
|
||||
/**
|
||||
* RC30StatusMessage - type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long
|
||||
* type 0x41 - data from the RC30 thermostat (0x10) - 14 bytes long
|
||||
* For reading the temp values only
|
||||
* received every 60 seconds
|
||||
*/
|
||||
void _process_RC30StatusMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[1]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(2, data);
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC30StatusMessage_setpoint]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(EMS_TYPE_RC30StatusMessage_curr, data);
|
||||
|
||||
EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT
|
||||
}
|
||||
|
||||
/**
|
||||
* RC35StatusMessage - type 0x3E - data from the RC35 thermostat (0x10)
|
||||
* type 0x3E - data from the RC35 thermostat (0x10) - 16 bytes
|
||||
* For reading the temp values only
|
||||
* received every 60 seconds
|
||||
*/
|
||||
void _process_RC35StatusMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[2]) / (float)2;
|
||||
EMS_Thermostat.curr_roomTemp = _toFloat(3, data);
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)data[EMS_TYPE_RC35StatusMessage_setpoint]) / (float)2;
|
||||
|
||||
// There is no current room temperature sensor in this telegram
|
||||
EMS_Thermostat.curr_roomTemp = EMS_VALUE_FLOAT_NOTSET;
|
||||
|
||||
EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT
|
||||
}
|
||||
|
||||
/**
|
||||
* EasyStatusMessage - type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long
|
||||
* type 0x0A - data from the Nefit Easy/TC100 thermostat (0x18) - 31 bytes long
|
||||
* The Easy has a digital precision of its floats to 2 decimal places, so values is divided by 100
|
||||
*/
|
||||
void _process_EasyStatusMessage(uint8_t * data, uint8_t length) {
|
||||
EMS_Thermostat.curr_roomTemp = ((float)(((data[8] << 8) + data[9]))) / 100;
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)(((data[10] << 8) + data[11]))) / 100;
|
||||
EMS_Thermostat.curr_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_curr] << 8) + data[9]))) / 100;
|
||||
EMS_Thermostat.setpoint_roomTemp = ((float)(((data[EMS_TYPE_EasyStatusMessage_setpoint] << 8) + data[11]))) / 100;
|
||||
|
||||
EMS_Sys_Status.emsRefreshed = true; // triggers a send the values back to Home Assistant via MQTT
|
||||
}
|
||||
|
||||
/**
|
||||
* RC20Temperature - type 0xA8 - for reading the mode from the RC20 thermostat (0x17)
|
||||
* type 0xA8 - for reading the mode from the RC20 thermostat (0x17)
|
||||
* received only after requested
|
||||
*/
|
||||
void _process_RC20Set(uint8_t * data, uint8_t length) {
|
||||
@@ -1008,7 +1020,7 @@ void _process_RC20Set(uint8_t * data, uint8_t length) {
|
||||
}
|
||||
|
||||
/**
|
||||
* RC30Temperature - type 0xA7 - for reading the mode from the RC30 thermostat (0x10)
|
||||
* type 0xA7 - for reading the mode from the RC30 thermostat (0x10)
|
||||
* received only after requested
|
||||
*/
|
||||
void _process_RC30Set(uint8_t * data, uint8_t length) {
|
||||
@@ -1016,7 +1028,8 @@ void _process_RC30Set(uint8_t * data, uint8_t length) {
|
||||
}
|
||||
|
||||
/**
|
||||
* RC35Temperature - type 0x3D - for reading the mode from the RC35 thermostat (0x10)
|
||||
* type 0x3D - for reading the mode from the RC35 thermostat (0x10)
|
||||
* Working Mode Heating Circuit 1 (HC1)
|
||||
* received only after requested
|
||||
*/
|
||||
void _process_RC35Set(uint8_t * data, uint8_t length) {
|
||||
@@ -1024,14 +1037,14 @@ void _process_RC35Set(uint8_t * data, uint8_t length) {
|
||||
}
|
||||
|
||||
/**
|
||||
* RCOutdoorTempMessage - type 0xA3 - for external temp settings from the the RC* thermostats
|
||||
* type 0xA3 - for external temp settings from the the RC* thermostats
|
||||
*/
|
||||
void _process_RCOutdoorTempMessage(uint8_t * data, uint8_t length) {
|
||||
// add support here if you're reading external sensors
|
||||
}
|
||||
|
||||
/**
|
||||
* Version - type 0x02 - get the firmware version and type of an EMS device
|
||||
* type 0x02 - get the firmware version and type of an EMS device
|
||||
*/
|
||||
void _process_Version(uint8_t * data, uint8_t length) {
|
||||
// ignore short messages that we can't interpret
|
||||
@@ -1049,16 +1062,17 @@ void _process_Version(uint8_t * data, uint8_t length) {
|
||||
char version[10] = {0};
|
||||
snprintf(version, sizeof(version), "%02d.%02d", major, minor);
|
||||
|
||||
// use product ID to search
|
||||
while (i < _Model_Types_max) {
|
||||
if (Model_Types[i].product_id == product_id) {
|
||||
typeFound = true; // we have a matching product id
|
||||
typeFound = true; // we have a matching product id. i is the index.
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (!typeFound) {
|
||||
myDebug("Unknown device found. Product ID %d, Version %s", product_id, version);
|
||||
myDebug("Unrecognized device found. Product ID %d, Version %s", product_id, version);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1072,7 +1086,7 @@ void _process_Version(uint8_t * data, uint8_t length) {
|
||||
}
|
||||
|
||||
// set a thermostat
|
||||
if ((isThermostat) && (!EMS_Sys_Status.emsThermostatEnabled)) {
|
||||
if (isThermostat) {
|
||||
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug("Found a Thermostat. Model %s with TypeID 0x%02X, Product ID %d, Version %s",
|
||||
Model_Types[i].model_string,
|
||||
@@ -1080,19 +1094,21 @@ void _process_Version(uint8_t * data, uint8_t length) {
|
||||
product_id,
|
||||
version);
|
||||
}
|
||||
// set its capabilities
|
||||
EMS_Thermostat.model_id = Model_Types[i].model_id;
|
||||
EMS_Thermostat.type_id = Model_Types[i].type_id;
|
||||
EMS_Thermostat.read_supported = Thermostat_Types[j].read_supported;
|
||||
EMS_Thermostat.write_supported = Thermostat_Types[j].write_supported;
|
||||
strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version));
|
||||
|
||||
ems_setThermostatEnabled(true);
|
||||
ems_getThermostatValues(); // get Thermostat values (if supported)
|
||||
}
|
||||
// if we don't have a thermostat set, use this one
|
||||
if (!ems_getThermostatEnabled()) {
|
||||
myDebug("Setting the Thermostat to this one.");
|
||||
// set its capabilities
|
||||
EMS_Thermostat.model_id = Model_Types[i].model_id;
|
||||
EMS_Thermostat.type_id = Model_Types[i].type_id;
|
||||
EMS_Thermostat.read_supported = Thermostat_Types[j].read_supported;
|
||||
EMS_Thermostat.write_supported = Thermostat_Types[j].write_supported;
|
||||
strlcpy(EMS_Thermostat.version, version, sizeof(EMS_Thermostat.version));
|
||||
|
||||
// otherwise assume its a boiler
|
||||
if ((!isThermostat) && (!EMS_Sys_Status.emsBoilerEnabled)) {
|
||||
ems_getThermostatValues(); // get Thermostat values (if supported)
|
||||
}
|
||||
} else {
|
||||
// otherwise assume its a boiler
|
||||
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug("Found a Boiler compatible device, model %s with TypeID 0x%02X, Product ID %d, Version %s",
|
||||
Model_Types[i].model_string,
|
||||
@@ -1100,13 +1116,93 @@ void _process_Version(uint8_t * data, uint8_t length) {
|
||||
product_id,
|
||||
version);
|
||||
}
|
||||
EMS_Boiler.type_id = Model_Types[i].type_id;
|
||||
EMS_Boiler.model_id = Model_Types[i].model_id;
|
||||
strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version));
|
||||
|
||||
ems_setBoilerEnabled(true);
|
||||
if (!ems_getBoilerEnabled()) {
|
||||
myDebug("Setting the Boiler to this one.");
|
||||
EMS_Boiler.type_id = Model_Types[i].type_id;
|
||||
EMS_Boiler.model_id = Model_Types[i].model_id;
|
||||
strlcpy(EMS_Boiler.version, version, sizeof(EMS_Boiler.version));
|
||||
|
||||
ems_getBoilerValues(); // get Boiler values that we would usually have to wait for
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a MODEL_ID, look up its data and set either a Thermostat or Boiler
|
||||
* return false if not found or no need to set
|
||||
*/
|
||||
bool _ems_setModel(_EMS_MODEL_ID model_id) {
|
||||
if (model_id == EMS_MODEL_NONE) {
|
||||
return false; // invalid model_id
|
||||
}
|
||||
|
||||
// see if we have a valid model_id
|
||||
uint8_t model_loc = 0;
|
||||
bool found = false;
|
||||
uint8_t i = 0;
|
||||
const _Model_Type * model_type;
|
||||
while (i < _Model_Types_max) {
|
||||
model_type = &Model_Types[model_loc];
|
||||
if (model_type->model_id == model_id) {
|
||||
found = true; // we have a matching product id. i is the index.
|
||||
break;
|
||||
}
|
||||
model_loc++;
|
||||
}
|
||||
if (!found) {
|
||||
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug("Unknown model specified");
|
||||
}
|
||||
return false; // unknown model_id
|
||||
}
|
||||
|
||||
// next check to see if its a known thermostat
|
||||
// j will have pointer to the Thermostat details
|
||||
bool isThermostat = false;
|
||||
uint8_t j = 0;
|
||||
while (j < _Thermostat_Types_max) {
|
||||
if (Thermostat_Types[j].model_id == model_id) {
|
||||
isThermostat = true; // we have a matching model
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
// set a thermostat
|
||||
if (isThermostat) {
|
||||
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug("Setting Thermostat. Model %s with TypeID 0x%02X, Product ID %d",
|
||||
model_type->model_string,
|
||||
model_type->type_id,
|
||||
model_type->product_id);
|
||||
}
|
||||
|
||||
// set its capabilities
|
||||
EMS_Thermostat.model_id = model_type->model_id;
|
||||
EMS_Thermostat.type_id = model_type->type_id;
|
||||
EMS_Thermostat.read_supported = Thermostat_Types[j].read_supported;
|
||||
EMS_Thermostat.write_supported = Thermostat_Types[j].write_supported;
|
||||
strlcpy(EMS_Thermostat.version, "unknown", sizeof(EMS_Thermostat.version));
|
||||
|
||||
ems_getThermostatValues(); // get Thermostat values (if supported)
|
||||
} else {
|
||||
// otherwise assume its a boiler
|
||||
if (EMS_Sys_Status.emsLogging >= EMS_SYS_LOGGING_BASIC) {
|
||||
myDebug("Setting Boiler. Model %s with TypeID 0x%02X, Product ID %d",
|
||||
model_type->model_string,
|
||||
model_type->type_id,
|
||||
model_type->product_id);
|
||||
}
|
||||
|
||||
EMS_Boiler.model_id = model_type->model_id;
|
||||
EMS_Boiler.type_id = model_type->type_id;
|
||||
strlcpy(EMS_Boiler.version, "unknown", sizeof(EMS_Boiler.version));
|
||||
|
||||
ems_getBoilerValues(); // get Boiler values that we would usually have to wait for
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1203,7 +1299,7 @@ void ems_printTxQueue() {
|
||||
* Generic function to return various settings from the thermostat
|
||||
*/
|
||||
void ems_getThermostatValues() {
|
||||
if (!EMS_Sys_Status.emsThermostatEnabled) {
|
||||
if (!ems_getThermostatEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1283,7 +1379,7 @@ char * _ems_buildModelString(char * buffer, uint8_t size, _EMS_MODEL_ID model_id
|
||||
*/
|
||||
char * ems_getThermostatType(char * buffer) {
|
||||
uint8_t size = 64;
|
||||
if (!EMS_Sys_Status.emsThermostatEnabled) {
|
||||
if (!ems_getThermostatEnabled()) {
|
||||
strlcpy(buffer, "<not enabled>", size);
|
||||
} else {
|
||||
_ems_buildModelString(buffer, size, EMS_Thermostat.model_id);
|
||||
@@ -1296,7 +1392,7 @@ char * ems_getThermostatType(char * buffer) {
|
||||
*/
|
||||
char * ems_getBoilerType(char * buffer) {
|
||||
uint8_t size = 64;
|
||||
if (!EMS_Sys_Status.emsBoilerEnabled) {
|
||||
if (!ems_getBoilerEnabled()) {
|
||||
strlcpy(buffer, "<not enabled>", size);
|
||||
} else {
|
||||
_ems_buildModelString(buffer, size, EMS_Boiler.model_id);
|
||||
@@ -1306,20 +1402,19 @@ char * ems_getBoilerType(char * buffer) {
|
||||
|
||||
// returns the model type for a thermostat
|
||||
_EMS_MODEL_ID ems_getThermostatModel() {
|
||||
if (EMS_Sys_Status.emsThermostatEnabled) {
|
||||
return (EMS_Thermostat.model_id);
|
||||
} else {
|
||||
return EMS_MODEL_NONE;
|
||||
}
|
||||
return (EMS_Thermostat.model_id);
|
||||
}
|
||||
|
||||
// returns the model type for a boiler
|
||||
_EMS_MODEL_ID ems_getBoilerModel() {
|
||||
return (EMS_Boiler.model_id);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find the versions of our connected devices
|
||||
*/
|
||||
void ems_getAllVersions() {
|
||||
void ems_scanDevices() {
|
||||
// send Version request to all known EMS devices
|
||||
ems_setThermostatEnabled(false);
|
||||
ems_setBoilerEnabled(false);
|
||||
myDebug("Scanning EMS bus for devices. This may take a few seconds.");
|
||||
for (int i = 0; i < _Model_Types_max; i++) {
|
||||
if ((Model_Types[i].model_id != EMS_MODEL_NONE) && (Model_Types[i].model_id != EMS_MODEL_SERVICEKEY)) {
|
||||
@@ -1448,7 +1543,7 @@ void ems_sendRawTelegram(char * telegram) {
|
||||
* Set the temperature of the thermostat
|
||||
*/
|
||||
void ems_setThermostatTemp(float temperature) {
|
||||
if (!EMS_Sys_Status.emsThermostatEnabled) {
|
||||
if (!ems_getThermostatEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1480,7 +1575,7 @@ void ems_setThermostatTemp(float temperature) {
|
||||
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC30StatusMessage;
|
||||
} else if ((model_id == EMS_MODEL_RC35) || (model_id == EMS_MODEL_ES73)) {
|
||||
EMS_TxTelegram.type = EMS_TYPE_RC35Set;
|
||||
EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp;
|
||||
EMS_TxTelegram.offset = EMS_OFFSET_RC35Set_temp_day; // day mode only for now
|
||||
EMS_TxTelegram.comparisonPostRead = EMS_TYPE_RC35StatusMessage;
|
||||
}
|
||||
|
||||
@@ -1499,7 +1594,7 @@ void ems_setThermostatTemp(float temperature) {
|
||||
* 0xA8 on a RC20 and 0xA7 on RC30
|
||||
*/
|
||||
void ems_setThermostatMode(uint8_t mode) {
|
||||
if (!EMS_Sys_Status.emsThermostatEnabled) {
|
||||
if (!ems_getThermostatEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
67
src/ems.h
67
src/ems.h
@@ -58,25 +58,34 @@
|
||||
#define EMS_TYPE_RCOutdoorTempMessage 0xA3 // is an automatic thermostat broadcast, outdoor external temp
|
||||
|
||||
// RC20 specific
|
||||
#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode
|
||||
#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature
|
||||
#define EMS_TYPE_RC20StatusMessage 0x91 // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC20Set 0xA8 // for setting values like temp and mode
|
||||
#define EMS_OFFSET_RC20Set_mode 23 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC20Set_temp 28 // position of thermostat setpoint temperature
|
||||
#define EMS_TYPE_RC20StatusMessage_setpoint 1 // setpoint temp
|
||||
#define EMS_TYPE_RC20StatusMessage_curr 2 // current temp
|
||||
|
||||
// RC30 specific
|
||||
#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode
|
||||
#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
|
||||
#define EMS_TYPE_RC30StatusMessage 0x41 // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC30Set 0xA7 // for setting values like temp and mode
|
||||
#define EMS_OFFSET_RC30Set_mode 23 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC30Set_temp 28 // position of thermostat setpoint temperature
|
||||
#define EMS_TYPE_RC30StatusMessage_setpoint 1 // setpoint temp
|
||||
#define EMS_TYPE_RC30StatusMessage_curr 2 // current temp
|
||||
|
||||
// RC35 specific - not implemented yet
|
||||
#define EMS_TYPE_RC35StatusMessage 0x3E // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC35Set 0x3D // for setting values like temp and mode
|
||||
#define EMS_OFFSET_RC35Set_mode 7 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC35Set_temp 2 // position of thermostat setpoint temperature
|
||||
// RC35 specific
|
||||
#define EMS_TYPE_RC35StatusMessage 0x3E // is an automatic thermostat broadcast giving us temps
|
||||
#define EMS_TYPE_RC35StatusMessage_setpoint 2 // desired temp
|
||||
#define EMS_TYPE_RC35Set 0x3D // for setting values like temp and mode (Working mode HC1)
|
||||
#define EMS_OFFSET_RC35Set_mode 6 // position of thermostat mode
|
||||
#define EMS_OFFSET_RC35Set_temp_day 2 // position of thermostat setpoint temperature for day time
|
||||
#define EMS_OFFSET_RC35Set_temp_night 1 // position of thermostat setpoint temperature for night time
|
||||
|
||||
// Easy specific
|
||||
#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat
|
||||
#define EMS_TYPE_EasyStatusMessage 0x0A // reading values on an Easy Thermostat
|
||||
#define EMS_TYPE_EasyStatusMessage_setpoint 10 // setpoint temp
|
||||
#define EMS_TYPE_EasyStatusMessage_curr 8 // current temp
|
||||
|
||||
|
||||
// default values
|
||||
#define EMS_VALUE_INT_ON 1 // boolean true
|
||||
@@ -119,17 +128,15 @@ typedef enum {
|
||||
typedef struct {
|
||||
_EMS_RX_STATUS emsRxStatus;
|
||||
_EMS_TX_STATUS emsTxStatus;
|
||||
uint16_t emsRxPgks; // received
|
||||
uint16_t emsTxPkgs; // sent
|
||||
uint16_t emxCrcErr; // CRC errors
|
||||
bool emsPollEnabled; // flag enable the response to poll messages
|
||||
bool emsTxEnabled; // flag if we're allowing sending of Tx packages
|
||||
bool emsThermostatEnabled; // if there is a RCxx thermostat active
|
||||
bool emsBoilerEnabled; // is the boiler online
|
||||
_EMS_SYS_LOGGING emsLogging; // logging
|
||||
bool emsRefreshed; // fresh data, needs to be pushed out to MQTT
|
||||
bool emsBusConnected; // is there an active bus
|
||||
unsigned long emsRxTimestamp; // timestamp of last EMS poll
|
||||
uint16_t emsRxPgks; // received
|
||||
uint16_t emsTxPkgs; // sent
|
||||
uint16_t emxCrcErr; // CRC errors
|
||||
bool emsPollEnabled; // flag enable the response to poll messages
|
||||
bool emsTxEnabled; // flag if we're allowing sending of Tx packages
|
||||
_EMS_SYS_LOGGING emsLogging; // logging
|
||||
bool emsRefreshed; // fresh data, needs to be pushed out to MQTT
|
||||
bool emsBusConnected; // is there an active bus
|
||||
unsigned long emsRxTimestamp; // timestamp of last EMS poll
|
||||
} _EMS_Sys_Status;
|
||||
|
||||
// The Tx send package
|
||||
@@ -310,7 +317,7 @@ typedef struct {
|
||||
|
||||
// function definitions
|
||||
extern void ems_parseTelegram(uint8_t * telegram, uint8_t len);
|
||||
void ems_init();
|
||||
void ems_init(_EMS_MODEL_ID boiler_modelid, _EMS_MODEL_ID thermostat_modelid);
|
||||
void ems_doReadCommand(uint8_t type, uint8_t dest, bool forceRefresh = false);
|
||||
void ems_sendRawTelegram(char * telegram);
|
||||
|
||||
@@ -322,12 +329,11 @@ void ems_setWarmTapWaterActivated(bool activated);
|
||||
void ems_setExperimental(uint8_t value);
|
||||
void ems_setPoll(bool b);
|
||||
void ems_setTxEnabled(bool b);
|
||||
void ems_setThermostatEnabled(bool b);
|
||||
void ems_setBoilerEnabled(bool b);
|
||||
void ems_setLogging(_EMS_SYS_LOGGING loglevel);
|
||||
void ems_setEmsRefreshed(bool b);
|
||||
void ems_setWarmWaterModeComfort(bool comfort);
|
||||
bool ems_checkEMSBUSAlive();
|
||||
void ems_setModels();
|
||||
|
||||
void ems_getThermostatValues();
|
||||
void ems_getBoilerValues();
|
||||
@@ -339,9 +345,10 @@ bool ems_getBusConnected();
|
||||
_EMS_SYS_LOGGING ems_getLogging();
|
||||
uint8_t ems_getEmsTypesCount();
|
||||
bool ems_getEmsRefreshed();
|
||||
void ems_getAllVersions();
|
||||
_EMS_MODEL_ID ems_getThermostatModel();
|
||||
_EMS_MODEL_ID ems_getBoilerModel();
|
||||
|
||||
void ems_scanDevices();
|
||||
void ems_printAllTypes();
|
||||
char * ems_getThermostatType(char * buffer);
|
||||
void ems_printTxQueue();
|
||||
@@ -354,6 +361,8 @@ void _debugPrintPackage(const char * prefix, uint8_t * data, uint8_t len, con
|
||||
void _ems_clearTxData();
|
||||
int _ems_findModel(_EMS_MODEL_ID model_id);
|
||||
char * _ems_buildModelString(char * buffer, uint8_t size, _EMS_MODEL_ID model_id);
|
||||
bool _ems_setModel(_EMS_MODEL_ID model_id);
|
||||
|
||||
|
||||
// global so can referenced in other classes
|
||||
extern _EMS_Sys_Status EMS_Sys_Status;
|
||||
|
||||
@@ -42,3 +42,19 @@
|
||||
|
||||
// set this if using an external temperature sensor like a DS18B20
|
||||
#define TEMPERATURE_SENSOR_PIN D7
|
||||
|
||||
// logging - EMS_SYS_LOGGING_VERBOSE, EMS_SYS_LOGGING_NONE, EMS_SYS_LOGGING_BASIC (see ems.h)
|
||||
// this can be changed via the Telnet console using the 'l' command
|
||||
#define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_NONE
|
||||
//#define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_VERBOSE
|
||||
//#define BOILER_DEFAULT_LOGGING EMS_SYS_LOGGING_BASIC
|
||||
|
||||
// By default the EMS bus will be scanned for known devices. You can override this here
|
||||
// by fixing the Boiler and Thermostat types
|
||||
// Options are in ems.h and include..
|
||||
// boilers: EMS_MODEL_BK15, EMS_MODEL_UBA, EMS_MODEL_BC10, EMS_MODEL_MM10, EMS_MODEL_WM10
|
||||
// thermostats: EMS_MODEL_ES73, EMS_MODEL_RC20, EMS_MODEL_RC30, EMS_MODEL_RC35, EMS_MODEL_EASY
|
||||
#define MY_BOILER_MODELID EMS_MODEL_NONE
|
||||
#define MY_THERMOSTAT_MODELID EMS_MODEL_NONE
|
||||
//#define MY_BOILER_MODELID EMS_MODEL_UBA
|
||||
//#define MY_THERMOSTAT_MODELID EMS_MODEL_RC20
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#define APP_NAME "EMS-ESP-Boiler"
|
||||
#define APP_VERSION "1.2.2"
|
||||
#define APP_VERSION "1.2.3"
|
||||
#define APP_HOSTNAME "boiler"
|
||||
|
||||
Reference in New Issue
Block a user