mirror of
https://github.com/esphome/esphome.git
synced 2025-04-13 06:10:30 +01:00
379 lines
11 KiB
C++
379 lines
11 KiB
C++
/*
|
|
* OpenTherm protocol implementation. Originally taken from https://github.com/jpraus/arduino-opentherm, but
|
|
* heavily modified to comply with ESPHome coding standards and provide better logging.
|
|
* Original code is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
|
|
* Public License, which is compatible with GPLv3 license, which covers C++ part of ESPHome project.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include "esphome/core/hal.h"
|
|
#include "esphome/core/log.h"
|
|
#include "esphome/core/helpers.h"
|
|
|
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
|
#include "driver/timer.h"
|
|
#endif
|
|
|
|
namespace esphome {
|
|
namespace opentherm {
|
|
|
|
template<class T> constexpr T read_bit(T value, uint8_t bit) { return (value >> bit) & 0x01; }
|
|
|
|
template<class T> constexpr T set_bit(T value, uint8_t bit) { return value |= (1UL << bit); }
|
|
|
|
template<class T> constexpr T clear_bit(T value, uint8_t bit) { return value &= ~(1UL << bit); }
|
|
|
|
template<class T> constexpr T write_bit(T value, uint8_t bit, uint8_t bit_value) {
|
|
return bit_value ? set_bit(value, bit) : clear_bit(value, bit);
|
|
}
|
|
|
|
enum OperationMode {
|
|
IDLE = 0, // no operation
|
|
|
|
LISTEN = 1, // waiting for transmission to start
|
|
READ = 2, // reading 32-bit data frame
|
|
RECEIVED = 3, // data frame received with valid start and stop bit
|
|
|
|
WRITE = 4, // writing data with timer_
|
|
SENT = 5, // all data written to output
|
|
|
|
ERROR_PROTOCOL = 8, // manchester protocol data transfer error
|
|
ERROR_TIMEOUT = 9 // read timeout
|
|
};
|
|
|
|
enum ProtocolErrorType {
|
|
NO_ERROR = 0, // No error
|
|
NO_TRANSITION = 1, // No transition in the middle of the bit
|
|
INVALID_STOP_BIT = 2, // Stop bit wasn't present when expected
|
|
PARITY_ERROR = 3, // Parity check didn't pass
|
|
NO_CHANGE_TOO_LONG = 4, // No level change for too much timer ticks
|
|
};
|
|
|
|
enum MessageType {
|
|
READ_DATA = 0,
|
|
READ_ACK = 4,
|
|
WRITE_DATA = 1,
|
|
WRITE_ACK = 5,
|
|
INVALID_DATA = 2,
|
|
DATA_INVALID = 6,
|
|
UNKNOWN_DATAID = 7
|
|
};
|
|
|
|
enum MessageId {
|
|
STATUS = 0,
|
|
CH_SETPOINT = 1,
|
|
CONTROLLER_CONFIG = 2,
|
|
DEVICE_CONFIG = 3,
|
|
COMMAND_CODE = 4,
|
|
FAULT_FLAGS = 5,
|
|
REMOTE = 6,
|
|
COOLING_CONTROL = 7,
|
|
CH2_SETPOINT = 8,
|
|
CH_SETPOINT_OVERRIDE = 9,
|
|
TSP_COUNT = 10,
|
|
TSP_COMMAND = 11,
|
|
FHB_SIZE = 12,
|
|
FHB_COMMAND = 13,
|
|
MAX_MODULATION_LEVEL = 14,
|
|
MAX_BOILER_CAPACITY = 15, // u8_hb - u8_lb gives min modulation level
|
|
ROOM_SETPOINT = 16,
|
|
MODULATION_LEVEL = 17,
|
|
CH_WATER_PRESSURE = 18,
|
|
DHW_FLOW_RATE = 19,
|
|
DAY_TIME = 20,
|
|
DATE = 21,
|
|
YEAR = 22,
|
|
ROOM_SETPOINT_CH2 = 23,
|
|
ROOM_TEMP = 24,
|
|
FEED_TEMP = 25,
|
|
DHW_TEMP = 26,
|
|
OUTSIDE_TEMP = 27,
|
|
RETURN_WATER_TEMP = 28,
|
|
SOLAR_STORE_TEMP = 29,
|
|
SOLAR_COLLECT_TEMP = 30,
|
|
FEED_TEMP_CH2 = 31,
|
|
DHW2_TEMP = 32,
|
|
EXHAUST_TEMP = 33,
|
|
FAN_SPEED = 35,
|
|
FLAME_CURRENT = 36,
|
|
ROOM_TEMP_CH2 = 37,
|
|
REL_HUMIDITY = 38,
|
|
DHW_BOUNDS = 48,
|
|
CH_BOUNDS = 49,
|
|
OTC_CURVE_BOUNDS = 50,
|
|
DHW_SETPOINT = 56,
|
|
MAX_CH_SETPOINT = 57,
|
|
OTC_CURVE_RATIO = 58,
|
|
|
|
// HVAC Specific Message IDs
|
|
HVAC_STATUS = 70,
|
|
REL_VENT_SETPOINT = 71,
|
|
DEVICE_VENT = 74,
|
|
HVAC_VER_ID = 75,
|
|
REL_VENTILATION = 77,
|
|
REL_HUMID_EXHAUST = 78,
|
|
EXHAUST_CO2 = 79,
|
|
SUPPLY_INLET_TEMP = 80,
|
|
SUPPLY_OUTLET_TEMP = 81,
|
|
EXHAUST_INLET_TEMP = 82,
|
|
EXHAUST_OUTLET_TEMP = 83,
|
|
EXHAUST_FAN_SPEED = 84,
|
|
SUPPLY_FAN_SPEED = 85,
|
|
REMOTE_VENTILATION_PARAM = 86,
|
|
NOM_REL_VENTILATION = 87,
|
|
HVAC_NUM_TSP = 88,
|
|
HVAC_IDX_TSP = 89,
|
|
HVAC_FHB_SIZE = 90,
|
|
HVAC_FHB_IDX = 91,
|
|
|
|
RF_SIGNAL = 98,
|
|
DHW_MODE = 99,
|
|
OVERRIDE_FUNC = 100,
|
|
|
|
// Solar Specific Message IDs
|
|
SOLAR_MODE_FLAGS = 101, // hb0-2 Controller storage mode
|
|
// lb0 Device fault
|
|
// lb1-3 Device mode status
|
|
// lb4-5 Device status
|
|
SOLAR_ASF = 102,
|
|
SOLAR_VERSION_ID = 103,
|
|
SOLAR_PRODUCT_ID = 104,
|
|
SOLAR_NUM_TSP = 105,
|
|
SOLAR_IDX_TSP = 106,
|
|
SOLAR_FHB_SIZE = 107,
|
|
SOLAR_FHB_IDX = 108,
|
|
SOLAR_STARTS = 109,
|
|
SOLAR_HOURS = 110,
|
|
SOLAR_ENERGY = 111,
|
|
SOLAR_TOTAL_ENERGY = 112,
|
|
|
|
FAILED_BURNER_STARTS = 113,
|
|
BURNER_FLAME_LOW = 114,
|
|
OEM_DIAGNOSTIC = 115,
|
|
BURNER_STARTS = 116,
|
|
CH_PUMP_STARTS = 117,
|
|
DHW_PUMP_STARTS = 118,
|
|
DHW_BURNER_STARTS = 119,
|
|
BURNER_HOURS = 120,
|
|
CH_PUMP_HOURS = 121,
|
|
DHW_PUMP_HOURS = 122,
|
|
DHW_BURNER_HOURS = 123,
|
|
OT_VERSION_CONTROLLER = 124,
|
|
OT_VERSION_DEVICE = 125,
|
|
VERSION_CONTROLLER = 126,
|
|
VERSION_DEVICE = 127
|
|
};
|
|
|
|
enum BitPositions { STOP_BIT = 33 };
|
|
|
|
/**
|
|
* Structure to hold Opentherm data packet content.
|
|
* Use f88(), u16() or s16() functions to get appropriate value of data packet accoridng to id of message.
|
|
*/
|
|
struct OpenthermData {
|
|
uint8_t type;
|
|
uint8_t id;
|
|
uint8_t valueHB;
|
|
uint8_t valueLB;
|
|
|
|
OpenthermData() : type(0), id(0), valueHB(0), valueLB(0) {}
|
|
|
|
/**
|
|
* @return float representation of data packet value
|
|
*/
|
|
float f88();
|
|
|
|
/**
|
|
* @param float number to set as value of this data packet
|
|
*/
|
|
void f88(float value);
|
|
|
|
/**
|
|
* @return unsigned 16b integer representation of data packet value
|
|
*/
|
|
uint16_t u16();
|
|
|
|
/**
|
|
* @param unsigned 16b integer number to set as value of this data packet
|
|
*/
|
|
void u16(uint16_t value);
|
|
|
|
/**
|
|
* @return signed 16b integer representation of data packet value
|
|
*/
|
|
int16_t s16();
|
|
|
|
/**
|
|
* @param signed 16b integer number to set as value of this data packet
|
|
*/
|
|
void s16(int16_t value);
|
|
};
|
|
|
|
struct OpenThermError {
|
|
ProtocolErrorType error_type;
|
|
uint32_t capture;
|
|
uint8_t clock;
|
|
uint32_t data;
|
|
uint8_t bit_pos;
|
|
};
|
|
|
|
/**
|
|
* Opentherm static class that supports either listening or sending Opentherm data packets in the same time
|
|
*/
|
|
class OpenTherm {
|
|
public:
|
|
OpenTherm(InternalGPIOPin *in_pin, InternalGPIOPin *out_pin, int32_t device_timeout = 800);
|
|
|
|
/**
|
|
* Setup pins.
|
|
*/
|
|
bool initialize();
|
|
|
|
/**
|
|
* Start listening for Opentherm data packet comming from line connected to given pin.
|
|
* If data packet is received then has_message() function returns true and data packet can be retrieved by calling
|
|
* get_message() function. If timeout > 0 then this function waits for incomming data package for timeout millis and
|
|
* if no data packet is recevived, error state is indicated by is_error() function. If either data packet is received
|
|
* or timeout is reached listening is stopped.
|
|
*/
|
|
void listen();
|
|
|
|
/**
|
|
* Use this function to check whether listen() function already captured a valid data packet.
|
|
*
|
|
* @return true if data packet has been captured from line by listen() function.
|
|
*/
|
|
bool has_message() { return mode_ == OperationMode::RECEIVED; }
|
|
|
|
/**
|
|
* Use this to retrive data packed captured by listen() function. Data packet is ready when has_message() function
|
|
* returns true. This function can be called multiple times until stop() is called.
|
|
*
|
|
* @param data reference to data structure to which fill the data packet data.
|
|
* @return true if packet was ready and was filled into data structure passed, false otherwise.
|
|
*/
|
|
bool get_message(OpenthermData &data);
|
|
|
|
/**
|
|
* Immediately send out Opentherm data packet to line connected on given pin.
|
|
* Completed data transfer is indicated by is_sent() function.
|
|
* Error state is indicated by is_error() function.
|
|
*
|
|
* @param data Opentherm data packet.
|
|
*/
|
|
void send(OpenthermData &data);
|
|
|
|
/**
|
|
* Stops listening for data packet or sending out data packet and resets internal state of this class.
|
|
* Stops all timers and unattaches all interrupts.
|
|
*/
|
|
void stop();
|
|
|
|
/**
|
|
* Get protocol error details in case a protocol error occured.
|
|
* @param error reference to data structure to which fill the error details
|
|
* @return true if protocol error occured during last conversation, false otherwise.
|
|
*/
|
|
bool get_protocol_error(OpenThermError &error);
|
|
|
|
/**
|
|
* Use this function to check whether send() function already finished sending data packed to line.
|
|
*
|
|
* @return true if data packet has been sent, false otherwise.
|
|
*/
|
|
bool is_sent() { return mode_ == OperationMode::SENT; }
|
|
|
|
/**
|
|
* Indicates whether listinig or sending is not in progress.
|
|
* That also means that no timers are running and no interrupts are attached.
|
|
*
|
|
* @return true if listening nor sending is in progress.
|
|
*/
|
|
bool is_idle() { return mode_ == OperationMode::IDLE; }
|
|
|
|
/**
|
|
* Indicates whether last listen() or send() operation ends up with an error. Includes both timeout and
|
|
* protocol errors.
|
|
*
|
|
* @return true if last listen() or send() operation ends up with an error.
|
|
*/
|
|
bool is_error() { return mode_ == OperationMode::ERROR_TIMEOUT || mode_ == OperationMode::ERROR_PROTOCOL; }
|
|
|
|
/**
|
|
* Indicates whether last listen() or send() operation ends up with a *timeout* error
|
|
* @return true if last listen() or send() operation ends up with a *timeout* error.
|
|
*/
|
|
bool is_timeout() { return mode_ == OperationMode::ERROR_TIMEOUT; }
|
|
|
|
/**
|
|
* Indicates whether last listen() or send() operation ends up with a *protocol* error
|
|
* @return true if last listen() or send() operation ends up with a *protocol* error.
|
|
*/
|
|
bool is_protocol_error() { return mode_ == OperationMode::ERROR_PROTOCOL; }
|
|
|
|
bool is_active() { return mode_ == LISTEN || mode_ == READ || mode_ == WRITE; }
|
|
|
|
OperationMode get_mode() { return mode_; }
|
|
|
|
void debug_data(OpenthermData &data);
|
|
void debug_error(OpenThermError &error) const;
|
|
|
|
const char *protocol_error_to_to_str(ProtocolErrorType error_type);
|
|
const char *message_type_to_str(MessageType message_type);
|
|
const char *operation_mode_to_str(OperationMode mode);
|
|
const char *message_id_to_str(MessageId id);
|
|
|
|
static bool timer_isr(OpenTherm *arg);
|
|
|
|
#ifdef ESP8266
|
|
static void esp8266_timer_isr();
|
|
#endif
|
|
|
|
private:
|
|
InternalGPIOPin *in_pin_;
|
|
InternalGPIOPin *out_pin_;
|
|
ISRInternalGPIOPin isr_in_pin_;
|
|
ISRInternalGPIOPin isr_out_pin_;
|
|
|
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
|
timer_group_t timer_group_;
|
|
timer_idx_t timer_idx_;
|
|
#endif
|
|
|
|
OperationMode mode_;
|
|
ProtocolErrorType error_type_;
|
|
uint32_t capture_;
|
|
uint8_t clock_;
|
|
uint32_t data_;
|
|
uint8_t bit_pos_;
|
|
int32_t timeout_counter_; // <0 no timeout
|
|
|
|
int32_t device_timeout_;
|
|
|
|
#if defined(ESP32) || defined(USE_ESP_IDF)
|
|
bool init_esp32_timer_();
|
|
void start_esp32_timer_(uint64_t alarm_value);
|
|
#endif
|
|
|
|
void stop_timer_();
|
|
|
|
void read_(); // data detected start reading
|
|
void start_read_timer_(); // reading timer_ to sample at 1/5 of manchester code bit length (at 5kHz)
|
|
void start_write_timer_(); // writing timer_ to send manchester code (at 2kHz)
|
|
bool check_parity_(uint32_t val);
|
|
|
|
void bit_read_(uint8_t value);
|
|
ProtocolErrorType verify_stop_bit_(uint8_t value);
|
|
void write_bit_(uint8_t high, uint8_t clock);
|
|
|
|
#ifdef ESP8266
|
|
// ESP8266 timer can accept callback with no parameters, so we have this hack to save a static instance of OpenTherm
|
|
static OpenTherm *instance_;
|
|
#endif
|
|
};
|
|
|
|
} // namespace opentherm
|
|
} // namespace esphome
|