ESPHome  2024.5.0
uponor_smatrix.cpp
Go to the documentation of this file.
1 #include "uponor_smatrix.h"
2 #include "esphome/core/log.h"
3 
4 namespace esphome {
5 namespace uponor_smatrix {
6 
7 static const char *const TAG = "uponor_smatrix";
8 
10 #ifdef USE_TIME
11  if (this->time_id_ != nullptr) {
12  this->time_id_->add_on_time_sync_callback([this] { this->send_time(); });
13  }
14 #endif
15 }
16 
18  ESP_LOGCONFIG(TAG, "Uponor Smatrix");
19  ESP_LOGCONFIG(TAG, " System address: 0x%04X", this->address_);
20 #ifdef USE_TIME
21  if (this->time_id_ != nullptr) {
22  ESP_LOGCONFIG(TAG, " Time synchronization: YES");
23  ESP_LOGCONFIG(TAG, " Time master device address: 0x%04X", this->time_device_address_);
24  }
25 #endif
26 
27  this->check_uart_settings(19200);
28 
29  if (!this->unknown_devices_.empty()) {
30  ESP_LOGCONFIG(TAG, " Detected unknown device addresses:");
31  for (auto device_address : this->unknown_devices_) {
32  ESP_LOGCONFIG(TAG, " 0x%04X", device_address);
33  }
34  }
35 }
36 
38  const uint32_t now = millis();
39 
40  // Discard stale data
41  if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) {
42  ESP_LOGD(TAG, "Discarding %d bytes of unparsed data", this->rx_buffer_.size());
43  this->rx_buffer_.clear();
44  }
45 
46  // Read incoming data
47  while (this->available()) {
48  // The controller polls devices every 10 seconds, with around 200 ms between devices.
49  // Remember timestamps so we can send our own packets when the bus is expected to be silent.
50  if (now - this->last_rx_ > 500) {
51  this->last_poll_start_ = now;
52  }
53  this->last_rx_ = now;
54 
55  uint8_t byte;
56  this->read_byte(&byte);
57  if (this->parse_byte_(byte)) {
58  this->rx_buffer_.clear();
59  }
60  }
61 
62  // Send packets during bus silence
63  if ((now - this->last_rx_ > 300) && (now - this->last_poll_start_ < 9500) && (now - this->last_tx_ > 200)) {
64 #ifdef USE_TIME
65  // Only build time packet when bus is silent and queue is empty to make sure we can send it right away
66  if (this->send_time_requested_ && this->tx_queue_.empty() && this->do_send_time_())
67  this->send_time_requested_ = false;
68 #endif
69  // Send the next packet in the queue
70  if (!this->tx_queue_.empty()) {
71  auto packet = std::move(this->tx_queue_.front());
72  this->tx_queue_.pop();
73 
74  this->write_array(packet);
75  this->flush();
76 
77  this->last_tx_ = now;
78  }
79  }
80 }
81 
83  this->rx_buffer_.push_back(byte);
84  const uint8_t *packet = this->rx_buffer_.data();
85  size_t packet_len = this->rx_buffer_.size();
86 
87  if (packet_len < 7) {
88  // Minimum packet size is 7 bytes, wait for more
89  return false;
90  }
91 
92  uint16_t system_address = encode_uint16(packet[0], packet[1]);
93  uint16_t device_address = encode_uint16(packet[2], packet[3]);
94  uint16_t crc = encode_uint16(packet[packet_len - 1], packet[packet_len - 2]);
95 
96  uint16_t computed_crc = crc16(packet, packet_len - 2);
97  if (crc != computed_crc) {
98  // CRC did not match, more data might be coming
99  return false;
100  }
101 
102  ESP_LOGV(TAG, "Received packet: sys=%04X, dev=%04X, data=%s, crc=%04X", system_address, device_address,
103  format_hex(&packet[4], packet_len - 6).c_str(), crc);
104 
105  // Detect or check system address
106  if (this->address_ == 0) {
107  ESP_LOGI(TAG, "Using detected system address 0x%04X", system_address);
108  this->address_ = system_address;
109  } else if (this->address_ != system_address) {
110  // This should never happen except if the system address was set or detected incorrectly, so warn the user.
111  ESP_LOGW(TAG, "Received packet from unknown system address 0x%04X", system_address);
112  return true;
113  }
114 
115  // Handle packet
116  size_t data_len = (packet_len - 6) / 3;
117  if (data_len == 0) {
118  if (packet[4] == UPONOR_ID_REQUEST)
119  ESP_LOGVV(TAG, "Ignoring request packet for device 0x%04X", device_address);
120  return true;
121  }
122 
123  // Decode packet payload data for easy access
124  UponorSmatrixData data[data_len];
125  for (int i = 0; i < data_len; i++) {
126  data[i].id = packet[(i * 3) + 4];
127  data[i].value = encode_uint16(packet[(i * 3) + 5], packet[(i * 3) + 6]);
128  }
129 
130 #ifdef USE_TIME
131  // Detect device that acts as time master if not set explicitely
132  if (this->time_device_address_ == 0 && data_len >= 2) {
133  // The first thermostat paired to the controller will act as the time master. Time can only be manually adjusted at
134  // this first thermostat. To synchronize time, we need to know its address, so we search for packets coming from a
135  // thermostat sending both room temperature and time information.
136  bool found_temperature = false;
137  bool found_time = false;
138  for (int i = 0; i < data_len; i++) {
139  if (data[i].id == UPONOR_ID_ROOM_TEMP)
140  found_temperature = true;
141  if (data[i].id == UPONOR_ID_DATETIME1)
142  found_time = true;
143  if (found_temperature && found_time) {
144  ESP_LOGI(TAG, "Using detected time device address 0x%04X", device_address);
145  this->time_device_address_ = device_address;
146  break;
147  }
148  }
149  }
150 #endif
151 
152  // Forward data to device components
153  bool found = false;
154  for (auto *device : this->devices_) {
155  if (device->address_ == device_address) {
156  found = true;
157  device->on_device_data(data, data_len);
158  }
159  }
160 
161  // Log unknown device addresses
162  if (!found && !this->unknown_devices_.count(device_address)) {
163  ESP_LOGI(TAG, "Received packet for unknown device address 0x%04X ", device_address);
164  this->unknown_devices_.insert(device_address);
165  }
166 
167  // Return true to reset buffer
168  return true;
169 }
170 
171 bool UponorSmatrixComponent::send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len) {
172  if (this->address_ == 0 || device_address == 0 || data == nullptr || data_len == 0)
173  return false;
174 
175  // Assemble packet for send queue. All fields are big-endian except for the little-endian checksum.
176  std::vector<uint8_t> packet;
177  packet.reserve(6 + 3 * data_len);
178 
179  packet.push_back(this->address_ >> 8);
180  packet.push_back(this->address_ >> 0);
181  packet.push_back(device_address >> 8);
182  packet.push_back(device_address >> 0);
183 
184  for (int i = 0; i < data_len; i++) {
185  packet.push_back(data[i].id);
186  packet.push_back(data[i].value >> 8);
187  packet.push_back(data[i].value >> 0);
188  }
189 
190  auto crc = crc16(packet.data(), packet.size());
191  packet.push_back(crc >> 0);
192  packet.push_back(crc >> 8);
193 
194  this->tx_queue_.push(packet);
195  return true;
196 }
197 
198 #ifdef USE_TIME
200  if (this->time_device_address_ == 0 || this->time_id_ == nullptr)
201  return false;
202 
203  ESPTime now = this->time_id_->now();
204  if (!now.is_valid())
205  return false;
206 
207  uint8_t year = now.year - 2000;
208  uint8_t month = now.month;
209  // ESPHome days are [1-7] starting with Sunday, Uponor days are [0-6] starting with Monday
210  uint8_t day_of_week = (now.day_of_week == 1) ? 6 : (now.day_of_week - 2);
211  uint8_t day_of_month = now.day_of_month;
212  uint8_t hour = now.hour;
213  uint8_t minute = now.minute;
214  uint8_t second = now.second;
215 
216  uint16_t time1 = (year & 0x7F) << 7 | (month & 0x0F) << 3 | (day_of_week & 0x07);
217  uint16_t time2 = (day_of_month & 0x1F) << 11 | (hour & 0x1F) << 6 | (minute & 0x3F);
218  uint16_t time3 = second;
219 
220  ESP_LOGI(TAG, "Sending local time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
221  now.minute, now.second);
222 
223  UponorSmatrixData data[] = {{UPONOR_ID_DATETIME1, time1}, {UPONOR_ID_DATETIME2, time2}, {UPONOR_ID_DATETIME3, time3}};
224  return this->send(this->time_device_address_, data, sizeof(data) / sizeof(data[0]));
225 }
226 #endif
227 
228 } // namespace uponor_smatrix
229 } // namespace esphome
ESPTime now()
Get the time in the currently defined timezone.
uint16_t year
Definition: date_entity.h:111
void write_array(const uint8_t *data, size_t len)
Definition: uart.h:21
std::string format_hex(const uint8_t *data, size_t length)
Format the byte array data of length len in lowercased hex.
Definition: helpers.cpp:349
A more user-friendly version of struct tm from time.h.
Definition: time.h:17
void add_on_time_sync_callback(std::function< void()> callback)
std::vector< UponorSmatrixDevice * > devices_
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:112
uint8_t minute
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
const char *const TAG
Definition: spi.cpp:8
void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits=1, UARTParityOptions parity=UART_CONFIG_PARITY_NONE, uint8_t data_bits=8)
Check that the configuration of the UART bus matches the provided values and otherwise print a warnin...
Definition: uart.cpp:13
uint8_t second
seconds after the minute [0-60]
Definition: time.h:21
bool read_byte(uint8_t *data)
Definition: uart.h:29
uint8_t hour
std::queue< std::vector< uint8_t > > tx_queue_
uint8_t second
uint8_t minute
minutes after the hour [0-59]
Definition: time.h:23
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:182
bool is_valid() const
Check if this ESPTime is valid (all fields in range and year is greater than 2018) ...
Definition: time.h:61
uint8_t day_of_week
day of the week; sunday=1 [1-7]
Definition: time.h:27
uint16_t year
year
Definition: time.h:35
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
uint8_t month
month; january=1 [1-12]
Definition: time.h:33
bool send(uint16_t device_address, const UponorSmatrixData *data, size_t data_len)
uint8_t hour
hours since midnight [0-23]
Definition: time.h:25
uint8_t day_of_month
day of the month [1-31]
Definition: time.h:29
uint8_t month
Definition: date_entity.h:112