diff --git a/tasmota/OpenTherm.cpp b/tasmota/OpenTherm.cpp new file mode 100644 index 000000000..a08608649 --- /dev/null +++ b/tasmota/OpenTherm.cpp @@ -0,0 +1,410 @@ +/* +OpenTherm.cpp - OpenTherm Communication Library For Arduino, ESP8266 +Copyright 2018, Ihor Melnyk +*/ + +#include "OpenTherm.h" + +OpenTherm::OpenTherm(int inPin, int outPin, bool isSlave): + status(OpenThermStatus::NOT_INITIALIZED), + inPin(inPin), + outPin(outPin), + isSlave(isSlave), + response(0), + responseStatus(OpenThermResponseStatus::NONE), + responseTimestamp(0), + handleInterruptCallback(NULL), + processResponseCallback(NULL) +{ +} + +void OpenTherm::begin(void(*handleInterruptCallback)(void), void(*processResponseCallback)(unsigned long, int)) +{ + pinMode(inPin, INPUT); + pinMode(outPin, OUTPUT); + if (handleInterruptCallback != NULL) { + this->handleInterruptCallback = handleInterruptCallback; + attachInterrupt(digitalPinToInterrupt(inPin), handleInterruptCallback, CHANGE); + } + activateBoiler(); + status = OpenThermStatus::READY; + this->processResponseCallback = processResponseCallback; +} + +void OpenTherm::begin(void(*handleInterruptCallback)(void)) +{ + begin(handleInterruptCallback, NULL); +} + +bool ICACHE_RAM_ATTR OpenTherm::isReady() +{ + return status == OpenThermStatus::READY; +} + +int ICACHE_RAM_ATTR OpenTherm::readState() { + return digitalRead(inPin); +} + +void OpenTherm::setActiveState() { + digitalWrite(outPin, LOW); +} + +void OpenTherm::setIdleState() { + digitalWrite(outPin, HIGH); +} + +void OpenTherm::activateBoiler() { + setIdleState(); + delay(1000); +} + +void OpenTherm::sendBit(bool high) { + if (high) setActiveState(); else setIdleState(); + delayMicroseconds(500); + if (high) setIdleState(); else setActiveState(); + delayMicroseconds(500); +} + +bool OpenTherm::sendRequestAync(unsigned long request) +{ + //Serial.println("Request: " + String(request, HEX)); + noInterrupts(); + const bool ready = isReady(); + interrupts(); + + if (!ready) + return false; + + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; + + sendBit(HIGH); //start bit + for (int i = 31; i >= 0; i--) { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); //stop bit + setIdleState(); + + status = OpenThermStatus::RESPONSE_WAITING; + responseTimestamp = micros(); + return true; +} + +unsigned long OpenTherm::sendRequest(unsigned long request) +{ + if (!sendRequestAync(request)) return 0; + while (!isReady()) { + process(); + yield(); + } + return response; +} + +bool OpenTherm::sendResponse(unsigned long request) +{ + status = OpenThermStatus::REQUEST_SENDING; + response = 0; + responseStatus = OpenThermResponseStatus::NONE; + + sendBit(HIGH); //start bit + for (int i = 31; i >= 0; i--) { + sendBit(bitRead(request, i)); + } + sendBit(HIGH); //stop bit + setIdleState(); + status = OpenThermStatus::READY; + return true; +} + +OpenThermResponseStatus OpenTherm::getLastResponseStatus() +{ + return responseStatus; +} + +void ICACHE_RAM_ATTR OpenTherm::handleInterrupt() +{ + if (isReady()) + { + if (isSlave && readState() == HIGH) { + status = OpenThermStatus::RESPONSE_WAITING; + } + else { + return; + } + } + + unsigned long newTs = micros(); + if (status == OpenThermStatus::RESPONSE_WAITING) { + if (readState() == HIGH) { + status = OpenThermStatus::RESPONSE_START_BIT; + responseTimestamp = newTs; + } + else { + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_START_BIT) { + if ((newTs - responseTimestamp < 750) && readState() == LOW) { + status = OpenThermStatus::RESPONSE_RECEIVING; + responseTimestamp = newTs; + responseBitIndex = 0; + } + else { + status = OpenThermStatus::RESPONSE_INVALID; + responseTimestamp = newTs; + } + } + else if (status == OpenThermStatus::RESPONSE_RECEIVING) { + if ((newTs - responseTimestamp) > 750) { + if (responseBitIndex < 32) { + response = (response << 1) | !readState(); + responseTimestamp = newTs; + responseBitIndex++; + } + else { //stop bit + status = OpenThermStatus::RESPONSE_READY; + responseTimestamp = newTs; + } + } + } +} + +void OpenTherm::process() +{ + noInterrupts(); + OpenThermStatus st = status; + unsigned long ts = responseTimestamp; + interrupts(); + + if (st == OpenThermStatus::READY) return; + unsigned long newTs = micros(); + if (st != OpenThermStatus::NOT_INITIALIZED && (newTs - ts) > 1000000) { + status = OpenThermStatus::READY; + responseStatus = OpenThermResponseStatus::TIMEOUT; + if (processResponseCallback != NULL) { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_INVALID) { + status = OpenThermStatus::DELAY; + responseStatus = OpenThermResponseStatus::INVALID; + if (processResponseCallback != NULL) { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::RESPONSE_READY) { + status = OpenThermStatus::DELAY; + responseStatus = (isSlave ? isValidRequest(response) : isValidResponse(response)) ? OpenThermResponseStatus::SUCCESS : OpenThermResponseStatus::INVALID; + if (processResponseCallback != NULL) { + processResponseCallback(response, responseStatus); + } + } + else if (st == OpenThermStatus::DELAY) { + if ((newTs - ts) > 100000) { + status = OpenThermStatus::READY; + } + } +} + +bool OpenTherm::parity(unsigned long frame) //odd parity +{ + byte p = 0; + while (frame > 0) + { + if (frame & 1) p++; + frame = frame >> 1; + } + return (p & 1); +} + +OpenThermMessageType OpenTherm::getMessageType(unsigned long message) +{ + OpenThermMessageType msg_type = static_cast((message >> 28) & 7); + return msg_type; +} + +OpenThermMessageID OpenTherm::getDataID(unsigned long frame) +{ + return (OpenThermMessageID)((frame >> 16) & 0xFF); +} + +unsigned long OpenTherm::buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long request = data; + if (type == OpenThermMessageType::WRITE_DATA) { + request |= 1ul << 28; + } + request |= ((unsigned long)id) << 16; + if (OpenTherm::parity(request)) request |= (1ul << 31); + return request; +} + +unsigned long OpenTherm::buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data) +{ + unsigned long response = data; + response |= type << 28; + response |= ((unsigned long)id) << 16; + if (OpenTherm::parity(response)) response |= (1ul << 31); + return response; +} + +bool OpenTherm::isValidResponse(unsigned long response) +{ + if (OpenTherm::parity(response)) return false; + byte msgType = (response << 1) >> 29; + return msgType == READ_ACK || msgType == WRITE_ACK; +} + +bool OpenTherm::isValidRequest(unsigned long request) +{ + if (OpenTherm::parity(request)) return false; + byte msgType = (request << 1) >> 29; + return msgType == READ_DATA || msgType == WRITE_DATA; +} + +void OpenTherm::end() { + if (this->handleInterruptCallback != NULL) { + detachInterrupt(digitalPinToInterrupt(inPin)); + } +} + +const char *OpenTherm::statusToString(OpenThermResponseStatus status) +{ + switch (status) { + case NONE: return "NONE"; + case SUCCESS: return "SUCCESS"; + case INVALID: return "INVALID"; + case TIMEOUT: return "TIMEOUT"; + default: return "UNKNOWN"; + } +} + +const char *OpenTherm::messageTypeToString(OpenThermMessageType message_type) +{ + switch (message_type) { + case READ_DATA: return "READ_DATA"; + case WRITE_DATA: return "WRITE_DATA"; + case INVALID_DATA: return "INVALID_DATA"; + case RESERVED: return "RESERVED"; + case READ_ACK: return "READ_ACK"; + case WRITE_ACK: return "WRITE_ACK"; + case DATA_INVALID: return "DATA_INVALID"; + case UNKNOWN_DATA_ID: return "UNKNOWN_DATA_ID"; + default: return "UNKNOWN"; + } +} + +//building requests + +unsigned long OpenTherm::buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2) { + unsigned int data = enableCentralHeating | (enableHotWater << 1) | (enableCooling << 2) | (enableOutsideTemperatureCompensation << 3) | (enableCentralHeating2 << 4); + data <<= 8; + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Status, data); +} + +unsigned long OpenTherm::buildSetBoilerTemperatureRequest(float temperature) { + unsigned int data = temperatureToData(temperature); + return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TSet, data); +} + +unsigned long OpenTherm::buildSetHotWaterTemperatureRequest(float temperature) { + unsigned int data = temperatureToData(temperature); + return buildRequest(OpenThermMessageType::WRITE_DATA, OpenThermMessageID::TdhwSet, data); +} + +unsigned long OpenTherm::buildGetBoilerTemperatureRequest() { + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::Tboiler, 0); +} + +unsigned long OpenTherm::buildSlaveConfigurationRequest() { + return buildRequest(OpenThermMessageType::READ_DATA, OpenThermMessageID::SConfigSMemberIDcode, 0); +} + +//parsing responses +bool OpenTherm::isFault(unsigned long response) { + return response & 0x1; +} + +bool OpenTherm::isCentralHeatingActive(unsigned long response) { + return response & 0x2; +} + +bool OpenTherm::isHotWaterActive(unsigned long response) { + return response & 0x4; +} + +bool OpenTherm::isFlameOn(unsigned long response) { + return response & 0x8; +} + +bool OpenTherm::isCoolingActive(unsigned long response) { + return response & 0x10; +} + +bool OpenTherm::isDiagnostic(unsigned long response) { + return response & 0x40; +} + +uint16_t OpenTherm::getUInt(const unsigned long response) { + const uint16_t u88 = response & 0xffff; + return u88; +} + +float OpenTherm::getFloat(const unsigned long response) { + const uint16_t u88 = getUInt(response); + const float f = (u88 & 0x8000) ? -(0x10000L - u88) / 256.0f : u88 / 256.0f; + return f; +} + +unsigned int OpenTherm::temperatureToData(float temperature) { + if (temperature < 0) temperature = 0; + if (temperature > 100) temperature = 100; + unsigned int data = (unsigned int)(temperature * 256); + return data; +} + +//basic requests + +unsigned long OpenTherm::setBoilerStatus(bool enableCentralHeating, bool enableHotWater, bool enableCooling, bool enableOutsideTemperatureCompensation, bool enableCentralHeating2) { + return sendRequest(buildSetBoilerStatusRequest(enableCentralHeating, enableHotWater, enableCooling, enableOutsideTemperatureCompensation, enableCentralHeating2)); +} + +bool OpenTherm::setBoilerTemperature(float temperature) { + unsigned long response = sendRequest(buildSetBoilerTemperatureRequest(temperature)); + return isValidResponse(response); +} + +bool OpenTherm::setHotWaterTemperature(float temperature) { + unsigned long response = sendRequest(buildSetHotWaterTemperatureRequest(temperature)); + return isValidResponse(response); +} + +float OpenTherm::getBoilerTemperature() { + unsigned long response = sendRequest(buildGetBoilerTemperatureRequest()); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getReturnTemperature() { + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::Tret, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getModulation() { + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::RelModLevel, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +float OpenTherm::getPressure() { + unsigned long response = sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::CHPressure, 0)); + return isValidResponse(response) ? getFloat(response) : 0; +} + +unsigned char OpenTherm::getFault() { + return ((sendRequest(buildRequest(OpenThermRequestType::READ, OpenThermMessageID::ASFflags, 0)) >> 8) & 0xff); +} + +unsigned long OpenTherm::getSlaveConfiguration() { + return sendRequest(buildSlaveConfigurationRequest()); +} \ No newline at end of file diff --git a/tasmota/OpenTherm.h b/tasmota/OpenTherm.h new file mode 100644 index 000000000..22bf965ad --- /dev/null +++ b/tasmota/OpenTherm.h @@ -0,0 +1,193 @@ +/* +OpenTherm.h - OpenTherm Library for the ESP8266/Arduino platform +https://github.com/ihormelnyk/OpenTherm +http://ihormelnyk.com/pages/OpenTherm +Licensed under MIT license +Copyright 2018, Ihor Melnyk + +Frame Structure: +P MGS-TYPE SPARE DATA-ID DATA-VALUE +0 000 0000 00000000 00000000 00000000 +*/ + +#ifndef OpenTherm_h +#define OpenTherm_h + +#include +#include + +enum OpenThermResponseStatus { + NONE, + SUCCESS, + INVALID, + TIMEOUT +}; + + +enum OpenThermMessageType { + /* Master to Slave */ + READ_DATA = B000, + READ = READ_DATA, // for backwared compatibility + WRITE_DATA = B001, + WRITE = WRITE_DATA, // for backwared compatibility + INVALID_DATA = B010, + RESERVED = B011, + /* Slave to Master */ + READ_ACK = B100, + WRITE_ACK = B101, + DATA_INVALID = B110, + UNKNOWN_DATA_ID = B111 +}; + +typedef OpenThermMessageType OpenThermRequestType; // for backwared compatibility + +enum OpenThermMessageID { + Status, // flag8 / flag8 Master and Slave Status flags. + TSet, // f8.8 Control setpoint ie CH water temperature setpoint (°C) + MConfigMMemberIDcode, // flag8 / u8 Master Configuration Flags / Master MemberID Code + SConfigSMemberIDcode, // flag8 / u8 Slave Configuration Flags / Slave MemberID Code + Command, // u8 / u8 Remote Command + ASFflags, // / OEM-fault-code flag8 / u8 Application-specific fault flags and OEM fault code + RBPflags, // flag8 / flag8 Remote boiler parameter transfer-enable & read/write flags + CoolingControl, // f8.8 Cooling control signal (%) + TsetCH2, // f8.8 Control setpoint for 2e CH circuit (°C) + TrOverride, // f8.8 Remote override room setpoint + TSP, // u8 / u8 Number of Transparent-Slave-Parameters supported by slave + TSPindexTSPvalue, // u8 / u8 Index number / Value of referred-to transparent slave parameter. + FHBsize, // u8 / u8 Size of Fault-History-Buffer supported by slave + FHBindexFHBvalue, // u8 / u8 Index number / Value of referred-to fault-history buffer entry. + MaxRelModLevelSetting, // f8.8 Maximum relative modulation level setting (%) + MaxCapacityMinModLevel, // u8 / u8 Maximum boiler capacity (kW) / Minimum boiler modulation level(%) + TrSet, // f8.8 Room Setpoint (°C) + RelModLevel, // f8.8 Relative Modulation Level (%) + CHPressure, // f8.8 Water pressure in CH circuit (bar) + DHWFlowRate, // f8.8 Water flow rate in DHW circuit. (litres/minute) + DayTime, // special / u8 Day of Week and Time of Day + Date, // u8 / u8 Calendar date + Year, // u16 Calendar year + TrSetCH2, // f8.8 Room Setpoint for 2nd CH circuit (°C) + Tr, // f8.8 Room temperature (°C) + Tboiler, // f8.8 Boiler flow water temperature (°C) + Tdhw, // f8.8 DHW temperature (°C) + Toutside, // f8.8 Outside temperature (°C) + Tret, // f8.8 Return water temperature (°C) + Tstorage, // f8.8 Solar storage temperature (°C) + Tcollector, // f8.8 Solar collector temperature (°C) + TflowCH2, // f8.8 Flow water temperature CH2 circuit (°C) + Tdhw2, // f8.8 Domestic hot water temperature 2 (°C) + Texhaust, // s16 Boiler exhaust temperature (°C) + TdhwSetUBTdhwSetLB = 48, // s8 / s8 DHW setpoint upper & lower bounds for adjustment (°C) + MaxTSetUBMaxTSetLB, // s8 / s8 Max CH water setpoint upper & lower bounds for adjustment (°C) + HcratioUBHcratioLB, // s8 / s8 OTC heat curve ratio upper & lower bounds for adjustment + TdhwSet = 56, // f8.8 DHW setpoint (°C) (Remote parameter 1) + MaxTSet, // f8.8 Max CH water setpoint (°C) (Remote parameters 2) + Hcratio, // f8.8 OTC heat curve ratio (°C) (Remote parameter 3) + RemoteOverrideFunction = 100, // flag8 / - Function of manual and program changes in master and remote room setpoint. + OEMDiagnosticCode = 115, // u16 OEM-specific diagnostic/service code + BurnerStarts, // u16 Number of starts burner + CHPumpStarts, // u16 Number of starts CH pump + DHWPumpValveStarts, // u16 Number of starts DHW pump/valve + DHWBurnerStarts, // u16 Number of starts burner during DHW mode + BurnerOperationHours, // u16 Number of hours that burner is in operation (i.e. flame on) + CHPumpOperationHours, // u16 Number of hours that CH pump has been running + DHWPumpValveOperationHours, // u16 Number of hours that DHW pump has been running or DHW valve has been opened + DHWBurnerOperationHours, // u16 Number of hours that burner is in operation during DHW mode + OpenThermVersionMaster, // f8.8 The implemented version of the OpenTherm Protocol Specification in the master. + OpenThermVersionSlave, // f8.8 The implemented version of the OpenTherm Protocol Specification in the slave. + MasterVersion, // u8 / u8 Master product version number and type + SlaveVersion, // u8 / u8 Slave product version number and type +}; + +enum OpenThermStatus { + NOT_INITIALIZED, + READY, + DELAY, + REQUEST_SENDING, + RESPONSE_WAITING, + RESPONSE_START_BIT, + RESPONSE_RECEIVING, + RESPONSE_READY, + RESPONSE_INVALID +}; + +class OpenTherm +{ +public: + OpenTherm(int inPin = 4, int outPin = 5, bool isSlave = false); + volatile OpenThermStatus status; + void begin(void(*handleInterruptCallback)(void)); + void begin(void(*handleInterruptCallback)(void), void(*processResponseCallback)(unsigned long, int)); + bool isReady(); + unsigned long sendRequest(unsigned long request); + bool sendResponse(unsigned long request); + bool sendRequestAync(unsigned long request); + static unsigned long buildRequest(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + static unsigned long buildResponse(OpenThermMessageType type, OpenThermMessageID id, unsigned int data); + OpenThermResponseStatus getLastResponseStatus(); + const char *statusToString(OpenThermResponseStatus status); + void handleInterrupt(); + void process(); + void end(); + + static bool parity(unsigned long frame); + OpenThermMessageType getMessageType(unsigned long message); + OpenThermMessageID getDataID(unsigned long frame); + const char *messageTypeToString(OpenThermMessageType message_type); + bool isValidRequest(unsigned long request); + bool isValidResponse(unsigned long response); + + //requests + unsigned long buildSetBoilerStatusRequest(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false); + unsigned long buildSetBoilerTemperatureRequest(float temperature); + unsigned long buildGetBoilerTemperatureRequest(); + unsigned long buildSetHotWaterTemperatureRequest(float temperature); + unsigned long buildSlaveConfigurationRequest(); + + + //responses + static bool isFault(unsigned long response); + static bool isCentralHeatingActive(unsigned long response); + static bool isHotWaterActive(unsigned long response); + static bool isFlameOn(unsigned long response); + static bool isCoolingActive(unsigned long response); + static bool isDiagnostic(unsigned long response); + static uint16_t getUInt(const unsigned long response); + static float getFloat(const unsigned long response); + static unsigned int temperatureToData(float temperature); + + //basic requests + unsigned long setBoilerStatus(bool enableCentralHeating, bool enableHotWater = false, bool enableCooling = false, bool enableOutsideTemperatureCompensation = false, bool enableCentralHeating2 = false); + bool setBoilerTemperature(float temperature); + bool setHotWaterTemperature(float temperature); + float getBoilerTemperature(); + float getReturnTemperature(); + float getModulation(); + float getPressure(); + unsigned char getFault(); + unsigned long getSlaveConfiguration(); + +private: + const int inPin; + const int outPin; + const bool isSlave; + + volatile unsigned long response; + volatile OpenThermResponseStatus responseStatus; + volatile unsigned long responseTimestamp; + volatile byte responseBitIndex; + + int readState(); + void setActiveState(); + void setIdleState(); + void activateBoiler(); + + void sendBit(bool high); + void(*handleInterruptCallback)(); + void(*processResponseCallback)(unsigned long, int); +}; + +#ifndef ICACHE_RAM_ATTR +#define ICACHE_RAM_ATTR +#endif + +#endif // OpenTherm_h \ No newline at end of file diff --git a/tasmota/language/bg_BG.h b/tasmota/language/bg_BG.h index 4919563f9..14bf6f698 100644 --- a/tasmota/language/bg_BG.h +++ b/tasmota/language/bg_BG.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Частици" +// xsns_27_apds9960.ino +#define D_GESTURE "Жест" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Грийн" +#define D_COLOR_BLUE "син" +#define D_CCT "CCT" +#define D_PROXIMITY "близост" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Ускорение - ос X" #define D_AY_AXIS "Ускорение - ос Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_BG_BG_H_ diff --git a/tasmota/language/cs_CZ.h b/tasmota/language/cs_CZ.h index 8f2005276..6da814036 100644 --- a/tasmota/language/cs_CZ.h +++ b/tasmota/language/cs_CZ.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "částic" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesto" +#define D_COLOR_RED "Červená" +#define D_COLOR_GREEN "Zelená" +#define D_COLOR_BLUE "Modrá" +#define D_CCT "CCT" +#define D_PROXIMITY "Blízkost" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. osa-X" #define D_AY_AXIS "Accel. osa-Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_CS_CZ_H_ diff --git a/tasmota/language/de_DE.h b/tasmota/language/de_DE.h index dbe0797fe..63d38dc58 100644 --- a/tasmota/language/de_DE.h +++ b/tasmota/language/de_DE.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Partikel" +// xsns_27_apds9960.ino +#define D_GESTURE "Geste" +#define D_COLOR_RED "Rot" +#define D_COLOR_GREEN "Grün" +#define D_COLOR_BLUE "Blau" +#define D_CCT "CCT" +#define D_PROXIMITY "Nähe" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Beschl. X-Achse" #define D_AY_AXIS "Beschl. Y-Achse" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "Kalibrierung fehlerhaft" #define D_AS3935_CAL_OK "Cap gesetzt auf:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_DE_DE_H_ diff --git a/tasmota/language/el_GR.h b/tasmota/language/el_GR.h index e4d6508e5..d89a8c5ce 100644 --- a/tasmota/language/el_GR.h +++ b/tasmota/language/el_GR.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particals" +// xsns_27_apds9960.ino +#define D_GESTURE "Χειρονομία" +#define D_COLOR_RED "Κόκκινο" +#define D_COLOR_GREEN "Πράσινο" +#define D_COLOR_BLUE "Μπλε" +#define D_CCT "CCT" +#define D_PROXIMITY "Εγγύτητα" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_EL_GR_H_ diff --git a/tasmota/language/en_GB.h b/tasmota/language/en_GB.h index c76446a09..dcc945c80 100644 --- a/tasmota/language/en_GB.h +++ b/tasmota/language/en_GB.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particles" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -688,6 +696,7 @@ #define D_UNIT_GALLONS "gal" #define D_UNIT_GALLONS_PER_MIN "g/m" #define D_UNIT_INCREMENTS "inc" +#define D_UNIT_KELVIN "°K" #define D_UNIT_KILOMETER "km" #define D_UNIT_KILOGRAM "kg" #define D_UNIT_KILOMETER_PER_HOUR "km/h" // or "km/h" @@ -786,4 +795,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_EN_GB_H_ diff --git a/tasmota/language/es_ES.h b/tasmota/language/es_ES.h index 512f360cd..19cbf5e66 100644 --- a/tasmota/language/es_ES.h +++ b/tasmota/language/es_ES.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Partículas" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesto" +#define D_COLOR_RED "Rojo" +#define D_COLOR_GREEN "Verde" +#define D_COLOR_BLUE "Azul" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximidad" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_ES_ES_H_ diff --git a/tasmota/language/fr_FR.h b/tasmota/language/fr_FR.h index ea36e45af..44e1d1020 100644 --- a/tasmota/language/fr_FR.h +++ b/tasmota/language/fr_FR.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particules" +// xsns_27_apds9960.ino +#define D_GESTURE "Geste" +#define D_COLOR_RED "Rouge" +#define D_COLOR_GREEN "Vert" +#define D_COLOR_BLUE "Bleu" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximité" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accél. Axe-X" #define D_AY_AXIS "Accél. Axe-Y" @@ -503,7 +511,7 @@ #define D_CALIBRATE "Étalonner" #define D_CALIBRATION "Étalonnage" -//xsns_35_TX20.ino +// xsns_35_TX20.ino #define D_TX20_WIND_DIRECTION "Direction du vent" #define D_TX20_WIND_SPEED "Vitesse du vent" #define D_TX20_WIND_SPEED_MIN "Vitesse Min" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_FR_FR_H_ diff --git a/tasmota/language/he_HE.h b/tasmota/language/he_HE.h index e66b1ca3d..058789eb3 100644 --- a/tasmota/language/he_HE.h +++ b/tasmota/language/he_HE.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "חלקיקים" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_HE_HE_H_ diff --git a/tasmota/language/hu_HU.h b/tasmota/language/hu_HU.h index 4e6398c6d..cad66ebee 100644 --- a/tasmota/language/hu_HU.h +++ b/tasmota/language/hu_HU.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Részecskék" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesztus" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "közelség" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Gyorsulásm. X-tengely" #define D_AY_AXIS "Gyorsulásm. Y-tengely" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_HU_HU_H_ diff --git a/tasmota/language/it_IT.h b/tasmota/language/it_IT.h index 151d0f1bb..5c6dcae10 100644 --- a/tasmota/language/it_IT.h +++ b/tasmota/language/it_IT.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particelle" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesto" +#define D_COLOR_RED "Rosso" +#define D_COLOR_GREEN "Verde" +#define D_COLOR_BLUE "Blu" +#define D_CCT "CCT" +#define D_PROXIMITY "Vicinanza" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accelerazione asse X" #define D_AY_AXIS "Accelerazione asse Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibrazione fallita" #define D_AS3935_CAL_OK "calibrazione impostata a:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_IT_IT_H_ diff --git a/tasmota/language/ko_KO.h b/tasmota/language/ko_KO.h index 90d7d06ee..7a84823ff 100644 --- a/tasmota/language/ko_KO.h +++ b/tasmota/language/ko_KO.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "입자" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_KO_KO_H_ diff --git a/tasmota/language/nl_NL.h b/tasmota/language/nl_NL.h index 0e3f20e36..d5922cb6a 100644 --- a/tasmota/language/nl_NL.h +++ b/tasmota/language/nl_NL.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Stofdeeltjes" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Versn. X-as" #define D_AY_AXIS "Versn. Y-as" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_NL_NL_H_ diff --git a/tasmota/language/pl_PL.h b/tasmota/language/pl_PL.h index 5af9128a7..f50d22de4 100644 --- a/tasmota/language/pl_PL.h +++ b/tasmota/language/pl_PL.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Cząstki" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_PL_PL_D_H_ diff --git a/tasmota/language/pt_BR.h b/tasmota/language/pt_BR.h index cb10bd035..dab2a3a54 100644 --- a/tasmota/language/pt_BR.h +++ b/tasmota/language/pt_BR.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Partículas" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_PT_BR_H_ diff --git a/tasmota/language/pt_PT.h b/tasmota/language/pt_PT.h index 9545d0d1e..e056245c1 100644 --- a/tasmota/language/pt_PT.h +++ b/tasmota/language/pt_PT.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Partículas" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_PT_PT_H_ diff --git a/tasmota/language/ro_RO.h b/tasmota/language/ro_RO.h index 0c14cceae..83bb05575 100644 --- a/tasmota/language/ro_RO.h +++ b/tasmota/language/ro_RO.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particule" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel.Axa-X" #define D_AY_AXIS "Accel.Axa-Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_RO_RO_H_ diff --git a/tasmota/language/ru_RU.h b/tasmota/language/ru_RU.h index fe698888d..93cbf2c72 100644 --- a/tasmota/language/ru_RU.h +++ b/tasmota/language/ru_RU.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particals" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_RU_RU_H_ diff --git a/tasmota/language/sk_SK.h b/tasmota/language/sk_SK.h index 9e915b1b4..0a6415c16 100644 --- a/tasmota/language/sk_SK.h +++ b/tasmota/language/sk_SK.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "častíc" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. os-X" #define D_AY_AXIS "Accel. os-Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_SK_SK_H_ diff --git a/tasmota/language/sv_SE.h b/tasmota/language/sv_SE.h index 02a965e5a..438bcd3c1 100644 --- a/tasmota/language/sv_SE.h +++ b/tasmota/language/sv_SE.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Partiklar" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axel" #define D_AY_AXIS "Accel. Y-Axel" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_SV_SE_H_ diff --git a/tasmota/language/tr_TR.h b/tasmota/language/tr_TR.h index 675b67a05..740b92774 100644 --- a/tasmota/language/tr_TR.h +++ b/tasmota/language/tr_TR.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Particals" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_TR_TR_H_ diff --git a/tasmota/language/uk_UA.h b/tasmota/language/uk_UA.h index 334eb4b1d..bf0377e7b 100644 --- a/tasmota/language/uk_UA.h +++ b/tasmota/language/uk_UA.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "Частинки понад" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Приск. Вісь-X" #define D_AY_AXIS "Приск. Вісь-Y" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_UK_UA_H_ diff --git a/tasmota/language/zh_CN.h b/tasmota/language/zh_CN.h index 3fb418a27..49e7b8592 100644 --- a/tasmota/language/zh_CN.h +++ b/tasmota/language/zh_CN.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "颗粒物直径大于" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "加速度计X轴分量" #define D_AY_AXIS "加速度计Y轴分量" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_ZH_CN_H_ diff --git a/tasmota/language/zh_TW.h b/tasmota/language/zh_TW.h index 555e93283..7f92495a4 100644 --- a/tasmota/language/zh_TW.h +++ b/tasmota/language/zh_TW.h @@ -482,6 +482,14 @@ #define D_ENVIRONMENTAL_CONCENTRATION "PM" // Environmetal Particle Matter #define D_PARTICALS_BEYOND "顆粒物直徑大於" +// xsns_27_apds9960.ino +#define D_GESTURE "Gesture" +#define D_COLOR_RED "Red" +#define D_COLOR_GREEN "Green" +#define D_COLOR_BLUE "Blue" +#define D_CCT "CCT" +#define D_PROXIMITY "Proximity" + // xsns_32_mpu6050.ino #define D_AX_AXIS "Accel. X-Axis" #define D_AY_AXIS "Accel. Y-Axis" @@ -786,4 +794,8 @@ #define D_AS3935_CAL_FAIL "calibration failed" #define D_AS3935_CAL_OK "calibration set to:" +//xsns_68_opentherm.ino +#define D_SENSOR_BOILER_OT_RX "OpenTherm RX" +#define D_SENSOR_BOILER_OT_TX "OpenTherm TX" + #endif // _LANGUAGE_ZH_TW_H_ diff --git a/tasmota/my_user_config.h b/tasmota/my_user_config.h index 42b9fb6a2..1fdfbda8f 100644 --- a/tasmota/my_user_config.h +++ b/tasmota/my_user_config.h @@ -478,6 +478,10 @@ // #define USE_SI1145 // [I2cDriver19] Enable SI1145/46/47 sensor (I2C address 0x60) (+1k code) // #define USE_LM75AD // [I2cDriver20] Enable LM75AD sensor (I2C addresses 0x48 - 0x4F) (+0k5 code) // #define USE_APDS9960 // [I2cDriver21] Enable APDS9960 Proximity Sensor (I2C address 0x39). Disables SHT and VEML6070 (+4k7 code) + #define USE_APDS9960_GESTURE // Enable APDS9960 Gesture feature (+2k code) + #define USE_APDS9960_PROXIMITY // Enable APDS9960 Proximity feature (>50 code) + #define USE_APDS9960_COLOR // Enable APDS9960 Color feature (+0.8k code) + #define USE_APDS9960_STARTMODE 0 // Default to enable Gesture mode // #define USE_MCP230xx // [I2cDriver22] Enable MCP23008/MCP23017 - Must define I2C Address in #define USE_MCP230xx_ADDR below - range 0x20 - 0x27 (+4k7 code) // #define USE_MCP230xx_ADDR 0x20 // Enable MCP23008/MCP23017 I2C Address to use (Must be within range 0x20 through 0x26 - set according to your wired setup) // #define USE_MCP230xx_OUTPUT // Enable MCP23008/MCP23017 OUTPUT support through sensor29 commands (+1k5 code) @@ -664,6 +668,8 @@ //#define USE_WINDMETER // Add support for analog anemometer +//#define USE_OPENTHERM // Use OpenTherm implementation + // -- End of general directives ------------------- /*********************************************************************************************\ diff --git a/tasmota/settings.h b/tasmota/settings.h index 273bb4c35..b857d1899 100644 --- a/tasmota/settings.h +++ b/tasmota/settings.h @@ -482,7 +482,10 @@ struct { uint8_t shutter_startrelay[MAX_SHUTTERS]; // E84 uint8_t pcf8574_config[MAX_PCF8574]; // E88 - uint8_t free_e8c[4]; // E8C + uint8_t ot_hot_water_setpoint; // E8C + uint8_t ot_boiler_setpoint; // E8D + uint8_t ot_flags; // E8E + uint8_t free_e8f[1]; // E8F uint16_t dimmer_hw_min; // E90 uint16_t dimmer_hw_max; // E92 diff --git a/tasmota/support_features.ino b/tasmota/support_features.ino index fcaedf423..56f8ea557 100644 --- a/tasmota/support_features.ino +++ b/tasmota/support_features.ino @@ -554,12 +554,13 @@ void GetFeatures(void) #ifdef USE_PING feature6 |= 0x00000080; // xdrv_38_ping.ino #endif +#ifdef USE_THERMOSTAT + feature6 |= 0x00000100; // xsns_68_opentherm.ino +#endif #ifdef USE_WINDMETER - feature6 |= 0x00000100; // xsns_68_windmeter.ino + feature6 |= 0x00000200; // xsns_69_windmeter.ino #endif -// feature6 |= 0x00000100; -// feature6 |= 0x00000200; // feature6 |= 0x00000400; // feature6 |= 0x00000800; diff --git a/tasmota/tasmota_template.h b/tasmota/tasmota_template.h index 09d6a5bd8..cb40e6710 100644 --- a/tasmota/tasmota_template.h +++ b/tasmota/tasmota_template.h @@ -230,6 +230,8 @@ enum UserSelectablePins { GPIO_ELECTRIQ_MOODL_TX, // ElectriQ iQ-wifiMOODL Serial TX GPIO_AS3935, GPIO_PMS5003_TX, // Plantower PMS5003 Serial interface + GPIO_BOILER_OT_RX, // OpenTherm Boiler RX pin + GPIO_BOILER_OT_TX, // OpenTherm Boiler TX pin GPIO_WINDMETER_SPEED, // WindMeter speed counter pin GPIO_SENSOR_END }; @@ -319,6 +321,7 @@ const char kSensorNames[] PROGMEM = D_SENSOR_HRXL_RX "|" D_SENSOR_ELECTRIQ_MOODL "|" D_SENSOR_AS3935 "|" D_SENSOR_PMS5003_TX "|" + D_SENSOR_BOILER_OT_RX "|" D_SENSOR_BOILER_OT_TX "|" D_SENSOR_WINDMETER_SPEED ; @@ -593,6 +596,9 @@ const uint8_t kGpioNiceList[] PROGMEM = { #if defined(USE_TX20_WIND_SENSOR) || defined(USE_TX23_WIND_SENSOR) GPIO_TX2X_TXD_BLACK, // TX20/TX23 Transmission Pin #endif +#ifdef USE_WINDMETER + GPIO_WINDMETER_SPEED, +#endif #ifdef USE_MP3_PLAYER GPIO_MP3_DFR562, // RB-DFR-562, DFPlayer Mini MP3 Player Serial interface #endif @@ -667,8 +673,9 @@ const uint8_t kGpioNiceList[] PROGMEM = { #ifdef USE_AS3935 GPIO_AS3935, #endif -#ifdef USE_WINDMETER - GPIO_WINDMETER_SPEED, +#ifdef USE_OPENTHERM + GPIO_BOILER_OT_RX, + GPIO_BOILER_OT_TX, #endif }; diff --git a/tasmota/tasmota_template_ESP32.h b/tasmota/tasmota_template_ESP32.h index 4aaa39ae3..874a77b78 100644 --- a/tasmota/tasmota_template_ESP32.h +++ b/tasmota/tasmota_template_ESP32.h @@ -125,6 +125,8 @@ enum UserSelectablePins { GPIO_WEBCAM_PSCLK, GPIO_WEBCAM_HSD, GPIO_WEBCAM_PSRCS, + GPIO_BOILER_OT_RX, // OpenTherm Boiler RX pin + GPIO_BOILER_OT_TX, // OpenTherm Boiler TX pin GPIO_SENSOR_END }; enum ProgramSelectablePins { @@ -211,7 +213,8 @@ const char kSensorNames[] PROGMEM = D_GPIO_WEBCAM_VSYNC "|" D_GPIO_WEBCAM_HREF "|" D_GPIO_WEBCAM_PCLK "|" D_GPIO_WEBCAM_PSCLK "|" D_GPIO_WEBCAM_HSD "|" - D_GPIO_WEBCAM_PSRCS + D_GPIO_WEBCAM_PSRCS "|" + D_SENSOR_BOILER_OT_RX "|" D_SENSOR_BOILER_OT_TX ; const char kSensorNamesFixed[] PROGMEM = @@ -548,6 +551,10 @@ const uint16_t kGpioNiceList[] PROGMEM = { AGPIO(GPIO_WEBCAM_PSRCS), #endif +#ifdef USE_OPENTHERM + AGPIO(GPIO_BOILER_OT_RX), + AGPIO(GPIO_BOILER_OT_TX), +#endif }; //******************************************************************************************** diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino index 5040d38cb..d76954715 100755 --- a/tasmota/xdrv_10_scripter.ino +++ b/tasmota/xdrv_10_scripter.ino @@ -1181,6 +1181,12 @@ chknext: } } } +#ifdef ESP32 + if (!strncmp(vname,"core",4)) { + fvar=xPortGetCoreID(); + goto exit; + } +#endif break; case 'd': if (!strncmp(vname,"day",3)) { @@ -1705,6 +1711,12 @@ chknext: fvar=999; goto exit; } +#ifdef ESP32 + if (!strncmp(vname,"pheap",5)) { + fvar=ESP.getFreePsram(); + goto exit; + } +#endif if (!strncmp(vname,"prefix1",7)) { if (sp) strlcpy(sp,SettingsText(SET_MQTTPREFIX1),glob_script_mem.max_ssize); goto strexit; @@ -5235,7 +5247,9 @@ bool Xdrv10(uint8_t function) break; #ifdef USE_SCRIPT_WEB_DISPLAY case FUNC_WEB_ADD_MAIN_BUTTON: - ScriptWebShow('&'); + if (bitRead(Settings.rule_enabled, 0)) { + ScriptWebShow('&'); + } break; #endif // USE_SCRIPT_WEB_DISPLAY case FUNC_WEB_ADD_HANDLER: diff --git a/tasmota/xdrv_12_home_assistant.ino b/tasmota/xdrv_12_home_assistant.ino index 2a95fa591..51de71219 100644 --- a/tasmota/xdrv_12_home_assistant.ino +++ b/tasmota/xdrv_12_home_assistant.ino @@ -28,12 +28,14 @@ const char kHAssJsonSensorTypes[] PROGMEM = D_JSON_MOISTURE "|PB0.3|PB0.5|PB1|PB2.5|PB5|PB10|PM1|PM2.5|PM10|" D_JSON_POWERFACTOR "|" D_JSON_POWERUSAGE "|" D_JSON_REACTIVE_POWERUSAGE "|" D_JSON_TODAY "|" D_JSON_TOTAL "|" D_JSON_VOLTAGE "|" D_JSON_WEIGHT "|" D_JSON_YESTERDAY "|" D_JSON_CO2 "|" D_JSON_ECO2 "|" D_JSON_TVOC "|"; + const char kHAssJsonSensorUnits[] PROGMEM = "||||" "VA|%|A|Cm|Hz|%|LX|" "%|ppd|ppd|ppd|ppd|ppd|ppd|µg/m³|µg/m³|µg/m³|Cos φ|W|" "VAr|kWh|kWh|V|Kg|kWh|" "ppm|ppm|ppb|"; + const char kHAssJsonSensorDevCla[] PROGMEM = "dev_cla\":\"temperature|ic\":\"mdi:weather-rainy|dev_cla\":\"pressure|dev_cla\":\"pressure|" "dev_cla\":\"power|dev_cla\":\"battery|ic\":\"mdi:alpha-a-circle-outline|ic\":\"mdi:leak|ic\":\"mdi:current-ac|dev_cla\":\"humidity|dev_cla\":\"illuminance|" @@ -74,12 +76,12 @@ const char HASS_DISCOVER_BIN_PIR[] PROGMEM = "\"pl_on\":\"%s\"," // ON "\"off_dly\":1"; // Switchmode13 and Switchmode14 doesn't transmit an OFF state. -const char HASS_DISCOVER_LIGHT_DIMMER[] PROGMEM = +const char HASS_DISCOVER_BASE_LIGHT[] PROGMEM = ",\"bri_cmd_t\":\"%s\"," // cmnd/led2/Dimmer "\"bri_stat_t\":\"%s\"," // stat/led2/RESULT "\"bri_scl\":100," // 100% "\"on_cmd_type\":\"%s\"," // power on (first), power on (last), no power on (brightness) - "\"bri_val_tpl\":\"{{value_json." D_CMND_DIMMER "}}\""; + "\"bri_val_tpl\":\"{{value_json.%s}}\""; const char HASS_DISCOVER_LIGHT_COLOR[] PROGMEM = ",\"rgb_cmd_t\":\"%s2\"," // cmnd/led2/Color2 @@ -138,6 +140,16 @@ const char kHAssTriggerTypeButtons[] PROGMEM = const char kHAssTriggerStringButtons[] PROGMEM = "|SINGLE|DOUBLE|TRIPLE|QUAD|PENTA|HOLD|"; +const char kHAssError1[] PROGMEM = + "HASS: MQTT discovery failed due to too long topic or friendly name." + "Please shorten topic and friendly name. Failed to format"; + +const char kHAssError2[] PROGMEM = + "HASS: The configuration of the Relays is wrong, there is a Light that is using an index lower than the number of the selected relay.\n " + "The Relays have priority over the Lights, an incorrect order could lead to an erroneous Light control.\n " + "Please update your configuration to avoid inconsistent results.\n " + "Entities for Relays and Lights will not be available in Home Assistant until the configuration will be updated."; + uint8_t hass_init_step = 0; uint8_t hass_mode = 0; int hass_tele_period = 0; @@ -153,9 +165,7 @@ void TryResponseAppend_P(const char *format, ...) int slen = sizeof(mqtt_data) - 1 - mlen; if (dlen >= slen) { - AddLog_P2(LOG_LEVEL_ERROR, PSTR("HASS: MQTT discovery failed due to too long topic or friendly name. " - "Please shorten topic and friendly name. Failed to format(%u/%u):"), - dlen, slen); + AddLog_P2(LOG_LEVEL_ERROR, PSTR("%s (%u/%u):"), kHAssError1, dlen, slen); va_start(args, format); vsnprintf_P(log_data, sizeof(log_data), format, args); AddLog(LOG_LEVEL_ERROR); @@ -175,13 +185,33 @@ void HAssAnnounceRelayLight(void) char stemp2[TOPSZ]; char stemp3[TOPSZ]; char unique_id[30]; - bool is_light = false; - bool is_topic_light = false; + + bool LightControl = light_controller.isCTRGBLinked(); // SetOption37 - Color remapping for led channels, also provides an option for allowing independent handling of RGB and white channels + bool PwmMulti = Settings.flag3.pwm_multi_channels; // SetOption68 - Multi-channel PWM instead of a single light + bool is_topic_light = false; // Switch HAss domain between Lights and Relays + bool ind_light = false; // Controls Separated Lights when SetOption37 is >= 128 + bool ct_light = false; // Controls a CT Light when SetOption37 is >= 128 + bool wt_light = false; // Controls a White Light when SetOption37 is >= 128 + bool err_flag = false; // When true it blocks the creation of entities if the order of the Relays is not correct to avoid issue with Lights + + uint8_t dimmer = 1; + uint8_t max_lights = 1; + + // If there is a special Light to be enabled and managed with SetOption68 or SetOption37 >= 128, Discovery calculates the maximum number of entities to be generated in advance + + if (PwmMulti) { max_lights = Light.subtype; } + + if (!LightControl) { + ind_light = 1; + if (!PwmMulti) { max_lights = 2;} + } + + // Lights types: 0 = LST_NONE / 1 = LST_SINGLE / 2 = LST_COLDWARM / 3 = LST_RGB / 4 = LST_RGBW / 5 = LST_RGBCW for (uint32_t i = 1; i <= MAX_RELAYS; i++) { - is_light = ((i == devices_present) && (light_type)); - is_topic_light = Settings.flag.hass_light || is_light; // SetOption30 - Enforce HAss autodiscovery as light + bool RelayX = PinUsed(GPIO_REL1 +i-1); + is_topic_light = Settings.flag.hass_light && RelayX || light_type && !RelayX; // SetOption30 - Enforce HAss autodiscovery as light mqtt_data[0] = '\0'; // Clear retained message @@ -195,70 +225,95 @@ void HAssAnnounceRelayLight(void) snprintf_P(stopic, sizeof(stopic), PSTR(HOME_ASSISTANT_DISCOVERY_PREFIX "/%s/%s/config"), (is_topic_light) ? "light" : "switch", unique_id); - if (Settings.flag.hass_discovery && (i <= devices_present)) - { // SetOption19 - Control Home Assistantautomatic discovery (See SetOption59) - char name[33 + 2]; // friendlyname(33) + " " + index - char value_template[33]; - char prefix[TOPSZ]; - char *command_topic = stemp1; - char *state_topic = stemp2; - char *availability_topic = stemp3; + if ((i < Light.device) && !RelayX) { + err_flag = true; + AddLog_P2(LOG_LEVEL_ERROR, PSTR("%s"), kHAssError2); + } else { + if (Settings.flag.hass_discovery && (RelayX || (Light.device > 0) && (max_lights > 0)) && !err_flag ) + { // SetOption19 - Control Home Assistantautomatic discovery (See SetOption59) + char name[33 + 2]; // friendlyname(33) + " " + index + char value_template[33]; + char prefix[TOPSZ]; + char *command_topic = stemp1; + char *state_topic = stemp2; + char *availability_topic = stemp3; - if (i > MAX_FRIENDLYNAMES) { - snprintf_P(name, sizeof(name), PSTR("%s %d"), SettingsText(SET_FRIENDLYNAME1), i); - } else { - snprintf_P(name, sizeof(name), SettingsText(SET_FRIENDLYNAME1 + i - 1)); + if (i > MAX_FRIENDLYNAMES) { + snprintf_P(name, sizeof(name), PSTR("%s %d"), SettingsText(SET_FRIENDLYNAME1), i-1); + } else { + snprintf_P(name, sizeof(name), SettingsText(SET_FRIENDLYNAME1 + i-1)); + } + + GetPowerDevice(value_template, i, sizeof(value_template), Settings.flag.device_index_enable); // SetOption26 - Switch between POWER or POWER1 + GetTopic_P(command_topic, CMND, mqtt_topic, value_template); + GetTopic_P(state_topic, TELE, mqtt_topic, D_RSLT_STATE); + GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); + Response_P(HASS_DISCOVER_BASE, name, state_topic, availability_topic); + TryResponseAppend_P(HASS_DISCOVER_RELAY, command_topic, value_template, SettingsText(SET_STATE_TXT1), SettingsText(SET_STATE_TXT2)); + TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO_SHORT, unique_id, ESP_getChipId()); + + #ifdef USE_LIGHT + if ((i >= Light.device) + #ifdef ESP8266 + || PWM_DIMMER == my_module_type + #endif + ) + { + if (!RelayX) { + char *brightness_command_topic = stemp1; + strncpy_P(stemp3, Settings.flag.not_power_linked ? PSTR("last") : PSTR("brightness"), sizeof(stemp3)); // SetOption20 - Control power in relation to Dimmer/Color/Ct changes + char channel_num[9]; + if (PwmMulti) { // SetOption68 - Multi-channel PWM instead of a single light + snprintf_P(channel_num, sizeof(channel_num), PSTR("Channel%d"), i); + } else { + if (!LightControl) { // SetOption37 >= 128 - Color remapping for led channels, also provides an option for allowing independent handling of RGB and white channels + snprintf_P(channel_num, sizeof(channel_num), PSTR("" D_CMND_DIMMER "%d"), dimmer); + dimmer ++; + } else { + snprintf_P(channel_num, sizeof(channel_num), PSTR("" D_CMND_DIMMER "")); + } + } + GetTopic_P(brightness_command_topic, CMND, mqtt_topic, channel_num); + TryResponseAppend_P(HASS_DISCOVER_BASE_LIGHT, brightness_command_topic, state_topic, stemp3, channel_num); + } + if ((ind_light && !PwmMulti) || LightControl) { + + if (Light.subtype >= LST_RGB) { + char *rgb_command_topic = stemp1; + + GetTopic_P(rgb_command_topic, CMND, mqtt_topic, D_CMND_COLOR); + TryResponseAppend_P(HASS_DISCOVER_LIGHT_COLOR, rgb_command_topic, state_topic); + + char *effect_command_topic = stemp1; + GetTopic_P(effect_command_topic, CMND, mqtt_topic, D_CMND_SCHEME); + TryResponseAppend_P(HASS_DISCOVER_LIGHT_SCHEME, effect_command_topic, state_topic); + } + if (LST_RGBW == Light.subtype) { wt_light = true; } + if (LST_RGBCW == Light.subtype) { ct_light = true; } + } + + if ((!ind_light && ct_light) || (LST_COLDWARM == Light.subtype && + !PwmMulti && LightControl)) { + char *color_temp_command_topic = stemp1; + + GetTopic_P(color_temp_command_topic, CMND, mqtt_topic, D_CMND_COLORTEMPERATURE); + TryResponseAppend_P(HASS_DISCOVER_LIGHT_CT, color_temp_command_topic, state_topic); + ct_light = false; + } + if ((!ind_light && wt_light) || (LST_RGBW == Light.subtype && + !PwmMulti && LightControl)) { + char *white_temp_command_topic = stemp1; + + GetTopic_P(white_temp_command_topic, CMND, mqtt_topic, D_CMND_WHITE); + TryResponseAppend_P(HASS_DISCOVER_LIGHT_WHITE, white_temp_command_topic, state_topic); + wt_light = false; + } + ind_light = false; + max_lights--; + } + #endif // USE_LIGHT + TryResponseAppend_P(PSTR("}")); } - GetPowerDevice(value_template, i, sizeof(value_template), Settings.flag.device_index_enable); // SetOption26 - Switch between POWER or POWER1 - GetTopic_P(command_topic, CMND, mqtt_topic, value_template); - GetTopic_P(state_topic, TELE, mqtt_topic, D_RSLT_STATE); - GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); - - Response_P(HASS_DISCOVER_BASE, name, state_topic, availability_topic); - TryResponseAppend_P(HASS_DISCOVER_RELAY, command_topic, value_template, SettingsText(SET_STATE_TXT1), SettingsText(SET_STATE_TXT2)); - TryResponseAppend_P(HASS_DISCOVER_DEVICE_INFO_SHORT, unique_id, ESP_getChipId()); - -#ifdef USE_LIGHT - if (is_light -#ifdef ESP8266 - || PWM_DIMMER == my_module_type -#endif - ) - { - char *brightness_command_topic = stemp1; - - GetTopic_P(brightness_command_topic, CMND, mqtt_topic, D_CMND_DIMMER); - strncpy_P(stemp3, Settings.flag.not_power_linked ? PSTR("last") : PSTR("brightness"), sizeof(stemp3)); // SetOption20 - Control power in relation to Dimmer/Color/Ct changes - TryResponseAppend_P(HASS_DISCOVER_LIGHT_DIMMER, brightness_command_topic, state_topic, stemp3); - - if (Light.subtype >= LST_RGB) - { - char *rgb_command_topic = stemp1; - - GetTopic_P(rgb_command_topic, CMND, mqtt_topic, D_CMND_COLOR); - TryResponseAppend_P(HASS_DISCOVER_LIGHT_COLOR, rgb_command_topic, state_topic); - - char *effect_command_topic = stemp1; - GetTopic_P(effect_command_topic, CMND, mqtt_topic, D_CMND_SCHEME); - TryResponseAppend_P(HASS_DISCOVER_LIGHT_SCHEME, effect_command_topic, state_topic); - } - if (LST_RGBW == Light.subtype) - { - char *white_temp_command_topic = stemp1; - - GetTopic_P(white_temp_command_topic, CMND, mqtt_topic, D_CMND_WHITE); - TryResponseAppend_P(HASS_DISCOVER_LIGHT_WHITE, white_temp_command_topic, state_topic); - } - if ((LST_COLDWARM == Light.subtype) || (LST_RGBCW == Light.subtype)) - { - char *color_temp_command_topic = stemp1; - - GetTopic_P(color_temp_command_topic, CMND, mqtt_topic, D_CMND_COLORTEMPERATURE); - TryResponseAppend_P(HASS_DISCOVER_LIGHT_CT, color_temp_command_topic, state_topic); - } - } -#endif // USE_LIGHT - TryResponseAppend_P(PSTR("}")); } MqttPublish(stopic, true); } @@ -346,7 +401,7 @@ void HAssAnnouncerBinSensors(uint8_t device, uint8_t present, uint8_t dual, uint GetTopic_P(state_topic, STAT, mqtt_topic, jsoname); GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); - snprintf_P(name, sizeof(name), PSTR("%s Switch%d"), SettingsText(SET_FRIENDLYNAME1), device + 1); + snprintf_P(name, sizeof(name), PSTR("%s Switch%d"), ModuleName().c_str(), device + 1); Response_P(HASS_DISCOVER_BASE, name, state_topic, availability_topic); if (!pir) { TryResponseAppend_P(HASS_DISCOVER_BIN_SWITCH, PSTR(D_RSLT_STATE), SettingsText(SET_STATE_TXT2), SettingsText(SET_STATE_TXT1)); @@ -372,7 +427,7 @@ void HAssAnnounceSwitches(void) if (PinUsed(GPIO_SWT1, switch_index)) { switch_present = 1; } - if (KeyTopicActive(1) && strcmp(SettingsText(SET_MQTT_SWITCH_TOPIC), mqtt_topic)) // Enable Discovery for Switches only if Switchtopic is set to a custom name + if (KeyTopicActive(1) && strcmp(SettingsText(SET_MQTT_SWITCH_TOPIC), mqtt_topic)) // Enable Discovery for Switches only if SwitchTopic is set to a custom name { // switch matrix for triggers and binary sensor generation when switchtopic is set as custom (default index is 0,0 - TOGGLE, TOGGLE): @@ -436,7 +491,6 @@ void HAssAnnounceSwitches(void) } } - void HAssAnnounceButtons(void) { for (uint32_t button_index = 0; button_index < MAX_KEYS; button_index++) @@ -469,7 +523,7 @@ void HAssAnnounceButtons(void) // Trigger types: 10 = button_short_press | 11 = button_double_press | 12 = button_triple_press | 13 = button_quadruple_press | 14 = button_quintuple_press | 3 = button_long_press - if (!Settings.flag3.mqtt_buttons) { // Enable buttons discovery [SetOption73] - Decouple button from relay and send just mqtt topic + if (!Settings.flag3.mqtt_buttons) { // Enable Buttons for discovery [SetOption73] - Decouple button from relay and send just mqtt topic button_present = 0; } else { if (Settings.flag.button_single) { // [SetOption13] Immediate action on button press, just SINGLE trigger @@ -504,7 +558,7 @@ void HAssAnnounceSensor(const char *sensorname, const char *subsensortype, const char *availability_topic = stemp2; GetTopic_P(state_topic, TELE, mqtt_topic, PSTR(D_RSLT_SENSOR)); - snprintf_P(name, sizeof(name), PSTR("%s %s %s"), SettingsText(SET_FRIENDLYNAME1), sensorname, MultiSubName); + snprintf_P(name, sizeof(name), PSTR("%s %s %s"), ModuleName().c_str(), sensorname, MultiSubName); GetTopic_P(availability_topic, TELE, mqtt_topic, S_LWT); Response_P(HASS_DISCOVER_BASE, name, state_topic, availability_topic); @@ -575,7 +629,6 @@ void HAssAnnounceSensors(void) snprintf_P(sensordata, sizeof(sensordata), PSTR("%s}"), sensordata); // {"INA219":{"Voltage":4.494,"Current":0.020,"Power":0.089}} // USE THE FOLLOWING LINE TO TEST JSON //snprintf_P(sensordata, sizeof(sensordata), PSTR("{\"HX711\":{\"Weight\":[22,34,1023.4]}}")); - //snprintf_P(sensordata, sizeof(sensordata), PSTR("{\"TX23\":{\"Speed\":{\"Act\":8.6,\"Avg\":8.2,\"Min\":0,\"Max\":15.8},\"Dir\":{\"Card\":\"SSO\",\"Deg\":157.5,\"Avg\":145.5,\"AvgCard\":\"SO\",\"Min\":112.5,\"Max\":292.5,\"Range\":180}}}")); StaticJsonBuffer<500> jsonBuffer; JsonObject &root = jsonBuffer.parseObject(sensordata); @@ -659,16 +712,13 @@ void HAssAnnounceDeviceInfoAndStatusSensor(void) void HAssPublishStatus(void) { - Response_P(PSTR("{\"" D_JSON_VERSION "\":\"%s%s\",\"" D_JSON_BUILDDATETIME "\":\"%s\"," - "\"" D_JSON_COREVERSION "\":\"" ARDUINO_CORE_RELEASE "\",\"" D_JSON_SDKVERSION "\":\"%s\"," - "\"" D_CMND_MODULE "\":\"%s\",\"" D_JSON_RESTARTREASON "\":\"%s\",\"" D_JSON_UPTIME "\":\"%s\"," - "\"WiFi " D_JSON_LINK_COUNT "\":%d,\"WiFi " D_JSON_DOWNTIME "\":\"%s\",\"" D_JSON_MQTT_COUNT "\":%d," - "\"" D_JSON_BOOTCOUNT "\":%d,\"" D_JSON_SAVECOUNT "\":%d,\"" D_CMND_IPADDRESS "\":\"%s\"," - "\"" D_JSON_RSSI "\":\"%d\",\"LoadAvg\":%lu}"), - my_version, my_image, GetBuildDateAndTime().c_str(), ESP.getSdkVersion(), ModuleName().c_str(), - GetResetReason().c_str(), GetUptime().c_str(), WifiLinkCount(), WifiDowntime().c_str(), MqttConnectCount(), - Settings.bootcount, Settings.save_flag, WiFi.localIP().toString().c_str(), - WifiGetRssiAsQuality(WiFi.RSSI()), loop_load_avg); + Response_P(PSTR("{\"" D_JSON_VERSION "\":\"%s%s\",\"" D_JSON_BUILDDATETIME "\":\"%s\",\"" D_CMND_MODULE " or " D_CMND_TEMPLATE"\":\"%s\"," + "\"" D_JSON_RESTARTREASON "\":\"%s\",\"" D_JSON_UPTIME "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\"," + "\"" D_CMND_IPADDRESS "\":\"%s\",\"" D_JSON_RSSI "\":\"%d\",\"" D_JSON_SIGNAL " (dBm)""\":\"%d\"," + "\"WiFi " D_JSON_LINK_COUNT "\":%d,\"WiFi " D_JSON_DOWNTIME "\":\"%s\",\"" D_JSON_MQTT_COUNT "\":%d,\"LoadAvg\":%lu}"), + my_version, my_image, GetBuildDateAndTime().c_str(), ModuleName().c_str(), GetResetReason().c_str(), + GetUptime().c_str(), my_hostname, WiFi.localIP().toString().c_str(), WifiGetRssiAsQuality(WiFi.RSSI()), + WiFi.RSSI(), WifiLinkCount(), WifiDowntime().c_str(), MqttConnectCount(), loop_load_avg); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_HASS_STATE)); } @@ -687,8 +737,6 @@ void HAssDiscovery(void) if (Settings.flag.hass_discovery || (1 == hass_mode)) { // SetOption19 - Control Home Assistantautomatic discovery (See SetOption59) - // Send info about relays and lights - HAssAnnounceRelayLight(); // Send info about buttons HAssAnnounceButtons(); @@ -699,6 +747,9 @@ void HAssDiscovery(void) // Send info about sensors HAssAnnounceSensors(); + // Send info about relays and lights + HAssAnnounceRelayLight(); + // Send info about status sensor HAssAnnounceDeviceInfoAndStatusSensor(); } diff --git a/tasmota/xdrv_39_webcam.ino b/tasmota/xdrv_39_webcam.ino index 377d420ae..f85a543b7 100644 --- a/tasmota/xdrv_39_webcam.ino +++ b/tasmota/xdrv_39_webcam.ino @@ -878,13 +878,13 @@ bool Xdrv39(uint8_t function) { wc_pic_setup(); break; case FUNC_WEB_ADD_MAIN_BUTTON: - //if (Settings.esp32_webcam_resolution) { -#ifndef USE_SCRIPT + if (Settings.esp32_webcam_resolution) { +//#ifndef USE_SCRIPT WcStreamControl(Settings.esp32_webcam_resolution); delay(50); // Give the webcam webserver some time to prepare the stream wc_show_stream(); -#endif - //} +//#endif + } break; case FUNC_COMMAND: result = DecodeCommand(kWCCommands, WCCommand); diff --git a/tasmota/xsns_27_apds9960.ino b/tasmota/xsns_27_apds9960.ino index 99feca1ba..e0fe1acaf 100644 --- a/tasmota/xsns_27_apds9960.ino +++ b/tasmota/xsns_27_apds9960.ino @@ -48,6 +48,7 @@ // #undef USE_TSL2561 // possible address conflict on the I2C-bus // #endif // #endif + #define XSNS_27 27 #define XI2C_21 21 // See I2CDEVICES.md @@ -61,17 +62,9 @@ #define APDS9930_CHIPID_1 0x12 // we will check, if someone got an incorrect sensor #define APDS9930_CHIPID_2 0x39 // there are case reports about "accidentially bought" 9930's - -// TODO() : Move to my_user_config.h file -#define USE_APDS9960_GESTURE // Enable Gesture feature (+2k code) -#define USE_APDS9960_PROXIMITY // Enable Proximity feature (>50 code) -#define USE_APDS9960_COLOR // Enable Color feature (+0.8k code) - #define APDS9960_MODE_GESTURE 0 #define APDS9960_MODE_COLOR 1 -#define USE_APDS9960_STARTMODE APDS9960_MODE_GESTURE - /* Gesture parameters */ #define GESTURE_THRESHOLD_OUT 10 #define GESTURE_SENSITIVITY_1 50 @@ -80,19 +73,8 @@ #define APDS9960_LONG_RECOVERY 50 // long pause after sensor overload in loops #define APDS9960_MAX_GESTURE_CYCLES 50 // how many FIFO-reads are allowed to prevent crash - -// TODO() : Move to Translate file -#define D_GESTURE "Gesture" -#define D_COLOR_RED "Red" -#define D_COLOR_GREEN "Green" -#define D_COLOR_BLUE "Blue" -#define D_CCT "CCT" -#define D_PROXIMITY "Proximity" - -#define D_UNIT_KELVIN "°K" - /******************************************************************************\ - * constants + * Constants \******************************************************************************/ const char APDS9960_TAG[] PROGMEM = "APDS9960"; // Only one actualy diff --git a/tasmota/xsns_68_opentherm.ino b/tasmota/xsns_68_opentherm.ino new file mode 100644 index 000000000..953f037e5 --- /dev/null +++ b/tasmota/xsns_68_opentherm.ino @@ -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 . +*/ + +#include + +#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 (PinUsed(GPIO_BOILER_OT_RX) && PinUsed(GPIO_BOILER_OT_TX)) + { + 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 diff --git a/tasmota/xsns_68_opentherm_protocol.ino b/tasmota/xsns_68_opentherm_protocol.ino new file mode 100644 index 000000000..2693b086c --- /dev/null +++ b/tasmota/xsns_68_opentherm_protocol.ino @@ -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 . +*/ +#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 \ No newline at end of file