Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
hub_management.cpp
Go to the documentation of this file.
1/// @file hub_management.cpp
2/// @brief Hub-level management actions such as device rename.
3/// @ingroup hioc_hub
4///
5/// This file owns advanced management operations that are not part of the normal cover/light/
6/// switch entity surface. These operations are exposed as ESPHome native API actions so Home
7/// Assistant can trigger them on demand without adding always-visible helper entities.
8
9#include "hub_internal.h"
10
11#include "proto_commands.h"
12
13#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
14#include "esphome/core/helpers.h"
15#endif
16
17#include <algorithm>
18#include <array>
19#include <cctype>
20#include <cstdio>
21#include <map>
22
23namespace esphome {
24namespace home_io_control {
25
26namespace {
27
28constexpr const char *MANAGEMENT_ACTION_RENAME_DEVICE = "rename_device";
29constexpr const char *MANAGEMENT_RESULT_EVENT = "esphome.home_io_control_action_result";
30constexpr size_t RESULT_CODE_BUFFER_SIZE = 5;
31constexpr size_t UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE = 64;
32
33} // namespace
34
35namespace detail {
36
37#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
38/// @brief Native API descriptor for the rename action.
39///
40/// ESPHome 2026.x does not expose the generated YAML action helper runtime to external
41/// components, so Home IO Control registers the action descriptor directly with APIServer.
42/// This keeps the Home Assistant action surface identical to native ESPHome actions while
43/// avoiding the unresolved link path behind CustomAPIDevice::register_service().
44class RenameDeviceServiceDescriptor : public api::UserServiceDescriptor {
45 public:
46 explicit RenameDeviceServiceDescriptor(IOHomeControlComponent *parent)
47 : parent_(parent), key_(fnv1_hash(MANAGEMENT_ACTION_RENAME_DEVICE)) {}
48
49 api::ListEntitiesServicesResponse encode_list_service_response() override {
50 api::ListEntitiesServicesResponse response;
51 response.name = StringRef(MANAGEMENT_ACTION_RENAME_DEVICE);
52 response.key = this->key_;
53 response.supports_response = api::enums::SUPPORTS_RESPONSE_NONE;
54 response.args.init(this->arg_names_.size());
55 for (const char *arg_name : this->arg_names_) {
56 auto &arg = response.args.emplace_back();
57 arg.name = StringRef(arg_name);
58 arg.type = api::enums::SERVICE_ARG_TYPE_STRING;
59 }
60 return response;
61 }
62
63 bool execute_service(const api::ExecuteServiceRequest &request) override {
64 if (request.key != this->key_ || request.args.size() != this->arg_names_.size())
65 return false;
66
67 this->parent_->api_rename_device_(request.args[0].string_.str(), request.args[1].string_.str());
68 return true;
69 }
70
71#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
72 bool execute_service(const api::ExecuteServiceRequest &request, uint32_t) override {
73 return this->execute_service(request);
74 }
75#endif
76
77 protected:
79 uint32_t key_;
80 const std::array<const char *, 2> arg_names_{"device_id", "new_name"};
81};
82#endif
83
84} // namespace detail
85
86std::string normalize_device_id_argument(const std::string &device_id) {
87 std::string normalized = trim_ascii_whitespace(device_id);
88 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
89 [](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
90 return normalized;
91}
92
93std::string bool_to_string(bool value) { return value ? "true" : "false"; }
94
95std::string format_result_code(uint8_t result_code) {
96 std::array<char, RESULT_CODE_BUFFER_SIZE> buffer{};
97 std::snprintf(buffer.data(), buffer.size(), "%02X", result_code);
98 return std::string(buffer.data());
99}
100
102 const std::string &device_id) {
104 result.action = action;
105 result.device_id = device_id;
106 return result;
107}
108
110#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
111 if (api::global_api_server == nullptr) {
112 ESP_LOGW(detail::TAG, "Native API server not available, rename action will not be registered");
113 return;
114 }
115
116 api::global_api_server->register_user_service(new detail::RenameDeviceServiceDescriptor(this)); // NOLINT
117#endif
118}
119
121 if (result.success) {
122 ESP_LOGI(detail::TAG, "Management action %s for device %s: %s", result.action.c_str(), result.device_id.c_str(),
123 result.message.c_str());
124 } else {
125 ESP_LOGW(detail::TAG, "Management action %s for device %s failed: %s", result.action.c_str(),
126 result.device_id.c_str(), result.message.c_str());
127 }
128
129 if (!this->is_connected())
130 return;
131
132 std::map<std::string, std::string> event_data{{"action", result.action},
133 {"device_id", result.device_id},
134 {"success", bool_to_string(result.success)},
135 {"verified", bool_to_string(result.verified)},
136 {"message", result.message}};
137
138 if (!result.requested_name.empty())
139 event_data["requested_name"] = result.requested_name;
140 if (!result.applied_name.empty())
141 event_data["applied_name"] = result.applied_name;
142 if (result.has_result_code) {
143 event_data["result_code"] = format_result_code(result.result_code);
144 event_data["result_code_name"] = command_result_name(result.result_code);
145 }
146
147 this->fire_homeassistant_event(MANAGEMENT_RESULT_EVENT, event_data);
148}
149
150void IOHomeControlComponent::api_rename_device_(const std::string &device_id, const std::string &new_name) {
151 this->publish_management_result_(this->rename_device(device_id, new_name));
152}
153
155 const std::string &new_name) {
156 const std::string normalized_device_id = normalize_device_id_argument(device_id);
157 ManagementActionResult result = make_management_result(MANAGEMENT_ACTION_RENAME_DEVICE, normalized_device_id);
158
159 if (!this->initialized_) {
160 result.message = "hub is not initialized";
161 return result;
162 }
163
164 uint8_t parsed_device_id[NODE_ID_SIZE]{};
165 if (!hex_to_bytes(normalized_device_id, parsed_device_id, NODE_ID_SIZE)) {
166 result.message = "device ID must be exactly 6 hexadecimal characters";
167 return result;
168 }
169
170 auto *dev = this->get_device(normalized_device_id);
171 if (dev == nullptr) {
172 result.message = "device is not registered on this hub";
173 return result;
174 }
175
176 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE];
177 std::string normalized_name;
178 const DeviceNameValidationError name_error = encode_device_name_payload(new_name, payload, normalized_name);
179 result.requested_name = normalized_name.empty() ? trim_ascii_whitespace(new_name) : normalized_name;
180 if (name_error != DeviceNameValidationError::NONE) {
182 return result;
183 }
184
185 IoFrame request;
186 if (!create_set_name(request, this->node_id_, dev->node_id, payload)) {
187 result.message = "failed to build rename request";
188 return result;
189 }
190
191 IoFrame response;
192 if (!this->send_and_receive_(request, response, FREQ_CH2)) {
193 this->log_exchange_debug_(normalized_device_id.c_str());
194 result.message = "no valid response to rename request";
195 return result;
196 }
197
198 if (response.cmd == CMD_ERROR_RESP) {
199 if (response.data_len == 0) {
200 result.message = "device returned an empty error response";
201 return result;
202 }
203
204 result.has_result_code = true;
205 result.result_code = response.data[0];
206 result.message =
207 std::string(command_result_name(result.result_code)) + ": " + command_result_description(result.result_code);
208 return result;
209 }
210
211 if (response.cmd != CMD_SET_NAME_RESP) {
212 std::array<char, UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE> buffer{};
213 std::snprintf(buffer.data(), buffer.size(), "unexpected rename response 0x%02X", response.cmd);
214 result.message = buffer.data();
215 return result;
216 }
217
218 result.success = true;
219 result.message = "rename acknowledged by device";
220
221 if (!this->request_device_name(normalized_device_id)) {
222 result.message = "rename acknowledged but verification readback failed";
223 return result;
224 }
225
226 auto *updated_device = this->get_device(normalized_device_id);
227 if (updated_device != nullptr)
228 result.applied_name = updated_device->name;
229
230 if (result.applied_name == normalized_name) {
231 result.verified = true;
232 result.message = "rename verified by device readback";
233 return result;
234 }
235
236 result.message = "rename acknowledged but readback did not match the requested name";
237 return result;
238}
239
240} // namespace home_io_control
241} // namespace esphome
virtual ManagementActionResult rename_device(const std::string &device_id, const std::string &new_name)
Rename a device and verify the result by reading the name back.
bool send_and_receive_(const IoFrame &request, IoFrame &response, uint32_t freq)
Main request/response exchange with retry and automatic authentication.
virtual IoDevice * get_device(const std::string &device_id)
Retrieve a device by ID; returns nullptr if not found.
Definition hub_core.cpp:258
void log_exchange_debug_(const char *device_id) const
Definition hub_core.cpp:59
void api_rename_device_(const std::string &device_id, const std::string &new_name)
Native API action callback: rename a registered device.
void publish_management_result_(const ManagementActionResult &result)
Publish the outcome of a management action as a Home Assistant event and structured logs.
virtual bool request_device_name(const std::string &device_id)
Request the stored device name from a device.
void register_management_actions_()
Register hub-level Home Assistant actions exposed through ESPHome's native API.
Internal helpers shared by the hub implementation .cpp files.
constexpr const char * TAG
Shared log tag for hub-level messages.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_frame.h:81
static constexpr uint8_t CMD_ERROR_RESP
Error response to any command.
std::string format_result_code(uint8_t result_code)
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.
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
IOHomeControlComponent::ManagementActionResult make_management_result(const std::string &action, const std::string &device_id)
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
Definition proto_frame.h:32
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.
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.
std::string bool_to_string(bool value)
std::string normalize_device_id_argument(const std::string &device_id)
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation 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.
Command builders for the IO‑Homecontrol protocol.
Result payload used by hub-level management actions such as rename.
Definition hub_core.h:73
bool has_result_code
True when result_code contains a decoded CMD_ERROR_RESP byte.
Definition hub_core.h:76
bool success
Whether the requested management action succeeded.
Definition hub_core.h:74
std::string requested_name
Requested normalized UTF-8 name for rename actions.
Definition hub_core.h:81
uint8_t result_code
Optional CMD_ERROR_RESP result byte from the device.
Definition hub_core.h:77
std::string device_id
Target IO-homecontrol device ID.
Definition hub_core.h:79
std::string applied_name
Verified cached UTF-8 name after a readback, when available.
Definition hub_core.h:82
std::string action
Action name, for example "rename_device".
Definition hub_core.h:78
bool verified
Whether a follow-up readback verified the applied state.
Definition hub_core.h:75
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t data[FRAME_MAX_DATA_SIZE]
Command parameters (0–23 bytes).
uint8_t data_len
Actual length of data.