14#include "esphome/core/log.h"
35inline constexpr const char *
TAG =
"home_io_control";
57inline uint8_t
round_percent(
float fraction) {
return static_cast<uint8_t
>(std::lround(fraction * 100.0F)); }
124 const char *expected) {
125 ESP_LOGW(
TAG,
"Rejecting %s for device %s: type=%s (%u) class=%s profile=%s expected=%s", operation,
138 const IoFrame *frame =
nullptr) {
145 if (frame !=
nullptr) {
146 ESP_LOGD(
"io_capture",
147 "chip=%s phase=component stage=%s freq=%" PRIu32
" ts=%" PRIu32
148 " len=%u cmd=0x%02X src=%02X%02X%02X dst=%02X%02X%02X payload=%s",
150 frame->src[1], frame->src[2], frame->dst[0], frame->dst[1], frame->dst[2], payload_hex);
153 ESP_LOGD(
"io_capture",
"chip=%s phase=component stage=%s freq=%" PRIu32
" ts=%" PRIu32
" len=%u payload=%s",
164 const IoFrame &frame, uint8_t len) {
167 const bool src_registered = component->
get_device(src_id) !=
nullptr;
168 const bool dst_registered = component->
get_device(dst_id) !=
nullptr;
170 if (src_registered || dst_registered) {
171 ESP_LOGW(
TAG,
"%s issue=%s cmd=%s(0x%02X) src=%s%s dst=%s%s len=%u data_len=%u", direction, reason,
172 command_name(frame.
cmd), frame.
cmd, src_id.c_str(), src_registered ?
" (registered)" :
"", dst_id.c_str(),
173 dst_registered ?
" (registered)" :
"", len, frame.
data_len);
177 ESP_LOGD(
TAG,
"%s issue=%s cmd=%s(0x%02X) src=%s dst=%s len=%u data_len=%u", direction, reason,
197 const char *target_label =
202 if (linked_devices !=
nullptr && !linked_devices->empty()) {
203 suffix =
" (linked →";
204 for (
const auto &dev_id : *linked_devices) {
212 ESP_LOGD(
TAG,
"rx 1W remote %s targets %s: %s(0x%02X) %s originator=%s priority=%s%s", src_id.c_str(), target_label,
218 ESP_LOGD(
TAG,
"rx 1W remote %s targets %s: %s(0x%02X) data_len=%u%s", src_id.c_str(), target_label,
238inline bool is_exposed_sender(
const std::vector<std::string> &exposed_senders,
const std::string &sender_id) {
239 return std::find(exposed_senders.begin(), exposed_senders.end(), sender_id) != exposed_senders.end();
247 std::array<char, NAME_AND_HEX_BUFFER_SIZE> buffer{};
248 std::snprintf(buffer.data(), buffer.size(),
"%s(0x%02X)", name, value);
249 return std::string(buffer.data());
266 return std::string(
"io_device_type: \"") + name +
"\"";
267 std::array<char, LEARNED_DEVICE_TYPE_HEX_BUFFER_SIZE> buffer{};
268 std::snprintf(buffer.data(), buffer.size(),
"io_device_type: 0x%02X",
static_cast<uint8_t
>(type));
269 return std::string(buffer.data());
288 {
"linked", linked ?
"true" :
"false"},
311 snprintf(byte_buf,
sizeof(byte_buf),
"%02X", key[i]);
334 const std::string &message) {
338 const size_t end = message.find(
'\n', start);
339 const std::string line = (end == std::string::npos) ? message.substr(start) : message.substr(start, end - start);
340 const std::string out = first ? prefix + line : line;
342 ESP_LOGW(tag,
"%s", out.c_str());
344 ESP_LOGI(tag,
"%s", out.c_str());
347 if (end == std::string::npos || end + 1 >= message.size())
368 return "not present";
400 std::transform(sender_hex_lower.begin(), sender_hex_lower.end(), sender_hex_lower.begin(),
401 [](
unsigned char c) { return std::tolower(c); });
404 std::string mac_line;
407 mac_line =
"MAC VERIFIED: this frame's MAC checked out under the recovered key -- the strongest evidence "
408 "available on the spot that it is correct.";
411 mac_line =
"MAC FAILED: this frame's MAC did NOT check out under the recovered key -- it is probably wrong. "
412 "Re-arm and repeat the key-copy gesture closer to the hub.";
416 mac_line =
"MAC not present: this frame carried no MAC trailer to verify against -- treat this key as "
417 "unconfirmed until tested.";
422 constexpr size_t manufacturer_line_size = 32;
423 char manufacturer_line[manufacturer_line_size];
424 snprintf(manufacturer_line,
sizeof(manufacturer_line),
" manufacturer: 0x%02X",
427 std::string type_lines;
428 if (observed_type_known) {
430 " # observed from this sender's traffic; verify\n";
432 type_lines =
" # io_device_type: unknown -- no other 1W traffic was observed from this sender while armed;\n"
433 " # check the DEBUG \"rx 1W remote ...\" log line once you see this sender transmit again.\n";
437 "\nThe hub always transmits under its own node_id, never the sender's -- copying the sender's address "
438 "would hijack its rolling sequence counter and break its existing remote.\n"
439 "Copy the block below into your hub's YAML.\n"
440 "oneway_controllers:\n"
441 " # node_id omitted -> derived from your hub node_id; see the boot log\n"
443 sender_hex_lower +
"\n" +
" system_key: \"" + key_hex +
"\"\n" + manufacturer_line +
"\n" + type_lines +
444 " commands: [open, close, stop]";
464 return "SYSTEM KEY EXTRACTED -- DO NOT SHARE YOUR SYSTEM KEY\n"
465 "Anyone with this key and node_id can control every device on this installation.\n"
466 "This exchange has not been independently confirmed against your specific hub -- test\n"
467 "this key (e.g. by controlling a device with it) before relying on it.\n"
468 "Copy the block below into a new hub's YAML.\n"
511 if (radio ==
nullptr)
543 static_cast<uint16_t
>(std::min<uint32_t>(
static_cast<uint32_t
>(dev.
exchange_attempt_count) + tries, UINT16_MAX));
568 "the Command Originator byte is the fourth byte of the last-command record; if one "
569 "offset moves the other must move with it");
627 out +=
" (this hub)";
629 out +=
" (this device)";
677 if (!target_differs && !motion_differs)
679 std::string out =
" [predicted:";
707 bool include_request_cmd =
false) {
709 if (include_request_cmd) {
710 ESP_LOGW(
TAG,
"Device %s: %s (0x%02X) returned %s result=0x%02X %s (%s)",
id.c_str(),
command_name(request_cmd),
715 ESP_LOGW(
TAG,
"Device %s: explicit %s result=0x%02X %s (%s)",
id.c_str(), kind, result,
command_result_name(result),
731 bool include_request_cmd =
false) {
The main IO-Homecontrol component.
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Abstract radio driver for IO-Homecontrol.
const RadioCaptureInfo & get_last_capture() const
Get the most recent radio capture info.
virtual const char * chip_name() const =0
Get a human‑readable chip name.
IO-Homecontrol ESPHome component — protocol controller.
Shared frame logging helpers for IO-Homecontrol.
bool known_device_accepts_execute_tilt(const IoDevice &dev)
Can this device accept a tilt command?
bool known_device_matches_entity_class(const IoDevice &dev, DeviceCapabilityClass expected)
Does the device's type match the expected HA entity class?
void log_rejected_operation(const std::string &device_id, const IoDevice &dev, const char *operation, const char *expected)
Log a rejected operation with capability mismatch details.
bool known_device_accepts_execute_position(const IoDevice &dev, uint8_t position)
Can this device accept an execute (position) command?
std::string format_name_and_hex(const char *name, uint8_t value)
Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
std::map< std::string, std::string > build_sender_event_data(const OneWayFrameInfo &info, bool linked)
Build the Home Assistant event data map for a decoded 1W sender frame.
constexpr const char * TAG
Shared log tag for hub-level messages.
constexpr size_t NAME_AND_HEX_BUFFER_SIZE
Buffer size for format_name_and_hex(): longest command name plus "(0xXX)" and a margin.
constexpr uint8_t PRIVATE_RESPONSE_LAST_COMMAND_OFFSET
Offset of the last-command record within each status-bearing payload.
std::string describe_learned_device_type(DeviceType type)
Build the YAML line to add once a device's type is learned at runtime.
constexpr float BINARY_ENTITY_ON_POSITION_THRESHOLD
Shared 0-100 cutoff: values below this mean binary "on".
bool known_device_supports_status_requests(const IoDevice &dev)
Does the device support status requests?
void apply_last_command_record(IoDevice &dev, const LastCommandRecord &record)
Store a decoded record on the device, if it is valid.
void log_multiline_result(const char *tag, bool is_warning, const std::string &prefix, const std::string &message)
Log prefix followed by message, one line per log call rather than one call for the whole (possibly mu...
void log_1w_remote_frame(const OneWayFrameInfo &info, const std::vector< std::string > *linked_devices=nullptr)
Log an already-decoded 1W remote frame at DEBUG level.
void record_exchange_timeout(IoDevice &dev, uint8_t tries)
Record that an outbound exchange to this device timed out (no valid response).
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.
std::string describe_status_update_originator(const IoFrame &frame)
Describe a 0x71 status update's Command Originator as "name(0xXX)".
std::string describe_last_command_source(const IoDevice &dev)
Render the "Last Command Source" sensor string.
bool is_exposed_sender(const std::vector< std::string > &exposed_senders, const std::string &sender_id)
Whether a 1W sender is on the exposed_senders allowlist for the sender HA event.
constexpr const char * ONEWAY_SENDER_EVENT
Home Assistant event fired when a decoded 1W frame carries a command intent from an exposed sender (a...
void update_link_health(IoDevice &dev, RadioDriver *radio)
Update per-device link-health stats from the radio's last capture.
uint8_t round_percent(float fraction)
Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
constexpr uint8_t STATUS_UPDATE_LAST_COMMAND_OFFSET
std::string build_key_extraction_report(const uint8_t node_id[NODE_ID_SIZE], const uint8_t key[AES_KEY_SIZE])
Build the ready-to-paste 2W system-key-extraction report: node_id:/system_key: as a home_io_control: ...
std::string format_key_hex(const uint8_t key[AES_KEY_SIZE])
Format a 16-byte key as an uppercase, unseparated hex string for display.
void normalize_stopped_state(IoDevice &dev)
Normalize stopped state: some devices briefly report stopped before target/current converge.
bool is_binary_entity_position(uint8_t position)
Is the given position value an on/off binary encoding?
constexpr uint32_t ONEWAY_DEDUP_WINDOW_MS
Suppress a repeated 1W log/poll for the same remote and the same intent within this window.
const char * oneway_mac_status_name(OneWayMacStatus status)
Human-readable name for a decoded 0x30's MAC-verification outcome.
void log_status_update(const std::string &id, const IoDevice &dev, const char *suffix="")
Log a concise status‑update line used by inbound handlers.
constexpr uint8_t STATUS_UPDATE_ORIGINATOR_OFFSET
Offset of the Command Originator byte in a CMD_STATUS_UPDATE (0x71) payload.
constexpr size_t LEARNED_DEVICE_TYPE_HEX_BUFFER_SIZE
Buffer size for describe_learned_device_type()'s hex fallback: "io_device_type: 0xXX" plus margin.
std::string describe_last_commander(const IoDevice &dev, const uint8_t *hub_node_id)
Render the "Last Commanded By" sensor string.
void log_frame_issue(IOHomeControlComponent *component, const char *direction, const char *reason, const IoFrame &frame, uint8_t len)
Log a frame‑level issue (unregistered endpoints, unsupported commands).
std::string describe_prediction(const IoDevice &dev)
Describe the hub's live optimistic predictions where they disagree with the observation.
void clear_command_result(IoDevice &dev)
Clear a previously recorded CMD_ERROR_RESP result, if any.
void log_command_result(const std::string &id, uint8_t result, uint8_t request_cmd=0, bool include_request_cmd=false)
Log a decoded CMD_ERROR_RESP result with optional request-command context.
LastCommandRecord decode_last_command_record(const IoFrame &frame, uint8_t base)
Decode the last-command record at base from a status-bearing payload.
void record_command_result(IoDevice &dev, const std::string &id, uint8_t result, uint8_t request_cmd=0, bool include_request_cmd=false)
Store a decoded CMD_ERROR_RESP result on the device and log it.
std::string build_oneway_adoption_report(const OneWayAdoptedKey &adopted, bool observed_type_known, DeviceType observed_type)
Build the full 1W controller-key-adoption report: MAC-verification status, the own-address transmissi...
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
std::string format_device_type_for_yaml(DeviceType type)
Build the YAML value for a device's io_device_type key.
OneWayMacStatus
Outcome of checking the out-of-length MAC trailer (IoFrame::has_mac) on a decoded CMD_ONEWAY_ADD_CONT...
@ VERIFIED
frame.has_mac was true and the MAC verified under the recovered key.
@ NOT_PRESENT
frame.has_mac was false; nothing to verify.
@ FAILED
frame.has_mac was true and the MAC did NOT verify under the recovered key.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
float effective_target(const IoDevice &dev)
The main-position target a consumer should act on: the prediction when one stands,...
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
int16_t rssi_scaled_to_dbm(int16_t scaled)
Convert an rssi_ema_scaled fixed-point value to whole dBm (round half away from zero).
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
bool device_supports_binary_control(DeviceType type)
Does this device type support binary on/off control?
std::string format_position(float pos)
Format a position float as a human‑readable string (e.g.
bool device_supports_lock_control(DeviceType type)
Does this device type support binary lock/unlock control via execute commands?
const char * command_result_description(uint8_t result)
Return a human-readable explanation for a CMD_ERROR_RESP result code.
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 ...
const char * command_result_name(uint8_t result)
Return a stable symbolic name for a CMD_ERROR_RESP result code.
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
static constexpr uint8_t BINARY_ENTITY_ON_POSITION
Position value written for binary ON commands (light on, switch on, lock unlock).
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
const char * acei_level_name(uint8_t level)
Get a human-readable name for an ACEI priority level (0–7).
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
static constexpr int16_t RSSI_EMA_SCALE
EMA weight denominator and fixed-point scale for IoDevice::rssi_ema_scaled.
@ BROADCAST_ALL
Broadcast to all devices of a type (address suffix 0x3F).
const char * address_class_name(AddressClass address_class)
Get a human-readable name for an address classification.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
bool has_reached_target_position(float target, float position)
Has the device reached its target within tolerance?
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
const char * yaml_device_type_name(DeviceType type)
Return the YAML-friendly device-type name for types exposed in the Python schema.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ LIGHT
Binary on/off light.
static constexpr int16_t RSSI_UNKNOWN_DBM
Sentinel value meaning "no RSSI sample recorded yet" for last_rssi_dbm/rssi_ema_scaled.
bool is_limitation_result(uint8_t result)
Check whether a result code represents an environmental or control limitation.
const char * originator_name(uint8_t originator)
Get a human-readable name for a command originator byte.
static constexpr uint8_t BINARY_ENTITY_OFF_POSITION
Position value written for binary OFF commands (light off, switch off, lock lock).
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
Runtime state of a paired IO‑Homecontrol device.
uint32_t last_result_at_ms
millis() timestamp of last_result_code, 0 when none recorded.
float target
Target position the device is moving toward.
uint16_t exchange_attempt_count
Cumulative attempts (ExchangeEngine::DebugInfo::tries, 1-based per exchange) across those timed-out e...
uint8_t last_result_code
Last CMD_ERROR_RESP result byte (0 = none recorded).
uint8_t last_commander[NODE_ID_SIZE]
Node ID of the controller that last commanded this device, as reported verbatim by the device in its ...
uint8_t last_command_originator
That command's Command Originator byte (ORIGINATOR_* in proto_constants.h).
int16_t last_rssi_dbm
Most recent raw RSSI sample (dBm), or RSSI_UNKNOWN_DBM.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint32_t last_seen_ms
millis() of the last frame received from this device (any command), 0 = never.
int16_t rssi_ema_scaled
Smoothed RSSI as fixed point in 1/RSSI_EMA_SCALE dBm — read through device_rssi_ema_dbm(),...
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
bool has_last_command
True once a status reply carried a well-formed last-command record.
DeviceType type
Device type (shutter, awning, etc.).
uint16_t exchange_timeout_count
Cumulative count of outbound exchanges to this device with no valid response (see detail::record_exch...
bool is_stopped
True if device is not moving.
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 src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t dst[NODE_ID_SIZE]
Destination node ID (3 bytes).
uint8_t data_len
Actual length of data.
Recovered controller identity from a decoded CMD_ONEWAY_ADD_CONTROLLER (0x30) frame.
uint8_t manufacturer
Manufacturer ID byte from the payload (man_id).
OneWayMacStatus mac_status
MAC-verification outcome; see OneWayMacStatus.
uint8_t system_key[AES_KEY_SIZE]
Recovered network system key (crypto::crypt_1w_key() output).
uint8_t sender_node[NODE_ID_SIZE]
Sender's node address (frame.src) — the new identity's node.
Decoded representation of a 1W remote frame.
bool has_intent
True if originator/ACEI/intent fields were decoded.
uint8_t originator
Command originator byte (e.g., ORIGINATOR_USER_REMOTE).
DeviceType target_type
Target device class from broadcast address.
uint8_t acei_level
ACEI priority level (0–7).
uint8_t cmd
Command ID (e.g., CMD_EXECUTE, CMD_ACTIVATE_MODE).
AddressClass address_class
Classification of the broadcast address.
char intent[ONEWAY_INTENT_BUFFER_SIZE]
Human-readable command intent (e.g., "CLOSE").
uint8_t src[NODE_ID_SIZE]
Remote source node ID (3 bytes).
uint8_t data_len
Raw data length (for commands without decoded intent).
Diagnostic capture from a radio operation.
uint32_t timestamp_ms
Timestamp of capture (millis).
bool valid
True if capture is valid.
uint32_t freq_hz
RF frequency of capture (Hz).
int16_t rssi_dbm
Received signal strength (dBm).
One decoded last-command record.
uint8_t commander[NODE_ID_SIZE]
Controller that last commanded the device.
uint8_t originator
That command's Command Originator (ORIGINATOR_*).
bool valid
False when the payload was too short, or the record was unpopulated.