Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
tuning_registry.cpp
Go to the documentation of this file.
1/// @file tuning_registry.cpp
2/// @brief Parameter tables and lookup helpers for the tuning registry.
3/// @ingroup hioc_tuning
4///
5/// The getter/setter lambdas below carry all per-parameter parsing (bandwidth enum
6/// decode, the comma-separated command list with whitespace trimming, the
7/// destination/payload/low-power mapping). Keeping them here makes the hub's tuning
8/// dispatch methods pure table lookups.
9
10#include "tuning_registry.h"
11
12#include "proto_codecs.h" // ADDRESS_SUFFIX_* / encode_broadcast_address() / encode_discovery_address()
13#include "proto_device_model.h" // DeviceType
14
15#include <cstddef>
16#include <iterator>
17
18namespace esphome {
19namespace home_io_control {
20
21// === Numeric parameters (23) ===================================================
22// Each row narrows the incoming float to the field's storage type, matching the
23// original static_cast in update_tuning_number().
24static constexpr TuningNumberParam NUMBER_PARAMS[] = {
25 {"sx1262_response_preamble", [](const TuningConfig &t) { return static_cast<float>(t.sx1262_response_preamble); },
26 [](TuningConfig &t, float v) { t.sx1262_response_preamble = static_cast<uint16_t>(v); }, true},
27 {"sx1262_post_tx_settle_us", [](const TuningConfig &t) { return static_cast<float>(t.sx1262_post_tx_settle_us); },
28 [](TuningConfig &t, float v) { t.sx1262_post_tx_settle_us = static_cast<uint16_t>(v); }, true},
29 {"sx1276_response_preamble", [](const TuningConfig &t) { return static_cast<float>(t.sx1276_response_preamble); },
30 [](TuningConfig &t, float v) { t.sx1276_response_preamble = static_cast<uint16_t>(v); }, true},
31 {"sx1276_discovery_hop_slice_ms",
32 [](const TuningConfig &t) { return static_cast<float>(t.sx1276_discovery_hop_slice_ms); },
33 [](TuningConfig &t, float v) { t.sx1276_discovery_hop_slice_ms = static_cast<uint16_t>(v); }, false},
34 {"sx1262_discovery_hop_slice_ms",
35 [](const TuningConfig &t) { return static_cast<float>(t.sx1262_discovery_hop_slice_ms); },
36 [](TuningConfig &t, float v) { t.sx1262_discovery_hop_slice_ms = static_cast<uint16_t>(v); }, false},
37 {"lr1121_response_preamble", [](const TuningConfig &t) { return static_cast<float>(t.lr1121_response_preamble); },
38 [](TuningConfig &t, float v) { t.lr1121_response_preamble = static_cast<uint16_t>(v); }, true},
39 {"lr1121_post_tx_settle_us", [](const TuningConfig &t) { return static_cast<float>(t.lr1121_post_tx_settle_us); },
40 [](TuningConfig &t, float v) { t.lr1121_post_tx_settle_us = static_cast<uint16_t>(v); }, true},
41 {"lr1121_discovery_hop_slice_ms",
42 [](const TuningConfig &t) { return static_cast<float>(t.lr1121_discovery_hop_slice_ms); },
43 [](TuningConfig &t, float v) { t.lr1121_discovery_hop_slice_ms = static_cast<uint16_t>(v); }, false},
44 // false: unlike sx1262/sx1276/lr1121_response_preamble, this field is not chip-specific and
45 // is never cached into a radio driver via apply_tuning() — key_extraction_responder.cpp reads it
46 // straight out of TuningConfig at TX time, the same way the *_discovery_hop_slice_ms fields
47 // above are read live rather than applied.
48 {"cold_broadcast_reply_preamble",
49 [](const TuningConfig &t) { return static_cast<float>(t.cold_broadcast_reply_preamble); },
50 [](TuningConfig &t, float v) { t.cold_broadcast_reply_preamble = static_cast<uint16_t>(v); }, false},
51 // false: not chip-specific and never cached into a radio driver via apply_tuning() — the
52 // exchange engine reads it straight out of TuningConfig at TX time, the same as
53 // cold_broadcast_reply_preamble above.
54 {"normal_start_preamble", [](const TuningConfig &t) { return static_cast<float>(t.normal_start_preamble); },
55 [](TuningConfig &t, float v) { t.normal_start_preamble = static_cast<uint16_t>(v); }, false},
56 {"low_power_wake_preamble", [](const TuningConfig &t) { return static_cast<float>(t.low_power_wake_preamble); },
57 [](TuningConfig &t, float v) { t.low_power_wake_preamble = static_cast<uint16_t>(v); }, false},
58 {"lbt_max_retries", [](const TuningConfig &t) { return static_cast<float>(t.lbt_max_retries); },
59 [](TuningConfig &t, float v) { t.lbt_max_retries = static_cast<uint8_t>(v); }, false},
60 {"lbt_rssi_threshold_dbm", [](const TuningConfig &t) { return static_cast<float>(t.lbt_rssi_threshold_dbm); },
61 [](TuningConfig &t, float v) { t.lbt_rssi_threshold_dbm = static_cast<int16_t>(v); }, false},
62 {"exchange_start_response_wait_ms",
63 [](const TuningConfig &t) { return static_cast<float>(t.exchange_start_response_wait_ms); },
64 [](TuningConfig &t, float v) { t.exchange_start_response_wait_ms = static_cast<uint16_t>(v); }, false},
65 {"exchange_early_response_wait_ms",
66 [](const TuningConfig &t) { return static_cast<float>(t.exchange_early_response_wait_ms); },
67 [](TuningConfig &t, float v) { t.exchange_early_response_wait_ms = static_cast<uint16_t>(v); }, false},
68 {"exchange_response_wait_ms", [](const TuningConfig &t) { return static_cast<float>(t.exchange_response_wait_ms); },
69 [](TuningConfig &t, float v) { t.exchange_response_wait_ms = static_cast<uint16_t>(v); }, false},
70 {"exchange_total_budget_ms", [](const TuningConfig &t) { return static_cast<float>(t.exchange_total_budget_ms); },
71 [](TuningConfig &t, float v) { t.exchange_total_budget_ms = static_cast<uint16_t>(v); }, false},
72 // false: read straight out of TuningConfig by the hub when a linked remote is heard and when an
73 // operation ends (ADR 0080), never applied to a radio driver.
74 {"remote_activity_poll_delay_ms",
75 [](const TuningConfig &t) { return static_cast<float>(t.remote_activity_poll_delay_ms); },
76 [](TuningConfig &t, float v) { t.remote_activity_poll_delay_ms = static_cast<uint32_t>(v); }, false},
77 {"remote_activity_catchup_poll_ms",
78 [](const TuningConfig &t) { return static_cast<float>(t.remote_activity_catchup_poll_ms); },
79 [](TuningConfig &t, float v) { t.remote_activity_catchup_poll_ms = static_cast<uint32_t>(v); }, false},
80 // false: read straight out of TuningConfig at TX time by PairingEngine::run_discovery_phase_(),
81 // the same as normal_start_preamble/cold_broadcast_reply_preamble above. The getter reports the
82 // effective value (what discovery sends), so the Home Assistant number shows the start preamble
83 // it follows; the setter marks the value as set, and it stays set until the next restart (Home
84 // Assistant has no way back to "follow").
85 {"pairing_discovery_preamble",
86 [](const TuningConfig &t) { return static_cast<float>(effective_discovery_preamble(t)); },
87 [](TuningConfig &t, float v) {
88 t.pairing_discovery_preamble = static_cast<uint16_t>(v);
90 },
91 false},
92 {"pairing_discovery_wait_ms", [](const TuningConfig &t) { return static_cast<float>(t.pairing_discovery_wait_ms); },
93 [](TuningConfig &t, float v) { t.pairing_discovery_wait_ms = static_cast<uint16_t>(v); }, false},
94 {"pairing_discovery_initial_dwell_ms",
95 [](const TuningConfig &t) { return static_cast<float>(t.pairing_discovery_initial_dwell_ms); },
96 [](TuningConfig &t, float v) { t.pairing_discovery_initial_dwell_ms = static_cast<uint16_t>(v); }, false},
97 {"pairing_key_exchange_retries",
98 [](const TuningConfig &t) { return static_cast<float>(t.pairing_key_exchange_retries); },
99 [](TuningConfig &t, float v) { t.pairing_key_exchange_retries = static_cast<uint8_t>(v); }, false},
100 // false: read straight out of TuningConfig by PairingEngine::run_discover_confirm_step_() at
101 // the point of the pause, the same as pairing_discovery_preamble/pairing_discovery_wait_ms above.
102 {"pairing_key_init_delay_ms", [](const TuningConfig &t) { return static_cast<float>(t.pairing_key_init_delay_ms); },
103 [](TuningConfig &t, float v) { t.pairing_key_init_delay_ms = static_cast<uint16_t>(v); }, false},
104 // false: both are read by the key-extraction responder each time it builds a discovery answer.
105 {"key_extraction_type_word", [](const TuningConfig &t) { return static_cast<float>(t.key_extraction_type_word); },
106 [](TuningConfig &t, float v) { t.key_extraction_type_word = static_cast<uint16_t>(v); }, false},
107 {"key_extraction_multi_info", [](const TuningConfig &t) { return static_cast<float>(t.key_extraction_multi_info); },
108 [](TuningConfig &t, float v) { t.key_extraction_multi_info = static_cast<uint8_t>(v); }, false},
109};
110
111// Boolean select rows share one get/set shape (an On/Off toggle over a single TuningConfig
112// field). Template functions, not lambdas: a TuningSelectParam's get/set are plain function
113// pointers with no room for a per-row capture, and instantiating on the member pointer keeps
114// each row below a one-liner without duplicating the On/Off parse.
115template<bool TuningConfig::*Field> std::string bool_select_get(const TuningConfig &t) {
116 return t.*Field ? "On" : "Off";
117}
118template<bool TuningConfig::*Field> bool bool_select_set(TuningConfig &t, const std::string &v) {
119 if (v != "On" && v != "Off")
120 return false;
121 t.*Field = (v == "On");
122 return true;
123}
124
125// === Select parameters (13) ====================================================
126// The setters return false only when the option string is unparseable AND leaving the
127// value untouched is the intended behavior (currently the bandwidth enum and the boolean
128// rows). The destination/payload setters return false for unrecognized values so no radio
129// re-apply is triggered; that has no observable effect since neither applies to the radio.
130static constexpr TuningSelectParam SELECT_PARAMS[] = {
131 {"sx1262_rx_bandwidth", [](const TuningConfig &t) { return sx1262_bandwidth_to_string(t.sx1262_rx_bandwidth); },
132 [](TuningConfig &t, const std::string &v) -> bool {
133 auto bw = sx1262_bandwidth_from_string(v);
134 if (!bw.has_value())
135 return false;
136 t.sx1262_rx_bandwidth = bw.value();
137 return true;
138 },
139 true},
140 {"sx1276_rx_bandwidth", [](const TuningConfig &t) { return sx1276_bandwidth_to_string(t.sx1276_rx_bandwidth); },
141 [](TuningConfig &t, const std::string &v) -> bool {
142 auto bw = sx1276_bandwidth_from_string(v);
143 if (!bw.has_value())
144 return false;
145 t.sx1276_rx_bandwidth = bw.value();
146 return true;
147 },
148 true},
149 {"lr1121_rx_bandwidth", [](const TuningConfig &t) { return lr1121_bandwidth_to_string(t.lr1121_rx_bandwidth); },
150 [](TuningConfig &t, const std::string &v) -> bool {
151 auto bw = lr1121_bandwidth_from_string(v);
152 if (!bw.has_value())
153 return false;
154 t.lr1121_rx_bandwidth = bw.value();
155 return true;
156 },
157 true},
160 {"pairing_discovery_commands",
161 [](const TuningConfig &t) { return discovery_commands_to_csv(t.pairing_discovery_commands); },
162 [](TuningConfig &t, const std::string &v) -> bool {
164 // Support both individual commands and comma-separated preset strings.
165 std::size_t start = 0;
166 while (start < v.size()) {
167 const std::size_t comma = v.find(',', start);
168 std::string token = v.substr(start, comma - start);
169 // Trim whitespace.
170 token.erase(0, token.find_first_not_of(" \t"));
171 token.erase(token.find_last_not_of(" \t") + 1);
172 auto cmd = discovery_command_from_string(token);
173 if (cmd.has_value())
174 t.pairing_discovery_commands.push_back(cmd.value());
175 if (comma == std::string::npos)
176 break;
177 start = comma + 1;
178 }
179 return true;
180 },
181 false},
182 {"pairing_discovery_destination",
183 [](const TuningConfig &t) {
184 return discovery_destination_to_string(t.pairing_discovery_destination_auto,
185 t.pairing_discovery_destination.data());
186 },
187 [](TuningConfig &t, const std::string &v) -> bool {
188 if (v == "auto") {
190 return true;
191 }
192 if (v == "0x00003B") {
194 t.pairing_discovery_destination = {0x00, 0x00, ADDRESS_SUFFIX_DISCOVERY};
195 return true;
196 }
197 if (v == "0x00003F") {
199 t.pairing_discovery_destination = {0x00, 0x00, ADDRESS_SUFFIX_BROADCAST};
200 return true;
201 }
202 // Typed class addresses: the class's broadcast (all subtypes) or discovery form. The option
203 // string goes last because check-tuning-sync.py reads any row in this table that opens with
204 // a string literal as a parameter name.
205 struct TypedDestination {
206 DeviceType type;
207 void (*encode)(DeviceType, uint8_t[NODE_ID_SIZE]);
208 const char *option;
209 };
210 static constexpr TypedDestination TYPED_DESTINATIONS[] = {
213 // Remote controllers: the search an Atlantic Tatou heater sends to receive a key, and the
214 // one a Tatou offering its key answered (#199).
216 };
217 for (const TypedDestination &entry : TYPED_DESTINATIONS) {
218 if (v != entry.option)
219 continue;
220 uint8_t typed[NODE_ID_SIZE] = {0};
221 entry.encode(entry.type, typed);
223 t.pairing_discovery_destination.assign(std::begin(typed), std::end(typed));
224 return true;
225 }
226 return false;
227 },
228 false},
229 {"pairing_discovery_payload",
230 [](const TuningConfig &t) {
231 return discovery_payload_to_string(t.pairing_discovery_payload_enabled, t.pairing_discovery_payload);
232 },
233 [](TuningConfig &t, const std::string &v) -> bool {
234 if (v == "none") {
236 return true;
237 }
238 if (v == "0x00") {
241 return true;
242 }
243 return false;
244 },
245 false},
250 {"scan_power_classes", [](const TuningConfig &t) { return scan_power_classes_to_string(t.scan_power_classes); },
251 [](TuningConfig &t, const std::string &v) -> bool {
252 auto selection = scan_power_classes_from_string(v);
253 if (!selection.has_value())
254 return false;
255 t.scan_power_classes = selection.value();
256 return true;
257 },
258 false},
259 {"pairing_discover_confirm",
260 [](const TuningConfig &t) { return discover_confirm_mode_to_string(t.pairing_discover_confirm); },
261 [](TuningConfig &t, const std::string &v) -> bool {
263 if (!mode.has_value())
264 return false;
265 t.pairing_discover_confirm = mode.value();
266 return true;
267 },
268 false},
269 {"pairing_discovery_listen_channels",
270 [](const TuningConfig &t) { return discovery_listen_channels_to_string(t.pairing_discovery_listen_channels); },
271 [](TuningConfig &t, const std::string &v) -> bool {
272 auto channels = discovery_listen_channels_from_string(v);
273 if (!channels.has_value())
274 return false;
275 t.pairing_discovery_listen_channels = channels.value();
276 return true;
277 },
278 false},
279 // false: read straight out of TuningConfig by PairingEngine::discover_and_pair() when the key
280 // step starts, so a change applies to the next pairing attempt without a re-apply.
281 {"pairing_key_transfer", [](const TuningConfig &t) { return key_transfer_mode_to_string(t.pairing_key_transfer); },
282 [](TuningConfig &t, const std::string &v) -> bool {
283 auto mode = key_transfer_mode_from_string(v);
284 if (!mode.has_value())
285 return false;
286 t.pairing_key_transfer = mode.value();
287 return true;
288 },
289 false},
290};
291
292const TuningNumberParam *find_tuning_number(const std::string &name) {
293 for (const auto &param : NUMBER_PARAMS) {
294 if (name == param.name)
295 return &param;
296 }
297 return nullptr;
298}
299
300const TuningSelectParam *find_tuning_select(const std::string &name) {
301 for (const auto &param : SELECT_PARAMS) {
302 if (name == param.name)
303 return &param;
304 }
305 return nullptr;
306}
307
312
313} // namespace home_io_control
314} // namespace esphome
std::string discovery_commands_to_csv(const std::vector< DiscoveryCommand > &commands)
Format the ordered discovery command list as a UI/preset option string.
std::optional< LR1121RxBandwidth > lr1121_bandwidth_from_string(const std::string &value)
Convert a YAML LR1121 bandwidth string to the enum value.
const TuningNumberParam * tuning_number_params_end()
void encode_broadcast_address(DeviceType type, uint8_t out[NODE_ID_SIZE])
Encode a device type into its typed-broadcast destination address — the exact inverse of broadcast_ta...
std::string bool_select_get(const TuningConfig &t)
const TuningSelectParam * tuning_select_params_end()
std::string discovery_payload_to_string(bool payload_enabled, uint8_t payload)
Format a payload option for YAML/logs.
std::optional< DiscoveryCommand > discovery_command_from_string(const std::string &value)
Parse a discovery command string (e.g., "0x28") into the enum.
std::optional< DiscoverConfirmMode > discover_confirm_mode_from_string(const std::string &value)
Parse a pairing_discover_confirm string into the enum.
std::string lr1121_bandwidth_to_string(LR1121RxBandwidth bw)
Format an LR1121 bandwidth enum as its YAML/UI option string (bare kHz number).
std::string key_transfer_mode_to_string(KeyTransferMode value)
Render a KeyTransferMode as its YAML/select option string.
std::optional< ScanPowerClasses > scan_power_classes_from_string(const std::string &value)
Parse a scan_power_classes string into the enum.
DeviceType
IO-Homecontrol node types.
@ REMOTE_CONTROLLER
Remote controller (all ten type bits set, packed bytes FF C0).
std::optional< KeyTransferMode > key_transfer_mode_from_string(const std::string &value)
Parse a KeyTransferMode from its YAML/select option string.
bool bool_select_set(TuningConfig &t, const std::string &v)
std::optional< SX1262RxBandwidth > sx1262_bandwidth_from_string(const std::string &value)
Convert a YAML bandwidth string to the enum value.
std::string sx1276_bandwidth_to_string(SX1276RxBandwidth bw)
Format an SX1276 bandwidth enum as its YAML/UI option string (bare kHz number).
void encode_discovery_address(DeviceType type, uint8_t out[NODE_ID_SIZE])
Encode a device type into its typed discovery address, the inverse of discovery_target_type().
std::optional< DiscoveryListenChannels > discovery_listen_channels_from_string(const std::string &value)
Parse a DiscoveryListenChannels from its YAML/select option string.
const TuningNumberParam * tuning_number_params_begin()
std::string sx1262_bandwidth_to_string(SX1262RxBandwidth bw)
Format a bandwidth enum as its YAML/UI option string (bare kHz number, e.g.
const TuningSelectParam * find_tuning_select(const std::string &name)
Look up a select tuning parameter by name; returns nullptr if unknown.
const TuningSelectParam * tuning_select_params_begin()
std::string discovery_listen_channels_to_string(DiscoveryListenChannels value)
Render a DiscoveryListenChannels as its YAML/select option string.
static constexpr TuningSelectParam SELECT_PARAMS[]
const TuningNumberParam * find_tuning_number(const std::string &name)
Look up a numeric tuning parameter by name; returns nullptr if unknown.
static constexpr TuningNumberParam NUMBER_PARAMS[]
uint16_t effective_discovery_preamble(const TuningConfig &cfg)
The preamble length (bytes) of the pairing discovery broadcast (0x28, 0x2E).
std::optional< SX1276RxBandwidth > sx1276_bandwidth_from_string(const std::string &value)
Convert a YAML SX1276 bandwidth string to the enum value.
std::string discovery_destination_to_string(bool destination_auto, const uint8_t destination[NODE_ID_SIZE])
Format a destination option for YAML/logs.
std::string discover_confirm_mode_to_string(DiscoverConfirmMode value)
Format a DiscoverConfirmMode value for YAML/logs.
std::string scan_power_classes_to_string(ScanPowerClasses value)
Format a ScanPowerClasses value for YAML/logs.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol device-type model, capabilities and runtime device state.
All runtime tunable parameters for pairing and radio diagnostics.
uint16_t pairing_discovery_initial_dwell_ms
Initial dwell on CH2 before discovery hopping begins.
bool pairing_discovery_destination_auto
When true, map commands to conventional addresses.
SX1276RxBandwidth sx1276_rx_bandwidth
SX1276 RX bandwidth selector.
uint16_t exchange_early_response_wait_ms
Reply window of every attempt before the last when a mains target is asked more than once (decisions:...
uint16_t normal_start_preamble
Preamble for a directed start frame to a non-low-power target.
int16_t lbt_rssi_threshold_dbm
LBT channel-free threshold (dBm).
SX1262RxBandwidth sx1262_rx_bandwidth
SX1262 RX bandwidth selector.
uint32_t remote_activity_catchup_poll_ms
Read every linked device this long after the last exchange of a burst of linked-remote activity; 0 = ...
uint16_t low_power_wake_preamble
Wake-up preamble (bytes) on a directed start frame to a low_power target believed at rest.
uint16_t key_extraction_type_word
Packed type word the key-extraction 0x29 advertises to an open discovery (type << 6 | subtype).
uint16_t cold_broadcast_reply_preamble
Preamble for a start-flagged key-extraction broadcast reply (0x29).
std::vector< uint8_t > pairing_discovery_destination
Destination when auto=false.
std::vector< DiscoveryCommand > pairing_discovery_commands
Ordered discovery commands.
DiscoveryListenChannels pairing_discovery_listen_channels
Channels the discovery response wait covers.
uint16_t lr1121_discovery_hop_slice_ms
Per-channel dwell while LR1121 discovery hops.
uint16_t sx1276_response_preamble
SX1276 response preamble in bytes.
uint16_t exchange_response_wait_ms
Reply window for continuation frames and the post-auth final response.
uint16_t sx1262_post_tx_settle_us
Delay after SX1262 TX before RX (µs).
uint8_t pairing_key_exchange_retries
Retries for the authenticated key exchange phase.
uint16_t pairing_key_init_delay_ms
Pause after the discover-confirm step, before CMD_KEY_INIT (0x31).
uint32_t remote_activity_poll_delay_ms
Delay from a linked-remote press (not a STOP) to the first status poll of its devices.
DiscoverConfirmMode pairing_discover_confirm
Whether/how to send CMD_DISCOVER_CONFIRM (0x2C) during pairing.
uint16_t sx1276_discovery_hop_slice_ms
Per-channel dwell while SX1276 discovery hops.
uint16_t exchange_start_response_wait_ms
Reply window for a start frame (wakes a sleeping device).
KeyTransferMode pairing_key_transfer
Push our key, or pull the device's key.
uint16_t sx1262_response_preamble
SX1262 response preamble in bytes.
bool pairing_discovery_preamble_set
True when pairing_discovery_preamble was set, from YAML or by a write through the Home Assistant numb...
uint16_t pairing_discovery_preamble
Preamble (bytes) of the discovery broadcast (0x28/0x2E) when the user chose one.
uint16_t pairing_discovery_wait_ms
Total wait window after sending discovery commands.
bool pairing_discovery_payload_enabled
Whether the optional payload is enabled.
LR1121RxBandwidth lr1121_rx_bandwidth
LR1121 RX bandwidth selector.
uint16_t lr1121_post_tx_settle_us
Delay after LR1121 TX before RX (µs).
uint8_t lbt_max_retries
LBT retries before forced TX.
uint16_t sx1262_discovery_hop_slice_ms
Per-channel dwell while SX1262 discovery hops.
uint16_t exchange_total_budget_ms
Ceiling on one whole exchange, retries included.
uint16_t lr1121_response_preamble
LR1121 response preamble in bytes.
uint8_t pairing_discovery_payload
Optional payload byte (used for 0x2E).
ScanPowerClasses scan_power_classes
Power classes the scan_paired_devices roll-call calls.
uint8_t key_extraction_multi_info
Multi Information Byte the key-extraction 0x29 advertises.
One numeric tuning parameter: its wire name plus accessors over TuningConfig.
One select tuning parameter: its wire name plus string accessors over TuningConfig.
Table-driven registry of runtime tuning parameters.