14#include "esphome/core/application.h"
15#include "esphome/core/hal.h"
32constexpr const char *PROBE_NAME_PRIVATE_FN =
"private_fn";
33constexpr const char *PROBE_NAME_PRIVATE_FN_SUB =
"private_fn_sub";
34constexpr const char *PROBE_NAME_STATUS_EXT =
"status_ext";
35constexpr const char *PROBE_NAME_STATUS_EXT_FN6 =
"status_ext_fn6";
36constexpr const char *PROBE_NAME_STATUS_EXT_FN9 =
"status_ext_fn9";
37constexpr const char *PROBE_NAME_GET_INFO1 =
"get_info1";
38constexpr const char *PROBE_NAME_GET_INFO2 =
"get_info2";
39constexpr const char *PROBE_NAME_GENERAL_INFO3 =
"general_info3";
40constexpr const char *PROBE_NAME_PRIVATE2 =
"private2";
41constexpr const char *PROBE_NAME_PRIVATE2_SHORT =
"private2_short";
42constexpr const char *PROBE_NAME_STATUS_MP_FP =
"status_mp_fp";
43constexpr const char *PROBE_NAME_STATUS_FP_HIGH =
45constexpr const char *PROBE_NAME_LIMITATION =
49constexpr uint8_t PROBE_LIMITATION_MAX_INDEX = 1;
50constexpr uint8_t PROBE_LIMITATION_INDEX_MINIMUM = 0;
51constexpr const char *PROBE_NAME_SUBSCRIBE_EVENT =
53constexpr const char *PROBE_NAME_READ_OBJECT =
55constexpr const char *PROBE_NAME_REGISTER_00 =
57constexpr const char *PROBE_NAME_REGISTER_01 =
60constexpr uint8_t PROBE_REGISTER_HEADER_LENGTH = 4;
61constexpr uint8_t PROBE_REGISTER_HIGH_00 = 0x00;
62constexpr uint8_t PROBE_REGISTER_HIGH_01 =
65constexpr uint8_t PROBE_READ_OBJECT_MAX_INDEX = TRANSFER_READABLE_OBJECT_COUNT - 1;
69constexpr uint32_t PROBE_READ_OBJECT_BLOCK_GAP_MS = 650;
76constexpr uint16_t PROBE_READ_OBJECT_MAX_BYTES = 24 * TRANSFER_BLOCK_SIZE;
78constexpr uint8_t PROBE_READ_OBJECT_LOG_BYTES_PER_LINE = 24;
80constexpr size_t PROBE_READ_OBJECT_HEADER_BUFFER_SIZE = 72;
82constexpr size_t PROBE_EVENT_HEADER_BUFFER_SIZE = 40;
84constexpr uint8_t PROBE_SUBSCRIBE_EVENT_MAX_INDEX = 1;
85constexpr uint8_t PROBE_SUBSCRIBE_EVENT_INDEX_BATTERY = 0;
86constexpr const char *PROBE_NAME_UNSUBSCRIBE_EVENT =
92constexpr uint8_t PROBE_EXT_FUNCTION_ID_06 = 0x06;
93constexpr uint8_t PROBE_EXT_FUNCTION_ID_09 = 0x09;
95constexpr uint8_t PROBE_PRIVATE_FN_SUB_FUNCTION_ID = 0x09;
99constexpr uint8_t PROBE_STATUS_EXT_SELECTOR = 0x80;
102constexpr uint8_t PROBE_SWEEP_MAX_INDICES = 16;
106constexpr uint32_t PROBE_SWEEP_DELAY_MS = 1000;
113using ProbeBuilderFn = bool (*)(
IoFrame &,
const uint8_t *,
const uint8_t *, uint8_t index,
bool low_power);
115bool build_probe_private_fn(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
118bool build_probe_private_fn_sub(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
121bool build_probe_status_ext(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
124bool build_probe_status_ext_fn6(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
127bool build_probe_status_ext_fn9(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
130bool build_probe_get_info1(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
133bool build_probe_get_info2(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
136bool build_probe_general_info3(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
139bool build_probe_private2_long(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
142bool build_probe_private2_short(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
145bool build_probe_status_mp_fp(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
150bool build_probe_status_fp_high(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
155bool status_fp_high_bitmap_valid(uint8_t bitmap) {
157 for (uint8_t v = bitmap; v != 0; v >>= 1)
159 return bits >= 1 && bits <= STATUS_MP_FP_PARAM_COUNT;
162bool build_probe_limitation(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
163 if (index > PROBE_LIMITATION_MAX_INDEX) {
173bool build_probe_subscribe_event(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
174 if (index > PROBE_SUBSCRIBE_EVENT_MAX_INDEX)
176 const uint16_t
event = index == PROBE_SUBSCRIBE_EVENT_INDEX_BATTERY ? EVENT_BATTERY_LEVEL_CHANGE : EVENT_LOCAL_SENSOR;
182bool build_probe_unsubscribe_event(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
183 if (index > PROBE_SUBSCRIBE_EVENT_MAX_INDEX)
185 const uint16_t
event = index == PROBE_SUBSCRIBE_EVENT_INDEX_BATTERY ? EVENT_BATTERY_LEVEL_CHANGE : EVENT_LOCAL_SENSOR;
191bool build_probe_read_object(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
192 if (index > PROBE_READ_OBJECT_MAX_INDEX)
200bool build_probe_register_read(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t reg_hi, uint8_t index,
207bool build_probe_register_00(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
208 return build_probe_register_read(f, own, dst, PROBE_REGISTER_HIGH_00, index, low_power);
211bool build_probe_register_01(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
212 return build_probe_register_read(f, own, dst, PROBE_REGISTER_HIGH_01, index, low_power);
220struct ProbeDescriptor {
224 ProbeBuilderFn builder;
234 const char *index_help{
nullptr};
235 bool needs_index{
true};
237 uint8_t max_index{UINT8_MAX};
238 bool reads_blocks{
false};
240 bool register_read{
false};
243 bool (*index_valid)(uint8_t){
nullptr};
251constexpr ProbeDescriptor PROBE_TABLE[] = {
252 {.name = PROBE_NAME_PRIVATE_FN, .builder = build_probe_private_fn, .sweepable =
true},
253 {.name = PROBE_NAME_PRIVATE_FN_SUB, .builder = build_probe_private_fn_sub, .sweepable =
true},
254 {.name = PROBE_NAME_STATUS_EXT, .builder = build_probe_status_ext, .sweepable =
true},
255 {.name = PROBE_NAME_STATUS_EXT_FN6, .builder = build_probe_status_ext_fn6, .sweepable =
true},
256 {.name = PROBE_NAME_STATUS_EXT_FN9, .builder = build_probe_status_ext_fn9, .sweepable =
true},
257 {.name = PROBE_NAME_GET_INFO1, .builder = build_probe_get_info1, .sweepable =
true, .needs_index =
false},
258 {.name = PROBE_NAME_GET_INFO2, .builder = build_probe_get_info2, .sweepable =
true, .needs_index =
false},
259 {.name = PROBE_NAME_GENERAL_INFO3, .builder = build_probe_general_info3, .sweepable =
true, .needs_index =
false},
260 {.name = PROBE_NAME_PRIVATE2, .builder = build_probe_private2_long, .sweepable =
true},
261 {.name = PROBE_NAME_PRIVATE2_SHORT, .builder = build_probe_private2_short, .sweepable =
true},
262 {.name = PROBE_NAME_STATUS_MP_FP, .builder = build_probe_status_mp_fp, .sweepable =
true, .needs_index =
false},
264 {.name = PROBE_NAME_STATUS_FP_HIGH,
265 .builder = build_probe_status_fp_high,
267 .index_help =
"FP9-FP16 bitmap, bit 7 = FP9 ... bit 0 = FP16, at most 7 bits (e.g. 0x43 = FP10, FP15, FP16)",
268 .index_valid = status_fp_high_bitmap_valid},
269 {.name = PROBE_NAME_LIMITATION,
270 .builder = build_probe_limitation,
272 .index_help =
"index 0 (minimum) or 1 (maximum)",
273 .max_index = PROBE_LIMITATION_MAX_INDEX},
276 {.name = PROBE_NAME_SUBSCRIBE_EVENT,
277 .builder = build_probe_subscribe_event,
279 .index_help =
"index 0 (battery level event 0x2001) or 1 (local sensor event 0x2005)",
280 .max_index = PROBE_SUBSCRIBE_EVENT_MAX_INDEX},
281 {.name = PROBE_NAME_UNSUBSCRIBE_EVENT,
282 .builder = build_probe_unsubscribe_event,
284 .index_help =
"index 0 (battery level event 0x2001) or 1 (local sensor event 0x2005)",
285 .max_index = PROBE_SUBSCRIBE_EVENT_MAX_INDEX},
288 {.name = PROBE_NAME_READ_OBJECT,
289 .builder = build_probe_read_object,
291 .index_help =
"index 0 (actuator capacity 0x8100), 1 (actuator state 0x8103), 2 (name record 0x8003), 3 (general "
292 "info 1, 0x8001), 4 (general info 2, 0x8002), or 5 to 29 (further objects, see the diagnostic "
293 "probes documentation)",
294 .max_index = PROBE_READ_OBJECT_MAX_INDEX,
295 .reads_blocks =
true},
298 {.name = PROBE_NAME_REGISTER_00, .builder = build_probe_register_00, .sweepable =
true, .register_read =
true},
299 {.name = PROBE_NAME_REGISTER_01, .builder = build_probe_register_01, .sweepable =
true, .register_read =
true},
301constexpr uint8_t PROBE_TABLE_SIZE = std::size(PROBE_TABLE);
305consteval bool probe_table_consistent() {
306 return std::ranges::all_of(PROBE_TABLE, [](
const ProbeDescriptor &row) {
307 const bool restricted = row.max_index < UINT8_MAX || row.index_valid !=
nullptr;
308 return restricted == (row.index_help !=
nullptr);
311static_assert(probe_table_consistent(),
"a probe row has index_help exactly when its index is restricted");
315const ProbeDescriptor *find_probe_descriptor(
const std::string &probe) {
316 for (
const auto &descriptor : PROBE_TABLE) {
317 if (probe == descriptor.name)
325std::string unknown_probe_message(
const std::string &probe) {
327 for (uint8_t i = 0; i < PROBE_TABLE_SIZE; i++) {
329 names += (i + 1 == PROBE_TABLE_SIZE) ?
", or " :
", ";
330 names += PROBE_TABLE[i].name;
332 return "unknown probe \"" + probe +
"\" (expected " + names +
")";
336std::string index_out_of_range_message(
const ProbeDescriptor &descriptor) {
337 return "probe \"" + std::string(descriptor.name) +
"\" takes " + descriptor.index_help;
341bool probe_index_in_range(
const ProbeDescriptor &descriptor, uint8_t index) {
342 if (index > descriptor.max_index)
344 return descriptor.index_valid ==
nullptr || descriptor.index_valid(index);
352const ProbeDescriptor *resolve_probe_descriptor(
const std::string &probe,
ManagementActionResult &result) {
353 const ProbeDescriptor *descriptor = find_probe_descriptor(probe);
354 if (descriptor ==
nullptr)
355 result.message = unknown_probe_message(probe);
360std::string hex_range(
const uint8_t *data, uint16_t first, uint16_t end) {
362 for (uint16_t i = first; i < end; i++) {
380bool parse_probe_index(
const std::string &text, uint8_t &out) {
387 if (text.size() > 1 && text[0] ==
'0' && text[1] !=
'x' && text[1] !=
'X')
394 const uint32_t value = std::strtoul(text.c_str(), &end, 0);
395 if (end != text.c_str() + text.size())
397 if (errno == ERANGE || value > std::numeric_limits<uint8_t>::max())
399 out =
static_cast<uint8_t
>(value);
406bool register_probe_refused(
const ProbeDescriptor &descriptor,
const IoDevice &dev,
const std::string &probe,
410 result.message =
"probe \"" + probe +
411 "\" reads registers and only applies to a register-protocol heater or a domestic hot water "
412 "device; this device is neither";
413 result.terminal_refusal =
true;
420void report_register_reply(
const ProbeDescriptor &descriptor,
const IoFrame &request,
const IoFrame &response,
422 if (!descriptor.register_read)
424 const bool echoed = response.cmd == CMD_WRITE_PRIVATE_ACK && response.data_len >= PROBE_REGISTER_HEADER_LENGTH &&
425 std::memcmp(response.data, request.data, PROBE_REGISTER_HEADER_LENGTH) == 0;
428 for (uint8_t i = PROBE_REGISTER_HEADER_LENGTH; i < response.data_len; i++)
429 value += (i == PROBE_REGISTER_HEADER_LENGTH ?
"" :
" ") +
format_hex_byte(response.data[i]);
430 result.decoded =
"value: " + value;
431 result.message +=
" [" + result.decoded +
"]";
433 ESP_LOGI(
detail::TAG,
"%s register 0x%02X%02X on %s: %s", probe.c_str(), request.data[2], request.data[3],
434 result.device_id.c_str(), echoed ? result.decoded.c_str() :
command_name(response.cmd));
441 : node_id_(node_id), engine_(engine), registry_(registry), initialized_(initialized), hub_(std::move(hub)) {}
444 const std::string &index) {
451 if (dev ==
nullptr) {
454 return resolve_result;
461 if (!hub_.probes_enabled()) {
462 result.
message =
"diagnostic probes are not enabled; set diagnostic_probes: true in YAML";
469 result.
message =
"device is moving; refusing to probe mid-transaction";
474 const ProbeDescriptor *descriptor = resolve_probe_descriptor(probe, result);
475 if (descriptor ==
nullptr)
478 if (register_probe_refused(*descriptor, *dev, probe, result))
481 uint8_t index_byte = 0;
482 if (descriptor->needs_index && !parse_probe_index(index, index_byte)) {
483 result.
message =
"index must be a decimal or 0x-prefixed byte value (0-255)";
486 if (descriptor->needs_index && !probe_index_in_range(*descriptor, index_byte)) {
487 result.
message = index_out_of_range_message(*descriptor);
492 if (!descriptor->builder(request, node_id_, dev->node_id, index_byte, dev->low_power)) {
493 result.
message =
"failed to build probe request";
496 if (request.
cmd == CMD_SUBSCRIBE_EVENT && request.
data_len == 3) {
497 ESP_LOGI(
detail::TAG,
"%s %s %s event 0x%02X%02X (diagnostic)",
498 request.
data[0] == EVENT_SUBSCRIPTION_UNSUBSCRIBE ?
"Unsubscribing" :
"Subscribing",
499 result.
device_id.c_str(), request.
data[0] == EVENT_SUBSCRIPTION_UNSUBSCRIBE ?
"from" :
"to",
507 const ExchangeOutcome outcome = engine_.send_and_receive(request, response, FREQ_CH2);
509 engine_.log_debug(result.
device_id.c_str());
511 ?
"device accepted the probe request but sent no response"
512 :
"no reply after " + std::to_string(EXCHANGE_RETRY_COUNT) +
513 " attempts (device asleep, unreachable, or silently ignoring this opcode)";
521 uint8_t raw[FRAME_MAX_SIZE] = {0};
522 const uint8_t raw_len =
serialize(response, raw,
sizeof(raw));
553 char header[PROBE_EVENT_HEADER_BUFFER_SIZE];
554 snprintf(header,
sizeof(header),
"event 0x%04X, %u data bytes",
static_cast<unsigned>(event.
event_id),
555 static_cast<unsigned>(event.
data_len));
559 report_register_reply(*descriptor, request, response, probe, result);
561 if (descriptor->reads_blocks)
562 read_object_blocks_(*dev, response, TRANSFER_READABLE_OBJECTS[index_byte], result);
565 if (response.
cmd == CMD_LIMITATION_STATUS_RESP) {
574 const uint16_t object_id =
object.id;
576 if (open_reply.
cmd != CMD_TRANSFER_OPEN_RESP)
580 result.
message +=
" [not a well-formed transfer-open answer]";
583 char head[PROBE_READ_OBJECT_HEADER_BUFFER_SIZE];
584 snprintf(head,
sizeof(head),
"object 0x%04X class %u handle 0x%02X status %u length %u",
585 static_cast<unsigned>(object_id),
static_cast<unsigned>(
object.object_class),
586 static_cast<unsigned>(open.
handle),
static_cast<unsigned>(open.
status),
static_cast<unsigned>(open.
length));
589 if (open.
status == TRANSFER_STATUS_NOTHING_TO_READ && open.
length == 0) {
590 result.
message +=
" [object is empty (status 7)]";
594 result.
message +=
" [device refused the read]";
598 result.
message +=
" [object is empty]";
604 const auto pause_before_request = []() {
606 delay(PROBE_READ_OBJECT_BLOCK_GAP_MS);
608 const auto close_transfer = [&]() {
609 IoFrame close_request;
611 pause_before_request();
613 engine_.send_and_receive(close_request, close_reply, FREQ_CH2);
615 if (open.
length > PROBE_READ_OBJECT_MAX_BYTES) {
617 result.
message +=
" [longer than " + std::to_string(PROBE_READ_OBJECT_MAX_BYTES) +
" bytes, closed unread]";
621 uint8_t data[PROBE_READ_OBJECT_MAX_BYTES] = {0};
624 bool complete =
true;
625 for (uint16_t chunk = chunks; chunk >= 1; chunk--) {
628 pause_before_request();
634 const uint8_t *block =
nullptr;
635 uint8_t block_len = 0;
639 have + block_len > PROBE_READ_OBJECT_MAX_BYTES) {
643 std::memcpy(data + have, block, block_len);
644 have =
static_cast<uint16_t
>(have + block_len);
648 const std::string hex = hex_range(data, 0, have);
650 ESP_LOGI(
detail::TAG,
"read_object %s object 0x%04X class %u: %u of %u bytes%s", result.
device_id.c_str(),
651 static_cast<unsigned>(object_id),
static_cast<unsigned>(
object.object_class),
static_cast<unsigned>(have),
652 static_cast<unsigned>(open.
length), complete ?
"" :
" (incomplete)");
654 for (uint16_t i = 0; i < have; i += PROBE_READ_OBJECT_LOG_BYTES_PER_LINE) {
655 ESP_LOGI(
detail::TAG,
" +%03X: %s",
static_cast<unsigned>(i),
656 hex_range(data, i, std::min<uint16_t>(have, i + PROBE_READ_OBJECT_LOG_BYTES_PER_LINE)).c_str());
658 result.
message += (complete ?
" data=" :
" incomplete, data so far=") + hex;
662 const std::string &first_index,
const std::string &last_index) {
677 const ProbeDescriptor *descriptor = resolve_probe_descriptor(probe, result);
678 if (descriptor ==
nullptr)
680 if (!descriptor->sweepable) {
681 result.
message =
"probe \"" + probe +
"\" cannot be swept; use probe_device";
684 if (!descriptor->needs_index) {
685 result.
message =
"probe \"" + probe +
"\" takes no index; use probe_device";
691 if (!parse_probe_index(first_index, first) || !parse_probe_index(last_index, last)) {
692 result.
message =
"first_index/last_index must be decimal or 0x-prefixed byte values (0-255)";
696 result.
message =
"last_index must be >= first_index";
699 if (last > descriptor->max_index) {
700 result.
message = index_out_of_range_message(*descriptor);
703 const uint32_t span =
static_cast<uint32_t
>(last) - first + 1;
704 if (span > PROBE_SWEEP_MAX_INDICES) {
706 "sweep range too wide: " + std::to_string(span) +
" indices, max " + std::to_string(PROBE_SWEEP_MAX_INDICES);
711 uint32_t answered_count = 0;
712 bool stopped_early =
false;
713 for (uint32_t idx = first; idx <= last; idx++) {
716 delay(PROBE_SWEEP_DELAY_MS);
718 const std::string index_str = std::to_string(idx);
720 report +=
"index=0x" +
format_hex_byte(
static_cast<uint8_t
>(idx)) +
": ";
725 report +=
" [" + step.
decoded +
"]";
736 report +=
"(stopping sweep: " + step.
message +
")\n";
737 stopped_early =
true;
746 result.
success = !stopped_early;
752 "]: " + std::to_string(answered_count) +
" answered\n" + report;
Owns the per-hub device table, update callbacks, and linked-remote associations.
DiagnosticProbes(const uint8_t *node_id, ExchangeEngine &engine, DeviceRegistry ®istry, const bool *initialized, DiagnosticProbesHub hub)
Construct with the collaborators a probe exchange needs.
ManagementActionResult probe_sweep(const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index)
Walk a bounded index range, one probe_device() call per index, in one user gesture.
ManagementActionResult probe_device(const std::string &device_id, const std::string &probe, const std::string &index)
Send a single diagnostic probe frame to an already-paired device and report the raw reply.
Opt-in diagnostic probes: single frames and bounded sweeps for protocol research.
Conversions and renderers shared by the hub and its Home Assistant entities.
Hub-layer log tag and log/format helpers shared by the hub and its collaborators.
std::string describe_limitation_reply(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_RESP frame for a log line or probe report.
constexpr const char * TAG
Shared log tag for hub-level messages.
void log_component_capture(const RadioDriver *radio, const char *stage, const uint8_t *buf, uint8_t len, const IoFrame *frame=nullptr)
Log a frame at the "io_capture" tag with structured fields.
constexpr size_t HEATING_PAYLOAD_MAX_SIZE
Largest payload any function produces: a set of a 16-bit value (4-byte header + 2 value bytes).
bool create_write_private(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t *payload, size_t payload_len)
Build a generic CMD_WRITE_PRIVATE (0x20) frame around a caller-supplied payload — the one builder beh...
bool decode_status_update_event(const IoFrame &frame, StatusUpdateEvent &out)
Decode the event header of a CMD_STATUS_UPDATE.
bool create_private2_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t modifier, bool long_form, bool low_power)
Build a CMD_PRIVATE2 (0x0C) request in either of the two field-observed shapes.
bool create_limitation_status_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, LimitationType type, bool low_power)
Build the limitation read (see the header for the evidence and the read-only guarantee).
bool create_transfer_open(IoFrame &f, const uint8_t *own, const uint8_t *dst, const TransferObject &object, bool low_power)
Build the transfer-open request (see the header for the allowlist and the byte layout).
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
bool create_get_info1(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_INFO1 (0x54) request. No payload. See proto_commands.h for the evidence note.
bool apply_error_response(const IoFrame &response, ManagementActionResult &result)
Decode a CMD_ERROR_RESP frame's result code into result.
const char * command_result_description(uint8_t result)
Return a human-readable explanation for a CMD_ERROR_RESP result code.
bool create_general_info3(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_GENERAL_INFO3 (0x58) request.
std::string format_hex_byte(uint8_t value)
Format a byte as two uppercase hex digits, no prefix.
bool decode_transfer_open_reply(const IoFrame &f, TransferOpenReply &out)
Decode a transfer-open reply (layout verified against a VELUX KLR 200 copy session).
bool create_get_info2(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build a CMD_GET_INFO2 (0x56) request. No payload. See proto_commands.h for the evidence note.
void render_frame_hex_redacted(const uint8_t *data, uint8_t len, char *out, size_t out_size)
Render a frame's bytes as spaced hex text, masking the payload when the command carries key material ...
constexpr const char * MANAGEMENT_ACTION_PROBE_DEVICE
Native API action names of the two probe actions, shared with ManagementActions' registration table.
const char * command_result_name(uint8_t result)
Return a stable symbolic name for a CMD_ERROR_RESP result code.
ExchangeOutcome
Authenticated exchange engine — outbound and inbound protocol flows.
@ SUCCESS_WITH_RESPONSE
Device replied; the caller's response frame is populated.
@ SUCCESS_UNCONFIRMED
Device authenticated the request — so it received and accepted it — but sent no final response.
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
bool create_unsubscribe_event(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint16_t event_id, bool low_power)
Build the unsubscribe request (see the header: the action value is untested).
bool create_transfer_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint8_t handle, uint16_t chunk, bool low_power)
Build the transfer-read request (see the header: exactly handle and chunk, no data).
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
uint16_t transfer_chunk_count(uint16_t length)
Number of CMD_UNKNOWN4A_REQ chunks that cover length bytes: ceil(length / TRANSFER_BLOCK_SIZE).
bool create_status_mp_fp_read(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t fpi1, uint8_t fpi2)
Build the KLF-style status read of the main parameter plus functional parameters (see the header for ...
bool create_subscribe_event(IoFrame &f, const uint8_t *own, const uint8_t *dst, uint16_t event_id, bool low_power)
Build the subscribe request (see the header for the allowlist).
size_t encode_heating_register_read(uint8_t reg_hi, uint8_t reg_lo, uint8_t out[HEATING_PAYLOAD_MAX_SIZE])
Encode a read ("get") of an arbitrary register into a CMD_WRITE_PRIVATE (0x20) payload.
IoDevice * resolve_management_device(const char *action, const std::string &device_id, bool initialized, DeviceRegistry ®istry, ManagementActionResult &result)
Shared preamble for every device-addressed action: validate the hub and device ID,...
bool create_private_function(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t function_id, uint8_t sub_index)
Build a CMD_PRIVATE (0x03) request for an arbitrary function ID.
bool decode_transfer_block(const IoFrame &f, uint8_t handle, uint16_t chunk, const uint8_t *&out, uint8_t &out_len)
Decode one CMD_UNKNOWN4A_RESP data block.
bool create_get_status_extended(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, uint8_t selector, uint8_t block, uint8_t function_id)
Build an extended CMD_PRIVATE (0x03) request with a selector/block pair — the shape real hubs use for...
LimitationType
On-air selector byte (request data[0]) choosing which resulting limit to read.
@ MAXIMUM
Resulting maximum limit.
@ MINIMUM
Resulting minimum limit; rain protection raises it (a floor on the position value).
ManagementActionResult make_management_result(const std::string &action, const std::string &device_id)
A result that carries only the action name and the target device ID.
bool device_accepts_register_probe(DeviceType type, uint8_t subtype)
May the diagnostic register-read probes (register_00, register_01) be sent to this device?
std::string normalize_device_id_argument(const std::string &device_id)
Trim and uppercase a device-ID argument as received from the native API.
constexpr const char * MANAGEMENT_ACTION_PROBE_SWEEP
uint8_t serialize(const IoFrame &f, uint8_t *buf, uint8_t buf_size)
Serialize a parsed frame into a wire buffer (without CRC).
Command builders for the IO‑Homecontrol protocol.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
Heating profiles: what a climate card offers for each kind of heater, and how.
The only three things a probe needs from the hub, injected by ManagementActions.
Runtime state of a paired IO‑Homecontrol device.
bool low_power
YAML-declared: this target is a low-power / duty-cycled receiver, so directed frames set CTRL1_LOW_PO...
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes). Never includes mac.
uint8_t data_len
Actual length of data.
Result of a hub-level management action such as rename.
std::string device_id
Target IO-homecontrol device ID.
bool has_response_cmd
True when response_cmd contains a probe reply's command byte.
std::string probe_index
Requested index argument, as received (before parsing).
uint8_t result_code
Optional CMD_ERROR_RESP result byte.
uint8_t response_cmd
Probe reply's command byte (distinct from a CMD_ERROR_RESP result_code above, which is a different by...
bool terminal_refusal
True when probe_device() failed for a reason that will recur identically for every remaining index in...
std::string decoded
Rendering of the probe reply for the replies this codebase can decode (currently a limitation reply,...
std::string probe_name
Probe name for probe_device()/probe_sweep(), e.g. "private_fn".
std::string response_hex
Probe reply's full raw wire hex, for pasting into scripts/corpus/ingest.py.
bool success
Whether the requested management action succeeded.
std::string message
Human-readable outcome summary.
The event header of a well-formed CMD_STATUS_UPDATE payload.
uint8_t data_len
Number of event-data bytes; equals the payload length minus the header.
uint16_t event_id
EVENT_STATUS_CHANGE for a position update, any other id for a device event report.
One object a CMD_TRANSFER_OPEN can name: its class byte and its 16-bit id.
One decoded CMD_TRANSFER_OPEN_RESP payload.
uint8_t status
data[1]; TRANSFER_STATUS_ACCEPTED / _ADAPTED, anything else is an error.
uint16_t length
data[2..3] big-endian: bytes available to read; 0 means nothing to read.
uint8_t handle
data[0]; the first byte of every CMD_UNKNOWN4A_REQ that follows.