ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
uponor_smatrix.cpp
Go to the documentation of this file.
1#include "uponor_smatrix.h"
4#include "esphome/core/log.h"
5
6#include <cinttypes>
7
9
10ESPHOME_LOG_TAG(TAG, "uponor_smatrix");
11
12// Maximum bytes to log in verbose hex output
13static constexpr size_t UPONOR_MAX_LOG_BYTES = 36;
14
16#ifdef USE_TIME
17 if (this->time_id_ != nullptr) {
18 this->time_id_->add_on_time_sync_callback([this] { this->send_time(); });
19 }
20#endif
21}
22
24 ESP_LOGCONFIG(TAG, "Uponor Smatrix");
25#ifdef USE_TIME
26 if (this->time_id_ != nullptr) {
27 ESP_LOGCONFIG(TAG, " Time synchronization: YES");
28 ESP_LOGCONFIG(TAG, " Time master device address: 0x%08" PRIX32 "", this->time_device_address_);
29 }
30#endif
31
32 if (!this->unknown_devices_.empty()) {
33 ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
34 for (auto device_address : this->unknown_devices_) {
35 ESP_LOGCONFIG(TAG, " 0x%08" PRIX32 "", device_address);
36 }
37 }
38}
39
42
43 // Discard stale data
44 if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) {
45 ESP_LOGD(TAG, "Discarding %d bytes of unparsed data", this->rx_buffer_.size());
46 this->rx_buffer_.clear();
47 }
48
49 // Read incoming data
50 this->read_available([this, now](uint8_t byte) {
51 // The controller polls devices every 10 seconds in some units or continuously in others with around 200 ms between
52 // devices. Remember timestamps so we can send our own packets when the bus is expected to be silent.
53 this->last_rx_ = now;
54
55 if (this->parse_byte_(byte)) {
56 this->rx_buffer_.clear();
57 }
58 });
59
60 // Send packets during bus silence
61 if (this->rx_buffer_.empty() && (now - this->last_rx_ > 50) && (now - this->last_rx_ < 100) &&
62 (now - this->last_tx_ > 200)) {
63#ifdef USE_TIME
64 // Only build time packet when bus is silent and queue is empty to make sure we can send it right away
65 if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())
66 this->send_time_requested_ = false;
67#endif
68 // Send the next packet in the queue
69 if (!this->tx_queue_.empty()) {
70 auto packet = std::move(this->tx_queue_.front());
71 this->tx_queue_.pop();
72
73 this->write_array(packet);
74 this->flush();
75
76 this->last_tx_ = now;
77 }
78 }
79}
80
82 this->rx_buffer_.push_back(byte);
83 const uint8_t *packet = this->rx_buffer_.data();
84 size_t packet_len = this->rx_buffer_.size();
85
86 if (packet_len < 7) {
87 // Minimum packet size is 7 bytes, wait for more
88 return false;
89 }
90
91 uint32_t device_address = encode_uint32(packet[0], packet[1], packet[2], packet[3]);
92 uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
93
94 uint16_t computed_crc = crc16(packet, packet_len - 2);
95 if (crc != computed_crc) {
96 // CRC did not match, more data might be coming
97 return false;
98 }
99
100#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
101 char hex_buf[format_hex_size(UPONOR_MAX_LOG_BYTES)];
102#endif
103 ESP_LOGV(TAG, "Received packet: addr=%08" PRIX32 ", data=%s, crc=%04X", device_address,
104 format_hex_to(hex_buf, &packet[4], packet_len - 6), crc);
105
106 // Handle packet
107 size_t data_len = (packet_len - 6) / 3;
108 if (data_len == 0) {
109 if (packet[4] == UPONOR_ID_REQUEST) {
110 ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08" PRIX32 "", device_address);
111 }
112 return true;
113 }
114
115 // Decode packet payload data for easy access
116 UponorSmatrixData data[data_len];
117 for (size_t i = 0; i < data_len; i++) {
118 data[i].id = packet[(i * 3) + 4];
119 data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]);
120 }
121
122#ifdef USE_TIME
123 // Detect device that acts as time master if not set explicitely
124 if (this->time_device_address_ == 0 && data_len >= 2) {
125 // The first thermostat paired to the controller will act as the time master. Time can only be manually adjusted at
126 // this first thermostat. To synchronize time, we need to know its address, so we search for packets coming from a
127 // thermostat sending both room temperature and time information.
128 bool found_temperature = false;
129 bool found_time = false;
130 for (size_t i = 0; i < data_len; i++) {
131 if (data[i].id == UPONOR_ID_ROOM_TEMP)
132 found_temperature = true;
133 if (data[i].id == UPONOR_ID_DATETIME1)
134 found_time = true;
135 if (found_temperature && found_time) {
136 ESP_LOGI(TAG, "Using detected time device address 0x%08" PRIX32 "", device_address);
137 this->time_device_address_ = device_address;
138 break;
139 }
140 }
141 }
142#endif
143
144 // Forward data to device components
145 bool found = false;
146 for (auto *device : this->devices_) {
147 if (device->address_ == device_address) {
148 found = true;
149 device->on_device_data(data, data_len);
150 }
151 }
152
153 // Log unknown device addresses
154 if (!found && !this->unknown_devices_.contains(device_address)) {
155 ESP_LOGI(TAG, "Received packet for unknown device address 0x%08" PRIX32 " ", device_address);
156 this->unknown_devices_.insert(device_address);
157 }
158
159 // Return true to reset buffer
160 return true;
161}
162
163bool UponorSmatrixComponent::send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len) {
164 if (device_address == 0 || data == nullptr || data_len == 0)
165 return false;
166
167 // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
168 std::vector<uint8_t> packet;
169 packet.reserve(6 + 3 * data_len);
170
171 packet.push_back(device_address >> 24);
172 packet.push_back(device_address >> 16);
173 packet.push_back(device_address >> 8);
174 packet.push_back(device_address >> 0);
175
176 for (size_t i = 0; i < data_len; i++) {
177 packet.push_back(data[i].id);
178 packet.push_back(data[i].value >> 8);
179 packet.push_back(data[i].value >> 0);
180 }
181
182 auto crc = crc16(packet.data(), packet.size());
183 packet.push_back(crc >> 0);
184 packet.push_back(crc >> 8);
185
186 this->tx_queue_.push(packet);
187 return true;
188}
189
190#ifdef USE_TIME
192 if (this->time_device_address_ == 0 || this->time_id_ == nullptr)
193 return false;
194
195 ESPTime now = this->time_id_->now();
196 if (!now.is_valid())
197 return false;
198
199 uint8_t year = now.year - 2000;
200 uint8_t month = now.month;
201 // ESPHome days are [1-7] starting with Sunday, Uponor days are [0-6] starting with Monday
202 uint8_t day_of_week = (now.day_of_week == 1) ? 6 : (now.day_of_week - 2);
203 uint8_t day_of_month = now.day_of_month;
204 uint8_t hour = now.hour;
205 uint8_t minute = now.minute;
206 uint8_t second = now.second;
207
208 uint16_t time1 = (year & 0x7F) << 7 | (month & 0x0F) << 3 | (day_of_week & 0x07);
209 uint16_t time2 = (day_of_month & 0x1F) << 11 | (hour & 0x1F) << 6 | (minute & 0x3F);
210 uint16_t time3 = second;
211
212 ESP_LOGI(TAG, "Sending local time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
213 now.minute, now.second);
214
215 UponorSmatrixData data[] = {{UPONOR_ID_DATETIME1, time1}, {UPONOR_ID_DATETIME2, time2}, {UPONOR_ID_DATETIME3, time3}};
216 return this->send(this->time_device_address_, data, sizeof(data) / sizeof(data[0]));
217}
218#endif
219
220} // namespace esphome::uponor_smatrix
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
ESPTime now()
Get the time in the currently defined timezone.
void add_on_time_sync_callback(F &&callback)
UARTFlushResult flush()
Definition uart.h:96
bool read_available(F &&on_byte)
Same as read_available_chunks(), calling on_byte(uint8_t) once per byte.
Definition uart.h:77
void write_array(const uint8_t *data, size_t len)
Definition uart.h:30
std::vector< UponorSmatrixDevice * > devices_
bool send(uint32_t device_address, const UponorSmatrixData *data, size_t data_len)
std::queue< std::vector< uint8_t > > tx_queue_
uint8_t month
Definition date_entity.h:1
uint16_t year
Definition date_entity.h:0
uint8_t second
uint8_t minute
uint8_t hour
ESPHOME_LOG_TAG(TAG, "uponor_smatrix.climate")
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
constexpr size_t format_hex_size(size_t byte_count)
Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1.
Definition helpers.h:1583
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:1006
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:998
char * format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length)
Format byte array as lowercase hex to buffer (base implementation).
Definition helpers.cpp:428
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
A more user-friendly version of struct tm from time.h.
Definition time.h:23
uint8_t minute
minutes after the hour [0-59]
Definition time.h:32
uint8_t second
seconds after the minute [0-60]
Definition time.h:30
uint8_t hour
hours since midnight [0-23]
Definition time.h:34
bool is_valid(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
Definition time.h:82
uint8_t day_of_month
day of the month [1-31]
Definition time.h:38
uint16_t year
year
Definition time.h:44
uint8_t month
month; january=1 [1-12]
Definition time.h:42
uint8_t day_of_week
day of the week; sunday=1 [1-7]
Definition time.h:36