16#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
17#include "esphome/core/helpers.h"
34constexpr const char *MANAGEMENT_ACTION_RENAME_DEVICE =
"rename_device";
35constexpr const char *MANAGEMENT_ACTION_IDENTIFY_DEVICE =
"identify_device";
36constexpr const char *MANAGEMENT_ACTION_FORCE_OPEN_DEVICE =
"force_open_device";
37constexpr const char *MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES =
"scan_paired_devices";
38constexpr const char *MANAGEMENT_RESULT_EVENT =
"esphome.home_io_control_action_result";
39constexpr size_t RESULT_CODE_BUFFER_SIZE = 5;
40constexpr size_t UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE = 64;
51constexpr uint8_t SCAN_MAX_REPLIES = 24;
68 bool metadata_complete;
82constexpr uint8_t SCAN_CHANNEL_COUNT =
sizeof(SCAN_CHANNELS) /
sizeof(SCAN_CHANNELS[0]);
88#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
102class ManagementServiceDescriptor :
public api::UserServiceDescriptor {
104 ManagementServiceDescriptor(
const char *name, std::vector<const char *> arg_names,
105 std::function<
void(
const api::ExecuteServiceRequest &)> callback)
106 : name_(name), key_(fnv1_hash(name)), arg_names_(std::move(arg_names)), callback_(std::move(callback)) {}
108 api::ListEntitiesServicesResponse encode_list_service_response()
override {
109 api::ListEntitiesServicesResponse response;
110 response.name = StringRef(this->name_);
111 response.key = this->key_;
112 response.supports_response = api::enums::SUPPORTS_RESPONSE_NONE;
113 response.args.init(this->arg_names_.size());
114 for (
const char *arg_name : this->arg_names_) {
115 auto &arg = response.args.emplace_back();
116 arg.name = StringRef(arg_name);
117 arg.type = api::enums::SERVICE_ARG_TYPE_STRING;
122 bool execute_service(
const api::ExecuteServiceRequest &request)
override {
123 if (request.key != this->key_ || request.args.size() != this->arg_names_.size())
125 this->callback_(request);
129#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
130 bool execute_service(
const api::ExecuteServiceRequest &request, uint32_t)
override {
131 return this->execute_service(request);
138 std::vector<const char *> arg_names_;
139 std::function<void(
const api::ExecuteServiceRequest &)> callback_;
149 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
150 [](
unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
154static std::string
bool_to_string(
bool value) {
return value ?
"true" :
"false"; }
157 std::array<char, RESULT_CODE_BUFFER_SIZE> buffer{};
158 std::snprintf(buffer.data(), buffer.size(),
"%02X", result_code);
159 return std::string(buffer.data());
185 const std::string &message) {
189 const size_t end = message.find(
'\n', start);
190 const std::string line = (end == std::string::npos) ? message.substr(start) : message.substr(start, end - start);
191 const std::string out = first ? prefix + line : line;
193 ESP_LOGW(tag,
"%s", out.c_str());
195 ESP_LOGI(tag,
"%s", out.c_str());
198 if (end == std::string::npos || end + 1 >= message.size())
216 result.
message =
"device returned an empty error response";
230 system_key_(system_key),
234 initialized_(initialized),
238#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
239 if (api::global_api_server ==
nullptr) {
240 ESP_LOGW(
detail::TAG,
"Native API server not available, management actions will not be registered");
243 api::global_api_server->register_user_service(
new detail::ManagementServiceDescriptor(
244 MANAGEMENT_ACTION_RENAME_DEVICE, {
"device_id",
"new_name"}, [
this](
const api::ExecuteServiceRequest &request) {
245 this->
api_rename_device(request.args[0].string_.str(), request.args[1].string_.str());
247 api::global_api_server->register_user_service(
new detail::ManagementServiceDescriptor(
248 MANAGEMENT_ACTION_IDENTIFY_DEVICE, {
"device_id"},
249 [
this](
const api::ExecuteServiceRequest &request) { this->
api_identify_device(request.args[0].string_.str()); }));
250 api::global_api_server->register_user_service(
new detail::ManagementServiceDescriptor(
251 MANAGEMENT_ACTION_FORCE_OPEN_DEVICE, {
"device_id"}, [
this](
const api::ExecuteServiceRequest &request) {
254 api::global_api_server->register_user_service(
new detail::ManagementServiceDescriptor(
255 MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES, {},
260IoDevice *ManagementActions::resolve_device_(
const char *action,
const std::string &device_id,
265 if (!*initialized_) {
266 result.
message =
"hub is not initialized";
272 result.
message =
"device ID must be exactly 6 hexadecimal characters";
276 auto *dev = registry_.get(normalized_device_id);
277 if (dev ==
nullptr) {
278 result.
message =
"device is not registered on this hub";
285bool ManagementActions::send_authenticated_request_(
const IoFrame &request,
IoFrame &response,
const char *action_verb,
287 if (engine_.send_and_receive(request, response,
FREQ_CH2))
289 engine_.log_debug(result.device_id.c_str());
290 result.message = std::string(
"no valid response to ") + action_verb +
" request";
301 const bool has_device = !result.
device_id.empty();
302 std::string prefix =
"Management action " + result.
action;
304 prefix +=
" for device " + result.
device_id;
305 prefix += result.
success ?
": " :
" failed: ";
308 if (!hub_->is_connected())
311 std::map<std::string, std::string> event_data{{
"action", result.
action},
326 hub_->fire_homeassistant_event(MANAGEMENT_RESULT_EVENT, event_data);
331 auto *dev = resolve_device_(MANAGEMENT_ACTION_RENAME_DEVICE, device_id, result);
336 std::string normalized_name;
346 result.
message =
"failed to build rename request";
351 if (!send_authenticated_request_(request, response,
"rename", result))
363 std::array<char, UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE> buffer{};
364 std::snprintf(buffer.data(), buffer.size(),
"unexpected rename response 0x%02X", response.
cmd);
365 result.
message = buffer.data();
370 result.
message =
"rename acknowledged by device";
372 if (!hub_->request_device_name(result.
device_id)) {
373 result.
message =
"rename acknowledged but verification readback failed";
377 auto *updated_device = registry_.get(result.
device_id);
378 if (updated_device !=
nullptr)
383 result.
message =
"rename verified by device readback";
387 result.
message =
"rename acknowledged but readback did not match the requested name";
399 auto *dev = resolve_device_(MANAGEMENT_ACTION_IDENTIFY_DEVICE, device_id, result);
405 result.
message =
"failed to build identify request";
410 if (!send_authenticated_request_(request, response,
"identify", result))
421 result.
message =
"identify triggered (device returned an empty error response)";
429 result.
message =
"identify acknowledged by device";
439 auto *dev = resolve_device_(MANAGEMENT_ACTION_FORCE_OPEN_DEVICE, device_id, result);
446 result.
message =
"device does not accept cover commands";
452 "force open queued (elevated-priority open; wind/rain lock bypass unconfirmed; movement result arrives via "
469 std::string line =
" " + device_id +
": " +
device_type_name(responder.type) +
470 " subtype=" + std::to_string(responder.subtype) +
" rssi=" + std::to_string(responder.rssi_dbm) +
472 if (responder.has_extended) {
475 line += std::string(
" manufacturer=") +
manufacturer_name(responder.manufacturer) +
478 line += known ?
" [known]\n" :
" [unknown]\n";
484 responder.metadata_complete, responder.inverted);
485 if (!snippet.empty())
486 return line +
" Paste this into your YAML to register it:\n" + snippet;
488 return line +
" no ready-to-paste YAML: no ESPHome platform for io_device_type: " +
512 const IoFrame &frame, int16_t rssi_dbm) {
513 for (uint8_t i = 0; i < count; i++) {
517 if (count >= capacity)
525 std::string unused_device_id;
528 ScanResponder &entry = responders[count++];
530 entry.type = device.
type;
531 entry.subtype = device.
subtype;
533 entry.rssi_dbm = rssi_dbm;
535 entry.flags = info.
flags;
544 if (!*initialized_) {
545 result.
message =
"hub is not initialized";
555 ScanResponder responders[SCAN_MAX_REPLIES];
557 bool truncated =
false;
558 for (uint8_t attempt = 0; attempt < SCAN_CHANNEL_COUNT; attempt++) {
561 false, 0, system_key_)) {
562 result.
message =
"failed to build roll-call request";
566 const uint8_t before = count;
567 const uint8_t heard = engine_.collect_broadcast_responses(
569 [&responders, &count, &truncated](
const IoFrame &frame, int16_t rssi_dbm) {
570 if (add_scan_responder(responders, count, SCAN_MAX_REPLIES, frame, rssi_dbm) == ScanAddResult::FULL) {
578 const uint8_t new_count = count - before;
579 ESP_LOGD(
detail::TAG,
"Roll-call attempt %u/%u (%" PRIu32
" Hz, %u ms window): %u repl%s heard, %u new",
580 attempt + 1, SCAN_CHANNEL_COUNT, SCAN_CHANNELS[attempt], tuning_->pairing_discovery_wait_ms, heard,
581 heard == 1 ?
"y" :
"ies", new_count);
587 std::string known_body;
588 std::string unknown_body;
589 uint8_t unknown_count = 0;
590 for (uint8_t i = 0; i < count; i++) {
592 const bool known = registry_.get(device_id) !=
nullptr;
602 if (!known_body.empty())
603 body +=
"Known:\n" + known_body;
604 if (!unknown_body.empty())
605 body +=
"Unknown:\n" + unknown_body;
607 result.success =
true;
608 result.message =
"Roll-call: " + std::to_string(count) +
" device" + (count == 1 ?
"" :
"s") +
" detected (" +
609 std::to_string(count - unknown_count) +
" known, " + std::to_string(unknown_count) +
" unknown)\n";
613 result.message +=
"NOTE: more than " + std::to_string(SCAN_MAX_REPLIES) +
614 " devices answered; the list below is truncated. Re-run to see whether other devices "
615 "appear, and raise SCAN_MAX_REPLIES if this install really is larger.\n";
617 result.message += body;
Owns the per-hub device table, update callbacks, and linked-remote associations.
Authenticated exchange engine — outbound and inbound protocol flows.
The main IO-Homecontrol component.
void register_actions()
Register all management actions (rename, identify, force-open, ...) with ESPHome's native API server.
void api_rename_device(const std::string &device_id, const std::string &new_name)
Native API callback: rename a device and publish the result as a HA event.
ManagementActionResult scan_paired_devices()
Broadcast a roll-call and report every device that answers.
ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name)
Rename a registered device and verify the result by reading the name back.
void publish_result(const ManagementActionResult &result)
Publish a management result as one or more structured log lines (one call per line of result....
void api_force_open_device(const std::string &device_id)
Native API callback: force-open a device and publish the result as a HA event.
ManagementActions(const uint8_t *node_id, const uint8_t *system_key, const TuningConfig *tuning, ExchangeEngine &engine, DeviceRegistry ®istry, const bool *initialized, IOHomeControlComponent *hub)
Construct with all required collaborators.
ManagementActionResult identify_device(const std::string &device_id)
Trigger a registered device's physical identify (brief jog/flash).
ManagementActionResult force_open_device(const std::string &device_id)
Move a registered cover device to fully open at elevated priority, intended to bypass wind/rain soft ...
void api_scan_paired_devices()
Native API callback: run a roll-call scan and publish the result as a HA event.
void api_identify_device(const std::string &device_id)
Native API callback: trigger a device's physical identify and publish the result as a HA event.
Internal helpers shared by the hub implementation .cpp files.
Hub-level management operations exposed as Home Assistant actions.
constexpr const char * TAG
Shared log tag for hub-level messages.
const char * manufacturer_name(uint8_t id)
Get a human-readable manufacturer name from the protocol manufacturer byte.
uint8_t discovery_power_save_mode(uint8_t flags)
Extract the power save mode field from a discovery response's Multi Information Byte.
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.
static ScanAddResult add_scan_responder(ScanResponder *responders, uint8_t &count, uint8_t capacity, const IoFrame &frame, int16_t rssi_dbm)
Record a responder unless its address is already present.
static constexpr uint8_t CMD_ERROR_RESP
Error response to any command.
ScanAddResult
Outcome of add_scan_responder(), so callers can tell a harmless repeat from real loss.
@ ADDED
New responder recorded.
@ DUPLICATE
Already recorded from an earlier reply; nothing changed.
@ FULL
Dropped: SCAN_MAX_REPLIES distinct responders already recorded.
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
const char * att_class_name(uint8_t att_class)
Get a human-readable turnaround time string for an ATT class value.
std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id, bool metadata_complete, bool inverted)
Build the ready-to-paste YAML block describing a device, for both a fully-decoded device and one whos...
const char * power_save_mode_name(uint8_t mode)
Get a human-readable power save mode name.
static ManagementActionResult make_management_result(const std::string &action, const std::string &device_id)
@ FORCE_OPEN
Move to fully open at elevated priority; intended to bypass soft locks and environmental limits (conf...
bool create_discovery_request(IoFrame &f, const uint8_t *own, uint8_t command, const uint8_t *dst, bool low_power, bool payload_enabled, uint8_t payload, const uint8_t *system_key)
Build a configurable discovery request command (0x28, 0x2A, or 0x2E).
static constexpr uint32_t FREQ_CH1
The protocol uses 3 frequency channels in the 868 MHz ISM band.
static std::string bool_to_string(bool value)
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
static constexpr uint8_t CMD_DISCOVER_SPE_RESP
Roll-call reply to CMD_DISCOVER_SPE_REQ, sent only by devices that already hold the requesting contro...
static constexpr uint8_t DEVICE_NAME_WRITE_PAYLOAD_SIZE
Fixed write payload: 15 visible chars plus trailing null/padding.
const char * command_result_description(uint8_t result)
Return a human-readable explanation for a CMD_ERROR_RESP result code.
const char * command_result_name(uint8_t result)
Return a stable symbolic name for a CMD_ERROR_RESP result code.
static constexpr uint8_t CMD_SET_NAME_RESP
Device-name write response.
DeviceNameValidationError
Validation result for outbound device-name writes.
@ NONE
Name is valid and encodable.
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
static constexpr uint8_t CMD_DISCOVER_SPE_REQ
Broadcast roll-call answered by every device that already holds this controller's system key,...
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
DeviceNameValidationError encode_device_name_payload(const std::string &name, uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE], std::string &normalized_name)
Validate and encode a user-supplied UTF-8 device name into the fixed Latin-1 write payload.
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 uint8_t BROADCAST_DISCOVER[NODE_ID_SIZE]
Broadcast address for device discovery (0x00003B).
static std::string format_scan_reply_line(const ScanResponder &responder, const std::string &device_id, bool known)
Format one roll-call responder's report line(s).
bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst)
Build an authenticated device-identify request (0x1E).
bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE])
Build an authenticated set-name request (0x52) using a fixed zero-padded Latin-1 payload.
uint8_t discovery_att_class(uint8_t flags)
Extract the ATT class field from a discovery response's Multi Information Byte.
static std::string normalize_device_id_argument(const std::string &device_id)
DiscoveryResponseInfo decode_discovery_response(const IoFrame &frame, IoDevice &device, std::string &device_id)
Decode a discovery-response payload (CMD_DISCOVER_RESP 0x29 or CMD_DISCOVER_SPE_RESP 0x2B — both carr...
static std::string format_result_code(uint8_t result_code)
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation result.
static bool apply_error_response(const IoFrame &response, ManagementActionResult &result)
Decode a CMD_ERROR_RESP frame's result code into result.
bool hex_to_bytes(const std::string &hex, uint8_t *out, uint8_t len)
Convert a hex string (e.g., "123ABC") to a byte array.
static 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...
Command builders for the IO‑Homecontrol protocol.
Extended discovery-response fields (manufacturer, Multi Information Byte, backbone address,...
bool has_extended
data_len >= DISCOVERY_RESP_FULL_SIZE (mfr/flags/timestamp present).
bool metadata_complete
data_len >= DEVICE_METADATA_SIZE (type/subtype present).
uint8_t manufacturer
Raw manufacturer ID; name via manufacturer_name().
uint8_t flags
Multi Information Byte; decode with DISCOVERY_FLAGS_* masks.
Runtime state of a paired IO‑Homecontrol device.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceType type
Device type (shutter, awning, etc.).
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t data_len
Actual length of data.
Result of a hub-level management action such as rename.
bool verified
Whether a follow-up readback confirmed the applied state.
std::string device_id
Target IO-homecontrol device ID.
uint8_t result_code
Optional CMD_ERROR_RESP result byte.
std::string action
Action name, e.g. "rename_device".
std::string applied_name
Verified cached UTF-8 name after a readback, when available.
bool success
Whether the requested management action succeeded.
bool has_result_code
True when result_code contains a decoded CMD_ERROR_RESP byte.
std::string message
Human-readable outcome summary.
std::string requested_name
Requested normalized UTF-8 name for rename actions.
All runtime tunable parameters for pairing and radio diagnostics.