mirror of https://github.com/arendst/Tasmota.git
Porting OT implementation from my personal repo
This commit is contained in:
parent
1332325a5b
commit
af98b26ef1
|
@ -0,0 +1,598 @@
|
||||||
|
/*
|
||||||
|
xsns_68_opentherm.ino - OpenTherm protocol support for Tasmota
|
||||||
|
|
||||||
|
Copyright (C) 2020 Yuriy Sannikov
|
||||||
|
|
||||||
|
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 <OpenTherm.h>
|
||||||
|
|
||||||
|
#ifdef USE_OPENTHERM
|
||||||
|
|
||||||
|
#define XSNS_68 68
|
||||||
|
|
||||||
|
// Hot water and boiler parameter ranges
|
||||||
|
#define OT_HOT_WATER_MIN 23
|
||||||
|
#define OT_HOT_WATER_MAX 55
|
||||||
|
#define OT_BOILER_MIN 40
|
||||||
|
#define OT_BOILER_MAX 85
|
||||||
|
|
||||||
|
#define OT_HOT_WATER_DEFAULT 36;
|
||||||
|
#define OT_BOILER_DEFAULT 85;
|
||||||
|
|
||||||
|
// Seconds before OT will make an attempt to connect to the boiler after connection error
|
||||||
|
#define SNS_OT_DISCONNECT_COOLDOWN_SECONDS 10
|
||||||
|
|
||||||
|
// Count of the OpenThermSettingsFlags
|
||||||
|
#define OT_FLAGS_COUNT 6
|
||||||
|
enum OpenThermSettingsFlags
|
||||||
|
{
|
||||||
|
// If set, central heating on/off state follows diagnostic indication bit(6), however
|
||||||
|
// EnableCentralHeating flag has a priority over it
|
||||||
|
EnableCentralHeatingOnDiagnostics = 0x01,
|
||||||
|
// If set, DHW is on after restart.
|
||||||
|
EnableHotWater = 0x02,
|
||||||
|
// If set, keep CH always on after restart. If off, follows the EnableCentralHeatingOnDiagnostics rule
|
||||||
|
EnableCentralHeating = 0x04,
|
||||||
|
EnableCooling = 0x08,
|
||||||
|
EnableTemperatureCompensation = 0x10,
|
||||||
|
EnableCentralHeating2 = 0x20,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum OpenThermConnectionStatus
|
||||||
|
{
|
||||||
|
OTC_NONE, // OT not initialized
|
||||||
|
OTC_DISCONNECTED, // OT communication timed out
|
||||||
|
OTC_CONNECTING, // Connecting after start or from DISCONNECTED state
|
||||||
|
OTC_HANDSHAKE, // Wait for the handshake response
|
||||||
|
OTC_READY, // Last Known Good response state is SUCCESS and no requests are in flight
|
||||||
|
OTC_INFLIGHT // Request sent, waiting from the response
|
||||||
|
};
|
||||||
|
|
||||||
|
OpenThermConnectionStatus sns_ot_connection_status = OpenThermConnectionStatus::OTC_NONE;
|
||||||
|
uint8_t sns_ot_disconnect_cooldown = 0;
|
||||||
|
|
||||||
|
OpenTherm *sns_ot_master = NULL;
|
||||||
|
|
||||||
|
// Has valid values if connection status is READY or INFLIGHT
|
||||||
|
typedef struct OT_BOILER_STATUS_T
|
||||||
|
{
|
||||||
|
// Boiler fault code
|
||||||
|
uint8_t m_fault_code;
|
||||||
|
// Boiler OEM fault code
|
||||||
|
uint8_t m_oem_fault_code;
|
||||||
|
// Boilder OEM Diagnostics code
|
||||||
|
uint16_t m_oem_diag_code;
|
||||||
|
// OpenTherm ID(3) response.
|
||||||
|
uint8_t m_slave_flags;
|
||||||
|
// OpenTherm ID(1) codes. Should be used to display state
|
||||||
|
unsigned long m_slave_raw_status;
|
||||||
|
// Desired boiler states
|
||||||
|
bool m_enableCentralHeating;
|
||||||
|
bool m_enableHotWater;
|
||||||
|
bool m_enableCooling;
|
||||||
|
bool m_enableOutsideTemperatureCompensation;
|
||||||
|
bool m_enableCentralHeating2;
|
||||||
|
|
||||||
|
// Some boilers has an input for the heat request. When short, heat is requested
|
||||||
|
// OT ID(0) bit 6 may indicate state of the Heat Request input
|
||||||
|
// By enabling this bit we will set m_enableCentralHeating to true when OT ID(0) bit 6 is set.
|
||||||
|
// This enables to use external mechanical thermostat to enable heating.
|
||||||
|
// Some of the use cases might be setting an emergency temperature to prevent freezing
|
||||||
|
// in case of the software thermostat failure.
|
||||||
|
bool m_useDiagnosticIndicationAsHeatRequest;
|
||||||
|
|
||||||
|
// Hot Water temperature
|
||||||
|
float m_hotWaterSetpoint_read;
|
||||||
|
// Flame Modulation
|
||||||
|
float m_flame_modulation_read;
|
||||||
|
// Boiler Temperature
|
||||||
|
float m_boiler_temperature_read;
|
||||||
|
|
||||||
|
// Boiler desired values
|
||||||
|
float m_boilerSetpoint;
|
||||||
|
float m_hotWaterSetpoint;
|
||||||
|
|
||||||
|
} OT_BOILER_STATUS;
|
||||||
|
|
||||||
|
OT_BOILER_STATUS sns_ot_boiler_status;
|
||||||
|
|
||||||
|
const char *sns_opentherm_connection_stat_to_str(int status)
|
||||||
|
{
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case OpenThermConnectionStatus::OTC_NONE:
|
||||||
|
return "NONE";
|
||||||
|
case OpenThermConnectionStatus::OTC_DISCONNECTED:
|
||||||
|
return "FAULT";
|
||||||
|
case OpenThermConnectionStatus::OTC_CONNECTING:
|
||||||
|
return "CONNECTING";
|
||||||
|
case OpenThermConnectionStatus::OTC_HANDSHAKE:
|
||||||
|
return "HANDSHAKE";
|
||||||
|
case OpenThermConnectionStatus::OTC_READY:
|
||||||
|
return "READY";
|
||||||
|
case OpenThermConnectionStatus::OTC_INFLIGHT:
|
||||||
|
return "BUSY";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_init_boiler_status()
|
||||||
|
{
|
||||||
|
memset(&sns_ot_boiler_status, 0, sizeof(OT_BOILER_STATUS));
|
||||||
|
|
||||||
|
// Settings
|
||||||
|
sns_ot_boiler_status.m_useDiagnosticIndicationAsHeatRequest = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics;
|
||||||
|
sns_ot_boiler_status.m_enableHotWater = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableHotWater;
|
||||||
|
sns_ot_boiler_status.m_enableCentralHeating = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeating;
|
||||||
|
sns_ot_boiler_status.m_enableCooling = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCooling;
|
||||||
|
sns_ot_boiler_status.m_enableOutsideTemperatureCompensation = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableTemperatureCompensation;
|
||||||
|
sns_ot_boiler_status.m_enableCentralHeating2 = Settings.ot_flags & (uint8_t)OpenThermSettingsFlags::EnableCentralHeating2;
|
||||||
|
|
||||||
|
sns_ot_boiler_status.m_boilerSetpoint = (float)Settings.ot_boiler_setpoint;
|
||||||
|
sns_ot_boiler_status.m_hotWaterSetpoint = (float)Settings.ot_hot_water_setpoint;
|
||||||
|
|
||||||
|
sns_ot_boiler_status.m_fault_code = 0;
|
||||||
|
sns_ot_boiler_status.m_oem_fault_code = 0;
|
||||||
|
sns_ot_boiler_status.m_oem_diag_code = 0;
|
||||||
|
sns_ot_boiler_status.m_hotWaterSetpoint_read = 0;
|
||||||
|
sns_ot_boiler_status.m_flame_modulation_read = 0;
|
||||||
|
sns_ot_boiler_status.m_boiler_temperature_read = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ICACHE_RAM_ATTR sns_opentherm_handleInterrupt()
|
||||||
|
{
|
||||||
|
sns_ot_master->handleInterrupt();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_processResponseCallback(unsigned long response, int st)
|
||||||
|
{
|
||||||
|
OpenThermResponseStatus status = (OpenThermResponseStatus)st;
|
||||||
|
AddLog_P2(LOG_LEVEL_DEBUG_MORE,
|
||||||
|
PSTR("[OTH]: Processing response. Status=%s, Response=0x%lX"),
|
||||||
|
sns_ot_master->statusToString(status), response);
|
||||||
|
|
||||||
|
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_HANDSHAKE)
|
||||||
|
{
|
||||||
|
return sns_ot_process_handshake(response, st);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case OpenThermResponseStatus::SUCCESS:
|
||||||
|
if (sns_ot_master->isValidResponse(response))
|
||||||
|
{
|
||||||
|
sns_opentherm_process_success_response(&sns_ot_boiler_status, response);
|
||||||
|
}
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OpenThermResponseStatus::INVALID:
|
||||||
|
sns_opentherm_check_retry_request();
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// Timeout may indicate not valid/supported command or connection error
|
||||||
|
// In this case we do reconnect.
|
||||||
|
// If this command will timeout multiple times, it will be excluded from the rotation later on
|
||||||
|
// after couple of failed attempts. See sns_opentherm_check_retry_request logic
|
||||||
|
case OpenThermResponseStatus::TIMEOUT:
|
||||||
|
sns_opentherm_check_retry_request();
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_DISCONNECTED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sns_opentherm_Init()
|
||||||
|
{
|
||||||
|
if (pin[GPIO_BOILER_OT_RX] < 99 && pin[GPIO_BOILER_OT_TX] < 99)
|
||||||
|
{
|
||||||
|
sns_ot_master = new OpenTherm(pin[GPIO_BOILER_OT_RX], pin[GPIO_BOILER_OT_TX]);
|
||||||
|
sns_ot_master->begin(sns_opentherm_handleInterrupt, sns_opentherm_processResponseCallback);
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_CONNECTING;
|
||||||
|
|
||||||
|
sns_opentherm_init_boiler_status();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
// !warning, sns_opentherm settings are not ready at this point
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_stat(bool json)
|
||||||
|
{
|
||||||
|
if (!sns_ot_master)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const char *statusStr = sns_opentherm_connection_stat_to_str(sns_ot_connection_status);
|
||||||
|
|
||||||
|
if (json)
|
||||||
|
{
|
||||||
|
ResponseAppend_P(PSTR(",\"OPENTHERM\":{"));
|
||||||
|
ResponseAppend_P(PSTR("\"conn\":\"%s\","), statusStr);
|
||||||
|
ResponseAppend_P(PSTR("\"settings\":%d,"), Settings.ot_flags);
|
||||||
|
sns_opentherm_dump_telemetry();
|
||||||
|
ResponseJsonEnd();
|
||||||
|
#ifdef USE_WEBSERVER
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}OpenTherm status{m}%s (0x%X){e}"), statusStr, (int)sns_ot_boiler_status.m_slave_flags);
|
||||||
|
if (sns_ot_connection_status < OpenThermConnectionStatus::OTC_READY)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WSContentSend_P(PSTR("{s}Std/OEM Fault Codes{m}%d / %d{e}"),
|
||||||
|
(int)sns_ot_boiler_status.m_fault_code,
|
||||||
|
(int)sns_ot_boiler_status.m_oem_fault_code);
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("{s}OEM Diagnostic Code{m}%d{e}"),
|
||||||
|
(int)sns_ot_boiler_status.m_oem_diag_code);
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("{s}Hot Water Setpoint{m}%d{e}"),
|
||||||
|
(int)sns_ot_boiler_status.m_hotWaterSetpoint_read);
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("{s}Flame Modulation{m}%d{e}"),
|
||||||
|
(int)sns_ot_boiler_status.m_flame_modulation_read);
|
||||||
|
|
||||||
|
WSContentSend_P(PSTR("{s}Boiler Temp/Setpnt{m}%d / %d{e}"),
|
||||||
|
(int)sns_ot_boiler_status.m_boiler_temperature_read,
|
||||||
|
(int)sns_ot_boiler_status.m_boilerSetpoint);
|
||||||
|
|
||||||
|
if (OpenTherm::isCentralHeatingActive(sns_ot_boiler_status.m_slave_raw_status))
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Central Heating is ACTIVE{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sns_ot_boiler_status.m_enableHotWater)
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Hot Water is Enabled{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenTherm::isHotWaterActive(sns_ot_boiler_status.m_slave_raw_status))
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Hot Water is ACTIVE{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenTherm::isFlameOn(sns_ot_boiler_status.m_slave_raw_status))
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Flame is ACTIVE{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sns_ot_boiler_status.m_enableCooling)
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Cooling is Enabled{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenTherm::isCoolingActive(sns_ot_boiler_status.m_slave_raw_status))
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Cooling is ACTIVE{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OpenTherm::isDiagnostic(sns_ot_boiler_status.m_slave_raw_status))
|
||||||
|
{
|
||||||
|
WSContentSend_P(PSTR("{s}Diagnostic Indication{m}{e}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // USE_WEBSERVER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_ot_start_handshake()
|
||||||
|
{
|
||||||
|
if (!sns_ot_master)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("[OTH]: perform handshake"));
|
||||||
|
|
||||||
|
sns_ot_master->sendRequestAync(
|
||||||
|
OpenTherm::buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0));
|
||||||
|
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_HANDSHAKE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_ot_process_handshake(unsigned long response, int st)
|
||||||
|
{
|
||||||
|
OpenThermResponseStatus status = (OpenThermResponseStatus)st;
|
||||||
|
|
||||||
|
if (status != OpenThermResponseStatus::SUCCESS || !sns_ot_master->isValidResponse(response))
|
||||||
|
{
|
||||||
|
AddLog_P2(LOG_LEVEL_ERROR,
|
||||||
|
PSTR("[OTH]: getSlaveConfiguration failed. Status=%s"),
|
||||||
|
sns_ot_master->statusToString(status));
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_DISCONNECTED;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddLog_P2(LOG_LEVEL_DEBUG, PSTR("[OTH]: getLastResponseStatus SUCCESS. Slave Cfg: %lX"), response);
|
||||||
|
|
||||||
|
sns_ot_boiler_status.m_slave_flags = (response & 0xFF00) >> 8;
|
||||||
|
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_CheckSettings(void)
|
||||||
|
{
|
||||||
|
bool settingsValid = true;
|
||||||
|
|
||||||
|
settingsValid &= Settings.ot_hot_water_setpoint >= OT_HOT_WATER_MIN;
|
||||||
|
settingsValid &= Settings.ot_hot_water_setpoint <= OT_HOT_WATER_MAX;
|
||||||
|
settingsValid &= Settings.ot_boiler_setpoint >= OT_BOILER_MIN;
|
||||||
|
settingsValid &= Settings.ot_boiler_setpoint <= OT_BOILER_MAX;
|
||||||
|
|
||||||
|
if (!settingsValid)
|
||||||
|
{
|
||||||
|
Settings.ot_hot_water_setpoint = OT_HOT_WATER_DEFAULT;
|
||||||
|
Settings.ot_boiler_setpoint = OT_BOILER_DEFAULT;
|
||||||
|
Settings.ot_flags =
|
||||||
|
OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics |
|
||||||
|
OpenThermSettingsFlags::EnableHotWater;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* Command Processing
|
||||||
|
\*********************************************************************************************/
|
||||||
|
const char *sns_opentherm_flag_text(uint8_t mode)
|
||||||
|
{
|
||||||
|
switch ((OpenThermSettingsFlags)mode)
|
||||||
|
{
|
||||||
|
case OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics:
|
||||||
|
return "CHOD";
|
||||||
|
case OpenThermSettingsFlags::EnableHotWater:
|
||||||
|
return "DHW";
|
||||||
|
case OpenThermSettingsFlags::EnableCentralHeating:
|
||||||
|
return "CH";
|
||||||
|
case OpenThermSettingsFlags::EnableCooling:
|
||||||
|
return "COOL";
|
||||||
|
case OpenThermSettingsFlags::EnableTemperatureCompensation:
|
||||||
|
return "OTC";
|
||||||
|
case OpenThermSettingsFlags::EnableCentralHeating2:
|
||||||
|
return "CH2";
|
||||||
|
default:
|
||||||
|
return "?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t sns_opentherm_parse_flag(char *flag)
|
||||||
|
{
|
||||||
|
if (!strncmp(flag, "CHOD", 4))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableCentralHeatingOnDiagnostics;
|
||||||
|
}
|
||||||
|
else if (!strncmp(flag, "COOL", 4))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableCooling;
|
||||||
|
}
|
||||||
|
else if (!strncmp(flag, "DHW", 3))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableHotWater;
|
||||||
|
}
|
||||||
|
else if (!strncmp(flag, "OTC", 3))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableTemperatureCompensation;
|
||||||
|
}
|
||||||
|
else if (!strncmp(flag, "CH2", 3))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableCentralHeating2;
|
||||||
|
}
|
||||||
|
else if (!strncmp(flag, "CH", 2))
|
||||||
|
{
|
||||||
|
return OpenThermSettingsFlags::EnableCentralHeating;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t sns_opentherm_read_flags(char *data, uint32_t len)
|
||||||
|
{
|
||||||
|
uint8_t tokens = 1;
|
||||||
|
for (int i = 0; i < len; ++i)
|
||||||
|
{
|
||||||
|
if (data[i] == ',')
|
||||||
|
{
|
||||||
|
++tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint8_t result = 0;
|
||||||
|
char sub_string[XdrvMailbox.data_len + 1];
|
||||||
|
for (int i = 1; i <= tokens; ++i)
|
||||||
|
{
|
||||||
|
char *flag = subStr(sub_string, data, ",", i);
|
||||||
|
if (!flag)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
result |= sns_opentherm_parse_flag(flag);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
#define D_PRFX_OTHERM "ot_"
|
||||||
|
// set the boiler temperature (CH). Sutable for the PID app.
|
||||||
|
// After restart will use the defaults from the settings
|
||||||
|
#define D_CMND_OTHERM_BOILER_SETPOINT "tboiler"
|
||||||
|
// set hot water (DHW) temperature. Do not write it in the flash memory.
|
||||||
|
// suitable for the temporary changes
|
||||||
|
#define D_CMND_OTHERM_DHW_SETPOINT "twater"
|
||||||
|
// This command will save CH and DHW setpoints into the settings. Those values will be used after system restart
|
||||||
|
// The reason to separate set and save is to reduce flash memory write count, especially if boiler temperature is controlled
|
||||||
|
// by the PID thermostat
|
||||||
|
#define D_CMND_OTHERM_SAVE_SETTINGS "save_setpoints"
|
||||||
|
// Get or set flags
|
||||||
|
|
||||||
|
// EnableCentralHeatingOnDiagnostics -> CHOD
|
||||||
|
// EnableHotWater -> DHW
|
||||||
|
// EnableCentralHeating -> CH
|
||||||
|
// EnableCooling -> COOL
|
||||||
|
// EnableTemperatureCompensation -> OTC
|
||||||
|
// EnableCentralHeating2 -> CH2
|
||||||
|
#define D_CMND_OTHERM_FLAGS "flags"
|
||||||
|
|
||||||
|
// Get/Set boiler status m_enableCentralHeating value. It's equivalent of the EnableCentralHeating settings
|
||||||
|
// flag value, however, this command does not update the settings.
|
||||||
|
// Usefull to buld automations
|
||||||
|
// Please note, if you set it to "0" and EnableCentralHeatingOnDiagnostics is set
|
||||||
|
// boiler will follow the Diagnostics bit and won't turn CH off. When Diagnostics bit cleared,
|
||||||
|
// and "ot_ch" is "1", boiler will keep heating
|
||||||
|
#define D_CMND_SET_CENTRAL_HEATING_ENABLED "ch"
|
||||||
|
|
||||||
|
const char kOpenThermCommands[] PROGMEM = D_PRFX_OTHERM "|" D_CMND_OTHERM_BOILER_SETPOINT "|" D_CMND_OTHERM_DHW_SETPOINT
|
||||||
|
"|" D_CMND_OTHERM_SAVE_SETTINGS "|" D_CMND_OTHERM_FLAGS "|" D_CMND_SET_CENTRAL_HEATING_ENABLED;
|
||||||
|
|
||||||
|
void (*const OpenThermCommands[])(void) PROGMEM = {
|
||||||
|
&sns_opentherm_boiler_setpoint_cmd,
|
||||||
|
&sns_opentherm_hot_water_setpoint_cmd,
|
||||||
|
&sns_opentherm_save_settings_cmd,
|
||||||
|
&sns_opentherm_flags_cmd,
|
||||||
|
&sns_opentherm_set_central_heating_cmd};
|
||||||
|
|
||||||
|
void sns_opentherm_cmd(void) { }
|
||||||
|
void sns_opentherm_boiler_setpoint_cmd(void)
|
||||||
|
{
|
||||||
|
bool query = strlen(XdrvMailbox.data) == 0;
|
||||||
|
if (!query)
|
||||||
|
{
|
||||||
|
sns_ot_boiler_status.m_boilerSetpoint = atof(XdrvMailbox.data);
|
||||||
|
}
|
||||||
|
ResponseCmndFloat(sns_ot_boiler_status.m_boilerSetpoint, Settings.flag2.temperature_resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_hot_water_setpoint_cmd(void)
|
||||||
|
{
|
||||||
|
bool query = strlen(XdrvMailbox.data) == 0;
|
||||||
|
if (!query)
|
||||||
|
{
|
||||||
|
sns_ot_boiler_status.m_hotWaterSetpoint = atof(XdrvMailbox.data);
|
||||||
|
}
|
||||||
|
ResponseCmndFloat(sns_ot_boiler_status.m_hotWaterSetpoint, Settings.flag2.temperature_resolution);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_save_settings_cmd(void)
|
||||||
|
{
|
||||||
|
Settings.ot_hot_water_setpoint = (uint8_t)sns_ot_boiler_status.m_hotWaterSetpoint;
|
||||||
|
Settings.ot_boiler_setpoint = (uint8_t)sns_ot_boiler_status.m_boilerSetpoint;
|
||||||
|
ResponseCmndDone();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_flags_cmd(void)
|
||||||
|
{
|
||||||
|
bool query = strlen(XdrvMailbox.data) == 0;
|
||||||
|
if (!query)
|
||||||
|
{
|
||||||
|
// Set flags value
|
||||||
|
Settings.ot_flags = sns_opentherm_read_flags(XdrvMailbox.data, XdrvMailbox.data_len);
|
||||||
|
// Reset boiler status to apply settings
|
||||||
|
sns_opentherm_init_boiler_status();
|
||||||
|
}
|
||||||
|
bool addComma = false;
|
||||||
|
mqtt_data[0] = 0;
|
||||||
|
for (int pos = 0; pos < OT_FLAGS_COUNT; ++pos)
|
||||||
|
{
|
||||||
|
int mask = 1 << pos;
|
||||||
|
int mode = Settings.ot_flags & (uint8_t)mask;
|
||||||
|
if (mode > 0)
|
||||||
|
{
|
||||||
|
if (addComma)
|
||||||
|
{
|
||||||
|
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,"), mqtt_data);
|
||||||
|
}
|
||||||
|
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s%s"), mqtt_data, sns_opentherm_flag_text(mode));
|
||||||
|
addComma = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_set_central_heating_cmd(void)
|
||||||
|
{
|
||||||
|
bool query = strlen(XdrvMailbox.data) == 0;
|
||||||
|
if (!query)
|
||||||
|
{
|
||||||
|
sns_ot_boiler_status.m_enableCentralHeating = atoi(XdrvMailbox.data);
|
||||||
|
}
|
||||||
|
ResponseCmndNumber(sns_ot_boiler_status.m_enableCentralHeating ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********************************************************************************************\
|
||||||
|
* Interface
|
||||||
|
\*********************************************************************************************/
|
||||||
|
|
||||||
|
bool Xsns68(uint8_t function)
|
||||||
|
{
|
||||||
|
bool result = false;
|
||||||
|
if (FUNC_INIT == function)
|
||||||
|
{
|
||||||
|
if (sns_opentherm_Init())
|
||||||
|
{
|
||||||
|
sns_opentherm_CheckSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sns_ot_master)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (function)
|
||||||
|
{
|
||||||
|
case FUNC_LOOP:
|
||||||
|
sns_ot_master->process();
|
||||||
|
break;
|
||||||
|
case FUNC_EVERY_100_MSECOND:
|
||||||
|
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_READY && sns_ot_master->isReady())
|
||||||
|
{
|
||||||
|
unsigned long request = sns_opentherm_get_next_request(&sns_ot_boiler_status);
|
||||||
|
if (-1 != request)
|
||||||
|
{
|
||||||
|
sns_ot_master->sendRequestAync(request);
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_INFLIGHT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FUNC_EVERY_SECOND:
|
||||||
|
if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_DISCONNECTED)
|
||||||
|
{
|
||||||
|
// If disconnected, wait for the SNS_OT_DISCONNECT_COOLDOWN_SECONDS before the handshake
|
||||||
|
if (sns_ot_disconnect_cooldown == 0)
|
||||||
|
{
|
||||||
|
sns_ot_disconnect_cooldown = SNS_OT_DISCONNECT_COOLDOWN_SECONDS;
|
||||||
|
}
|
||||||
|
else if (--sns_ot_disconnect_cooldown == 0)
|
||||||
|
{
|
||||||
|
sns_ot_connection_status = OpenThermConnectionStatus::OTC_CONNECTING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (sns_ot_connection_status == OpenThermConnectionStatus::OTC_CONNECTING)
|
||||||
|
{
|
||||||
|
sns_ot_start_handshake();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FUNC_COMMAND:
|
||||||
|
result = DecodeCommand(kOpenThermCommands, OpenThermCommands);
|
||||||
|
break;
|
||||||
|
case FUNC_JSON_APPEND:
|
||||||
|
sns_opentherm_stat(1);
|
||||||
|
break;
|
||||||
|
#ifdef USE_WEBSERVER
|
||||||
|
case FUNC_WEB_SENSOR:
|
||||||
|
sns_opentherm_stat(0);
|
||||||
|
break;
|
||||||
|
#endif // USE_WEBSERVER
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // USE_OPENTHERM
|
|
@ -0,0 +1,440 @@
|
||||||
|
/*
|
||||||
|
xsns_68_opentherm_protocol.ino - OpenTherm protocol support for Tasmota
|
||||||
|
|
||||||
|
Copyright (C) 2020 Yuriy Sannikov
|
||||||
|
|
||||||
|
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 "OpenTherm.h"
|
||||||
|
|
||||||
|
#ifdef USE_OPENTHERM
|
||||||
|
|
||||||
|
// Temperature tolerance. If temperature setpoint difference is less than the value,
|
||||||
|
// OT (1)(Control setpoint) command will be skipped
|
||||||
|
#define OPENTHERM_BOILER_SETPOINT_TOLERANCE 1.0
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
uint8_t m_flags;
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
uint8_t notSupported : 1; // If set, boiler does not support this command
|
||||||
|
uint8_t supported : 1; // Set if at least one response were successfull
|
||||||
|
uint8_t retryCount : 2; // Retry counter before notSupported flag being set
|
||||||
|
};
|
||||||
|
} OpenThermParamFlags;
|
||||||
|
|
||||||
|
typedef union {
|
||||||
|
float m_float;
|
||||||
|
uint8_t m_u8;
|
||||||
|
uint16_t m_u16;
|
||||||
|
unsigned long m_ul;
|
||||||
|
bool m_bool;
|
||||||
|
} ResponseStorage;
|
||||||
|
|
||||||
|
typedef struct OpenThermCommandT
|
||||||
|
{
|
||||||
|
const char *m_command_name;
|
||||||
|
uint8_t m_command_code;
|
||||||
|
OpenThermParamFlags m_flags;
|
||||||
|
ResponseStorage m_results[2];
|
||||||
|
unsigned long (*m_ot_make_request)(OpenThermCommandT *self, OT_BOILER_STATUS_T *boilerStatus);
|
||||||
|
void (*m_ot_parse_response)(OpenThermCommandT *self, OT_BOILER_STATUS_T *boilerStatus, unsigned long response);
|
||||||
|
void (*m_ot_appent_telemetry)(OpenThermCommandT *self);
|
||||||
|
} OpenThermCommand;
|
||||||
|
|
||||||
|
OpenThermCommand sns_opentherm_commands[] = {
|
||||||
|
{// Get/Set Slave Status Flags
|
||||||
|
.m_command_name = "SLAVE",
|
||||||
|
.m_command_code = 0,
|
||||||
|
// OpenTherm ID(0) should never go into the notSupported state due to some connectivity issues
|
||||||
|
// otherwice it may lose boiler control
|
||||||
|
.m_flags = {.supported = 1},
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_set_slave_flags,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_slave_flags,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_slave_flags},
|
||||||
|
{// Set boiler temperature
|
||||||
|
.m_command_name = "BTMP",
|
||||||
|
.m_command_code = 0,
|
||||||
|
// OpenTherm ID(1) also should never go into the notSupported state due to some connectivity issues
|
||||||
|
.m_flags = {.supported = 1},
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_set_boiler_temperature,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_set_boiler_temperature,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_boiler_temperature},
|
||||||
|
{// Set Hot Water temperature
|
||||||
|
.m_command_name = "HWTMP",
|
||||||
|
.m_command_code = 0,
|
||||||
|
// OpenTherm ID(56) may not be supported
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_set_boiler_dhw_temperature,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_boiler_dhw_temperature,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_boiler_dhw_temperature},
|
||||||
|
{// Read Application-specific fault flags and OEM fault code
|
||||||
|
.m_command_name = "ASFF",
|
||||||
|
.m_command_code = 0,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_flags,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_flags,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_flags},
|
||||||
|
{// Read An OEM-specific diagnostic/service code
|
||||||
|
.m_command_name = "OEMD",
|
||||||
|
.m_command_code = 0,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_oem_diag,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_oem_diag,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_oem_diag},
|
||||||
|
{// Read Flame modulation
|
||||||
|
.m_command_name = "FLM",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::RelModLevel,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_flame_modulation,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read Boiler Temperature
|
||||||
|
.m_command_name = "TB",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::Tboiler,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_boiler_temperature,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read DHW temperature
|
||||||
|
.m_command_name = "TDHW",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::Tdhw,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_generic_float,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read Outside temperature
|
||||||
|
.m_command_name = "TOUT",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::Toutside,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_generic_float,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read Return water temperature
|
||||||
|
.m_command_name = "TRET",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::Tret,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_generic_float,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read DHW setpoint
|
||||||
|
.m_command_name = "DHWS",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::TdhwSet,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_dhw_setpoint,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
{// Read max CH water setpoint
|
||||||
|
.m_command_name = "TMAX",
|
||||||
|
.m_command_code = (uint8_t)OpenThermMessageID::MaxTSet,
|
||||||
|
.m_flags = 0,
|
||||||
|
.m_results = {{.m_u8 = 0}, {.m_u8 = 0}},
|
||||||
|
.m_ot_make_request = sns_opentherm_get_generic_float,
|
||||||
|
.m_ot_parse_response = sns_opentherm_parse_generic_float,
|
||||||
|
.m_ot_appent_telemetry = sns_opentherm_tele_generic_float},
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/////////////////////////////////// Process Slave Status Flags & Control //////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_set_slave_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
|
||||||
|
{
|
||||||
|
bool centralHeatingIsOn = status->m_enableCentralHeating;
|
||||||
|
|
||||||
|
if (status->m_useDiagnosticIndicationAsHeatRequest) {
|
||||||
|
centralHeatingIsOn |= OpenTherm::isDiagnostic(status->m_slave_raw_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self->m_results[1].m_bool != centralHeatingIsOn) {
|
||||||
|
AddLog_P2(LOG_LEVEL_INFO,
|
||||||
|
PSTR("[OTH]: Central Heating transitioning from %s to %s"),
|
||||||
|
self->m_results[1].m_bool ? "on" : "off",
|
||||||
|
status->m_enableCentralHeating ? "on" : "off");
|
||||||
|
}
|
||||||
|
self->m_results[1].m_bool = centralHeatingIsOn;
|
||||||
|
|
||||||
|
unsigned int data = centralHeatingIsOn |
|
||||||
|
(status->m_enableHotWater << 1) |
|
||||||
|
(status->m_enableCooling << 2) |
|
||||||
|
(status->m_enableOutsideTemperatureCompensation << 3) |
|
||||||
|
(status->m_enableCentralHeating2 << 4);
|
||||||
|
|
||||||
|
data <<= 8;
|
||||||
|
|
||||||
|
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Status, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_slave_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
boilerStatus->m_slave_raw_status = response;
|
||||||
|
self->m_results[0].m_ul = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
#define OT_FLAG_TO_ON_OFF(status, flag) ((((status) & (flag)) != 0) ? 1 : 0)
|
||||||
|
void sns_opentherm_tele_slave_flags(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
unsigned long st = self->m_results[0].m_ul;
|
||||||
|
ResponseAppend_P(PSTR("{\"FAULT\":%d,\"CH\":%d,\"DHW\":%d,\"FL\":%d,\"COOL\":%d,\"CH2\":%d,\"DIAG\":%d,\"RAW\":%lu}"),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x01),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x02),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x04),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x08),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x10),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x20),
|
||||||
|
OT_FLAG_TO_ON_OFF(st, 0x40),
|
||||||
|
st);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// Set Boiler Temperature //////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_set_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
|
||||||
|
{
|
||||||
|
// Assuming some boilers might write setpoint temperature into the Flash memory
|
||||||
|
// Having PID controlled appliance may produce a lot of small fluctuations in the setpoint value
|
||||||
|
// wearing out Boiler flash memory.
|
||||||
|
float diff = abs(status->m_boilerSetpoint - self->m_results[0].m_float);
|
||||||
|
// Ignore small changes in the boiler setpoint temperature
|
||||||
|
if (diff < OPENTHERM_BOILER_SETPOINT_TOLERANCE)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
AddLog_P2(LOG_LEVEL_INFO,
|
||||||
|
PSTR("[OTH]: Setting Boiler Temp. Old: %d, New: %d"),
|
||||||
|
(int)self->m_results[0].m_float,
|
||||||
|
(int)status->m_boilerSetpoint);
|
||||||
|
self->m_results[0].m_float = status->m_boilerSetpoint;
|
||||||
|
|
||||||
|
unsigned int data = OpenTherm::temperatureToData(status->m_boilerSetpoint);
|
||||||
|
return OpenTherm::buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data);
|
||||||
|
}
|
||||||
|
void sns_opentherm_parse_set_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[1].m_float = OpenTherm::getFloat(response);
|
||||||
|
}
|
||||||
|
void sns_opentherm_tele_boiler_temperature(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
char requested[FLOATSZ];
|
||||||
|
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, requested);
|
||||||
|
char actual[FLOATSZ];
|
||||||
|
dtostrfd(self->m_results[1].m_float, Settings.flag2.temperature_resolution, actual);
|
||||||
|
|
||||||
|
// indicate fault if tepmerature demand and actual setpoint are greater then tolerance
|
||||||
|
bool isFault = abs(self->m_results[1].m_float - self->m_results[0].m_float) > OPENTHERM_BOILER_SETPOINT_TOLERANCE;
|
||||||
|
|
||||||
|
ResponseAppend_P(PSTR("{\"FAULT\":%d,\"REQ\":%s,\"ACT\": %s}"),
|
||||||
|
(int)isFault,
|
||||||
|
requested,
|
||||||
|
actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// Set Domestic Hot Water Temperature //////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_set_boiler_dhw_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *status)
|
||||||
|
{
|
||||||
|
// The same consideration as for the boiler temperature
|
||||||
|
float diff = abs(status->m_hotWaterSetpoint - self->m_results[0].m_float);
|
||||||
|
// Ignore small changes in the boiler setpoint temperature
|
||||||
|
if (diff < OPENTHERM_BOILER_SETPOINT_TOLERANCE)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
AddLog_P2(LOG_LEVEL_INFO,
|
||||||
|
PSTR("[OTH]: Setting Hot Water Temp. Old: %d, New: %d"),
|
||||||
|
(int)self->m_results[0].m_float,
|
||||||
|
(int)status->m_hotWaterSetpoint);
|
||||||
|
|
||||||
|
self->m_results[0].m_float = status->m_hotWaterSetpoint;
|
||||||
|
|
||||||
|
unsigned int data = OpenTherm::temperatureToData(status->m_hotWaterSetpoint);
|
||||||
|
return OpenTherm::buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data);
|
||||||
|
}
|
||||||
|
void sns_opentherm_parse_boiler_dhw_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[1].m_float = OpenTherm::getFloat(response);
|
||||||
|
}
|
||||||
|
void sns_opentherm_tele_boiler_dhw_temperature(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
char requested[FLOATSZ];
|
||||||
|
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, requested);
|
||||||
|
char actual[FLOATSZ];
|
||||||
|
dtostrfd(self->m_results[1].m_float, Settings.flag2.temperature_resolution, actual);
|
||||||
|
|
||||||
|
ResponseAppend_P(PSTR("{\"REQ\":%s,\"ACT\": %s}"),
|
||||||
|
requested,
|
||||||
|
actual);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// App Specific Fault Flags //////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_get_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
|
||||||
|
{
|
||||||
|
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_flags(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
uint8_t fault_code = (response >> 8) & 0xFF;
|
||||||
|
uint8_t oem_fault_code = response & 0xFF;
|
||||||
|
boilerStatus->m_fault_code = fault_code;
|
||||||
|
boilerStatus->m_oem_fault_code = fault_code;
|
||||||
|
self->m_results[0].m_u8 = fault_code;
|
||||||
|
self->m_results[1].m_u8 = oem_fault_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_tele_flags(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
ResponseAppend_P(PSTR("{\"FC\":%d,\"OFC\":%d}"),
|
||||||
|
(int)self->m_results[0].m_u8,
|
||||||
|
(int)self->m_results[1].m_u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// OEM Diag Code //////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_get_oem_diag(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
|
||||||
|
{
|
||||||
|
return OpenTherm::buildRequest(OpenThermRequestType::READ, OpenThermMessageID::OEMDiagnosticCode, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_oem_diag(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
uint16_t diag_code = (uint16_t)response & 0xFFFF;
|
||||||
|
boilerStatus->m_oem_diag_code = diag_code;
|
||||||
|
self->m_results[0].m_u16 = diag_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_tele_oem_diag(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
ResponseAppend_P(PSTR("%d"), (int)self->m_results[0].m_u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// Generic Single Float /////////////////////////////////////////////////
|
||||||
|
unsigned long sns_opentherm_get_generic_float(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *)
|
||||||
|
{
|
||||||
|
return OpenTherm::buildRequest(OpenThermRequestType::READ, (OpenThermMessageID)self->m_command_code, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_generic_float(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[0].m_float = OpenTherm::getFloat(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_tele_generic_float(struct OpenThermCommandT *self)
|
||||||
|
{
|
||||||
|
char str[FLOATSZ];
|
||||||
|
dtostrfd(self->m_results[0].m_float, Settings.flag2.temperature_resolution, str);
|
||||||
|
ResponseAppend_P(PSTR("%s"), str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////// Specific Floats Rerports to the /////////////////////////////////////////////////
|
||||||
|
void sns_opentherm_parse_dhw_setpoint(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[0].m_float = OpenTherm::getFloat(response);
|
||||||
|
boilerStatus->m_hotWaterSetpoint_read = self->m_results[0].m_float;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_flame_modulation(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[0].m_float = OpenTherm::getFloat(response);
|
||||||
|
boilerStatus->m_flame_modulation_read = self->m_results[0].m_float;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_parse_boiler_temperature(struct OpenThermCommandT *self, struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
self->m_results[0].m_float = OpenTherm::getFloat(response);
|
||||||
|
boilerStatus->m_boiler_temperature_read = self->m_results[0].m_float;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#define SNS_OT_COMMANDS_COUNT (sizeof(sns_opentherm_commands) / sizeof(OpenThermCommand))
|
||||||
|
int sns_opentherm_current_command = SNS_OT_COMMANDS_COUNT;
|
||||||
|
|
||||||
|
unsigned long sns_opentherm_get_next_request(struct OT_BOILER_STATUS_T *boilerStatus)
|
||||||
|
{
|
||||||
|
// get next and loop the command
|
||||||
|
if (++sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
|
||||||
|
{
|
||||||
|
sns_opentherm_current_command = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
|
||||||
|
// Return error if command known as not supported
|
||||||
|
if (cmd->m_flags.notSupported)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Retrurn OT compatible request
|
||||||
|
return cmd->m_ot_make_request(cmd, boilerStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_check_retry_request()
|
||||||
|
{
|
||||||
|
if (sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
|
||||||
|
|
||||||
|
bool canRetry = ++cmd->m_flags.retryCount < 3;
|
||||||
|
// In case of last retry and if this command never respond successfully, set notSupported flag
|
||||||
|
if (!canRetry && !cmd->m_flags.supported)
|
||||||
|
{
|
||||||
|
cmd->m_flags.notSupported = true;
|
||||||
|
AddLog_P2(LOG_LEVEL_ERROR,
|
||||||
|
PSTR("[OTH]: command %s is not supported by the boiler. Last status: %s"),
|
||||||
|
cmd->m_command_name,
|
||||||
|
sns_ot_master->statusToString(sns_ot_master->getLastResponseStatus()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_process_success_response(struct OT_BOILER_STATUS_T *boilerStatus, unsigned long response)
|
||||||
|
{
|
||||||
|
if (sns_opentherm_current_command >= SNS_OT_COMMANDS_COUNT)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
struct OpenThermCommandT *cmd = &sns_opentherm_commands[sns_opentherm_current_command];
|
||||||
|
// mark command as supported
|
||||||
|
cmd->m_flags.supported = true;
|
||||||
|
|
||||||
|
cmd->m_ot_parse_response(cmd, boilerStatus, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sns_opentherm_dump_telemetry()
|
||||||
|
{
|
||||||
|
bool add_coma = false;
|
||||||
|
for (int i = 0; i < SNS_OT_COMMANDS_COUNT; ++i)
|
||||||
|
{
|
||||||
|
struct OpenThermCommandT *cmd = &sns_opentherm_commands[i];
|
||||||
|
if (!cmd->m_flags.supported)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseAppend_P(PSTR("%s\"%s\":"), add_coma ? "," : "", cmd->m_command_name);
|
||||||
|
|
||||||
|
cmd->m_ot_appent_telemetry(cmd);
|
||||||
|
|
||||||
|
add_coma = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
Loading…
Reference in New Issue