16#include "esphome/core/application.h"
17#include "esphome/core/hal.h"
19#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
20#include "esphome/core/helpers.h"
42constexpr const char *MANAGEMENT_ACTION_RENAME_DEVICE =
"rename_device";
43constexpr const char *MANAGEMENT_ACTION_IDENTIFY_DEVICE =
"identify_device";
44constexpr const char *MANAGEMENT_ACTION_FORCE_OPEN_DEVICE =
"force_open_device";
45constexpr const char *MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES =
"scan_paired_devices";
46constexpr const char *MANAGEMENT_ACTION_ONEWAY_SET_POSITION =
"oneway_set_position";
47constexpr const char *MANAGEMENT_ACTION_ONEWAY_REMOVE_CONTROLLER =
"oneway_remove_controller";
48constexpr const char *MANAGEMENT_ACTION_PROBE_DEVICE =
"probe_device";
49constexpr const char *MANAGEMENT_ACTION_PROBE_SWEEP =
"probe_sweep";
50constexpr const char *MANAGEMENT_ACTION_HEATING_CONTROL =
"heating_control";
51constexpr const char *MANAGEMENT_RESULT_EVENT =
"esphome.home_io_control_action_result";
52constexpr size_t RESULT_CODE_BUFFER_SIZE = 5;
53constexpr size_t UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE = 64;
54constexpr size_t HEATING_RANGE_MESSAGE_BUFFER_SIZE = 64;
57constexpr const char *PROBE_NAME_PRIVATE_FN =
"private_fn";
58constexpr const char *PROBE_NAME_PRIVATE_FN_SUB =
"private_fn_sub";
59constexpr const char *PROBE_NAME_STATUS_EXT =
"status_ext";
60constexpr const char *PROBE_NAME_STATUS_EXT_FN6 =
"status_ext_fn6";
61constexpr const char *PROBE_NAME_STATUS_EXT_FN9 =
"status_ext_fn9";
62constexpr const char *PROBE_NAME_GET_INFO1 =
"get_info1";
63constexpr const char *PROBE_NAME_GET_INFO2 =
"get_info2";
64constexpr const char *PROBE_NAME_GENERAL_INFO3 =
"general_info3";
65constexpr const char *PROBE_NAME_PRIVATE2 =
"private2";
66constexpr const char *PROBE_NAME_PRIVATE2_SHORT =
"private2_short";
71constexpr uint8_t PROBE_EXT_FUNCTION_ID_06 = 0x06;
72constexpr uint8_t PROBE_EXT_FUNCTION_ID_09 = 0x09;
74constexpr uint8_t PROBE_PRIVATE_FN_SUB_FUNCTION_ID = 0x09;
78constexpr uint8_t PROBE_STATUS_EXT_SELECTOR = 0x80;
81constexpr uint8_t PROBE_SWEEP_MAX_INDICES = 16;
85constexpr uint32_t PROBE_SWEEP_DELAY_MS = 1000;
92using ProbeBuilderFn = bool (*)(
IoFrame &,
const uint8_t *,
const uint8_t *, uint8_t index,
bool low_power);
94bool build_probe_private_fn(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
97bool build_probe_private_fn_sub(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
100bool build_probe_status_ext(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
103bool build_probe_status_ext_fn6(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
106bool build_probe_status_ext_fn9(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
109bool build_probe_get_info1(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
112bool build_probe_get_info2(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
115bool build_probe_general_info3(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t ,
bool low_power) {
118bool build_probe_private2_long(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
121bool build_probe_private2_short(
IoFrame &f,
const uint8_t *own,
const uint8_t *dst, uint8_t index,
bool low_power) {
126struct ProbeDescriptor {
130 ProbeBuilderFn builder;
138constexpr ProbeDescriptor PROBE_TABLE[] = {
139 {PROBE_NAME_PRIVATE_FN,
true, build_probe_private_fn},
140 {PROBE_NAME_PRIVATE_FN_SUB,
true, build_probe_private_fn_sub},
141 {PROBE_NAME_STATUS_EXT,
true, build_probe_status_ext},
142 {PROBE_NAME_STATUS_EXT_FN6,
true, build_probe_status_ext_fn6},
143 {PROBE_NAME_STATUS_EXT_FN9,
true, build_probe_status_ext_fn9},
144 {PROBE_NAME_GET_INFO1,
false, build_probe_get_info1},
145 {PROBE_NAME_GET_INFO2,
false, build_probe_get_info2},
146 {PROBE_NAME_GENERAL_INFO3,
false, build_probe_general_info3},
147 {PROBE_NAME_PRIVATE2,
true, build_probe_private2_long},
148 {PROBE_NAME_PRIVATE2_SHORT,
true, build_probe_private2_short},
150constexpr uint8_t PROBE_TABLE_SIZE =
sizeof(PROBE_TABLE) /
sizeof(PROBE_TABLE[0]);
154const ProbeDescriptor *find_probe_descriptor(
const std::string &probe) {
155 for (
const auto &descriptor : PROBE_TABLE) {
156 if (probe == descriptor.name)
164std::string unknown_probe_message(
const std::string &probe) {
166 for (uint8_t i = 0; i < PROBE_TABLE_SIZE; i++) {
168 names += (i + 1 == PROBE_TABLE_SIZE) ?
", or " :
", ";
169 names += PROBE_TABLE[i].name;
171 return "unknown probe \"" + probe +
"\" (expected " + names +
")";
179const ProbeDescriptor *resolve_probe_descriptor(
const std::string &probe,
ManagementActionResult &result) {
180 const ProbeDescriptor *descriptor = find_probe_descriptor(probe);
181 if (descriptor ==
nullptr)
182 result.message = unknown_probe_message(probe);
195constexpr uint8_t SCAN_MAX_REPLIES = 24;
203struct ScanResponder {
209 uint8_t manufacturer;
212 bool metadata_complete;
230constexpr uint8_t SCAN_CHANNEL_COUNT =
sizeof(SCAN_CHANNELS) /
sizeof(SCAN_CHANNELS[0]);
236#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
248#if defined(ESPHOME_VERSION_CODE) && defined(VERSION_CODE)
249#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 9, 0)
250#define IOHOME_USERSERVICE_ENCODE_TAKES_SCRATCH 1
267class ManagementServiceDescriptor :
public api::UserServiceDescriptor {
269 ManagementServiceDescriptor(
const char *name, std::vector<const char *> arg_names,
270 std::function<
void(
const api::ExecuteServiceRequest &)> callback)
271 : name_(name), key_(fnv1_hash(name)), arg_names_(std::move(arg_names)), callback_(std::move(callback)) {}
276#ifdef IOHOME_USERSERVICE_ENCODE_TAKES_SCRATCH
277 api::ListEntitiesServicesResponse encode_list_service_response(std::span<char> )
override {
279 api::ListEntitiesServicesResponse encode_list_service_response()
override {
281 api::ListEntitiesServicesResponse response;
282 response.name = StringRef(this->name_);
283 response.key = this->key_;
284 response.supports_response = api::enums::SUPPORTS_RESPONSE_NONE;
285 response.args.init(this->arg_names_.size());
286 for (
const char *arg_name : this->arg_names_) {
287 auto &arg = response.args.emplace_back();
288 arg.name = StringRef(arg_name);
289 arg.type = api::enums::SERVICE_ARG_TYPE_STRING;
294 bool execute_service(
const api::ExecuteServiceRequest &request)
override {
295 if (request.key != this->key_ || request.args.size() != this->arg_names_.size())
297 this->callback_(request);
301#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
302 bool execute_service(
const api::ExecuteServiceRequest &request, uint32_t)
override {
303 return this->execute_service(request);
310 std::vector<const char *> arg_names_;
311 std::function<void(
const api::ExecuteServiceRequest &)> callback_;
314#undef IOHOME_USERSERVICE_ENCODE_TAKES_SCRATCH
323 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
324 [](
unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
328static std::string
bool_to_string(
bool value) {
return value ?
"true" :
"false"; }
334 std::array<char, RESULT_CODE_BUFFER_SIZE> buffer{};
335 std::snprintf(buffer.data(), buffer.size(),
"%02X", value);
336 return std::string(buffer.data());
358 result.
message =
"device returned an empty error response";
383 if (text.size() > 1 && text[0] ==
'0' && text[1] !=
'x' && text[1] !=
'X')
389 const uint32_t value = std::strtoul(text.c_str(), &end, 0);
390 if (end != text.c_str() + text.size())
392 if (errno == ERANGE || value > std::numeric_limits<uint8_t>::max())
394 out =
static_cast<uint8_t
>(value);
401 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
402 [](
unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
415 for (
size_t i = 0; i < table_len; i++) {
416 if (token == table[i].token) {
417 out = table[i].
value;
429 const float parsed = std::strtof(text.c_str(), &end);
430 if (text.empty() || end != text.c_str() + text.size() || errno == ERANGE || !std::isfinite(parsed)) {
431 error =
"temperature must be a number in degrees Celsius";
435 std::array<char, HEATING_RANGE_MESSAGE_BUFFER_SIZE> buffer{};
436 std::snprintf(buffer.data(), buffer.size(),
"temperature must be between %.1f and %.1f",
438 error = buffer.data();
462 float &value_out, std::string &error) {
469 static constexpr HeatingNamedValue PRESENCE_VALUES[] = {{
"on", 1.0F}, {
"off", 0.0F}};
470 static constexpr HeatingNamedValue WINDOW_VALUES[] = {{
"open", 1.0F}, {
"close", 0.0F}};
475 if (fn ==
"power_on") {
479 if (fn ==
"midnight_sync") {
483 if (fn ==
"set_temperature") {
487 if (fn ==
"set_mode") {
491 error =
"mode must be one of auto, manual, prog, off";
494 if (fn ==
"set_presence") {
499 error =
"presence must be 'on' or 'off'";
502 if (fn ==
"set_window") {
506 error =
"window must be 'open' or 'close'";
520 system_key_(system_key),
524 initialized_(initialized),
528#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
529 if (api::global_api_server ==
nullptr) {
530 ESP_LOGW(
detail::TAG,
"Native API server not available, management actions will not be registered");
540 std::vector<const char *> arg_names;
541 bool diagnostic_only;
542 std::function<void(
const api::ExecuteServiceRequest &)> callback;
544 const ActionReg actions[] = {
545 {MANAGEMENT_ACTION_RENAME_DEVICE,
546 {
"device_id",
"new_name"},
548 [
this](
const api::ExecuteServiceRequest &r) {
551 {MANAGEMENT_ACTION_IDENTIFY_DEVICE,
554 [
this](
const api::ExecuteServiceRequest &r) { this->
api_identify_device(r.args[0].string_.str()); }},
555 {MANAGEMENT_ACTION_FORCE_OPEN_DEVICE,
558 [
this](
const api::ExecuteServiceRequest &r) { this->
api_force_open_device(r.args[0].string_.str()); }},
559 {MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES,
563 {MANAGEMENT_ACTION_ONEWAY_SET_POSITION,
564 {
"controller_id",
"position"},
566 [
this](
const api::ExecuteServiceRequest &r) {
569 {MANAGEMENT_ACTION_ONEWAY_REMOVE_CONTROLLER,
573 {MANAGEMENT_ACTION_HEATING_CONTROL,
574 {
"device_id",
"function",
"value"},
576 [
this](
const api::ExecuteServiceRequest &r) {
577 this->
api_heating_control(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str());
579 {MANAGEMENT_ACTION_PROBE_DEVICE,
580 {
"device_id",
"probe",
"index"},
582 [
this](
const api::ExecuteServiceRequest &r) {
583 this->
api_probe_device(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str());
585 {MANAGEMENT_ACTION_PROBE_SWEEP,
586 {
"device_id",
"probe",
"first_index",
"last_index"},
588 [
this](
const api::ExecuteServiceRequest &r) {
589 this->
api_probe_sweep(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str(),
590 r.args[3].string_.str());
599 const bool probes_enabled = hub_->diagnostic_probes_enabled();
600 for (
const auto &action : actions) {
601 if (action.diagnostic_only && !probes_enabled)
603 api::global_api_server->register_user_service(
604 new detail::ManagementServiceDescriptor(action.name, action.arg_names, action.callback));
609IoDevice *ManagementActions::resolve_device_(
const char *action,
const std::string &device_id,
614 if (!*initialized_) {
615 result.
message =
"hub is not initialized";
621 result.
message =
"device ID must be exactly 6 hexadecimal characters";
625 auto *dev = registry_.get(normalized_device_id);
626 if (dev ==
nullptr) {
627 result.
message =
"device is not registered on this hub";
634bool ManagementActions::send_authenticated_request_(
const IoFrame &request,
IoFrame &response,
const char *action_verb,
639 engine_.log_debug(result.device_id.c_str());
644 ? std::string(
"device accepted the ") + action_verb +
" request but sent no response"
645 : std::string(
"no valid response to ") + action_verb +
" request";
656 const bool has_device = !result.
device_id.empty();
657 std::string prefix =
"Management action " + result.
action;
659 prefix +=
" for device " + result.
device_id;
660 prefix += result.
success ?
": " :
" failed: ";
663 if (!hub_->is_connected())
666 std::map<std::string, std::string> event_data{{
"action", result.
action},
695 hub_->fire_homeassistant_event(MANAGEMENT_RESULT_EVENT, event_data);
700 auto *dev = resolve_device_(MANAGEMENT_ACTION_RENAME_DEVICE, device_id, result);
705 std::string normalized_name;
714 if (!
create_set_name(request, node_id_, dev->node_id, dev->low_power, payload)) {
715 result.
message =
"failed to build rename request";
720 if (!send_authenticated_request_(request, response,
"rename", result))
732 std::array<char, UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE> buffer{};
733 std::snprintf(buffer.data(), buffer.size(),
"unexpected rename response 0x%02X", response.
cmd);
734 result.
message = buffer.data();
739 result.
message =
"rename acknowledged by device";
741 if (!hub_->request_device_name(result.
device_id)) {
742 result.
message =
"rename acknowledged but verification readback failed";
746 auto *updated_device = registry_.get(result.
device_id);
747 if (updated_device !=
nullptr)
752 result.
message =
"rename verified by device readback";
756 result.
message =
"rename acknowledged but readback did not match the requested name";
768 auto *dev = resolve_device_(MANAGEMENT_ACTION_IDENTIFY_DEVICE, device_id, result);
773 if (!
create_identify(request, node_id_, dev->node_id, dev->low_power)) {
774 result.
message =
"failed to build identify request";
779 if (!send_authenticated_request_(request, response,
"identify", result))
790 result.
message =
"identify triggered (device returned an empty error response)";
798 result.
message =
"identify acknowledged by device";
808 auto *dev = resolve_device_(MANAGEMENT_ACTION_FORCE_OPEN_DEVICE, device_id, result);
815 result.
message =
"device does not accept cover commands";
821 "force open queued (elevated-priority open; wind/rain lock bypass unconfirmed; movement result arrives via "
827 const std::string &value) {
832 const std::string &value) {
834 auto *dev = resolve_device_(MANAGEMENT_ACTION_HEATING_CONTROL, device_id, result);
839 float encoded_value = 0.0F;
840 std::string parse_error;
848 result.
message =
"device is not a climate device";
854 const uint8_t prior_result_code = dev->last_result_code;
855 const uint32_t prior_result_at_ms = dev->last_result_at_ms;
859 if (!hub_->send_heating_command(result.
device_id, fn, encoded_value)) {
861 if (
const auto *updated = registry_.get(result.
device_id);
862 updated !=
nullptr && updated->last_result_code != 0 &&
863 (updated->last_result_code != prior_result_code || updated->last_result_at_ms != prior_result_at_ms)) {
886 if (hub_->oneway_controllers().get(controller_id) ==
nullptr) {
887 result.
message =
"no oneway_controllers identity with that id";
896 if (trimmed.empty() || trimmed.find_first_not_of(
"0123456789") != std::string::npos) {
897 result.
message =
"position must be a whole number between 0 and 100";
901 const unsigned long parsed = strtoul(trimmed.c_str(),
nullptr, 10);
903 result.
message =
"position must be between 0 and 100";
908 hub_->send_oneway_position(controller_id,
static_cast<uint8_t
>(parsed));
912 result.
message =
"1W position command queued";
920 if (hub_->oneway_controllers().get(controller_id) ==
nullptr) {
921 result.
message =
"no oneway_controllers identity with that id";
926 hub_->send_oneway_unenroll(controller_id);
930 result.
message =
"1W remove-controller (0x39) queued";
944 std::string line =
" " + device_id +
": " +
device_type_name(responder.type) +
945 " subtype=" + std::to_string(responder.subtype) +
" rssi=" + std::to_string(responder.rssi_dbm) +
947 if (responder.has_extended) {
950 line += std::string(
" manufacturer=") +
manufacturer_name(responder.manufacturer) +
953 line += known ?
" [known]\n" :
" [unknown]\n";
960 responder.metadata_complete, responder.inverted, low_power);
961 if (!snippet.empty())
962 return line +
" Paste this into your YAML to register it:\n" + snippet;
964 return line +
" no ready-to-paste YAML: no ESPHome platform for io_device_type: " +
988 const IoFrame &frame, int16_t rssi_dbm) {
989 for (uint8_t i = 0; i < count; i++) {
993 if (count >= capacity)
1001 std::string unused_device_id;
1004 ScanResponder &entry = responders[count++];
1006 entry.type = device.
type;
1007 entry.subtype = device.
subtype;
1009 entry.rssi_dbm = rssi_dbm;
1011 entry.flags = info.
flags;
1020 if (!*initialized_) {
1021 result.
message =
"hub is not initialized";
1031 ScanResponder responders[SCAN_MAX_REPLIES];
1033 bool truncated =
false;
1034 for (uint8_t attempt = 0; attempt < SCAN_CHANNEL_COUNT; attempt++) {
1037 false, 0, system_key_)) {
1038 result.
message =
"failed to build roll-call request";
1042 const uint8_t before = count;
1043 const uint8_t heard = engine_.collect_broadcast_responses(
1045 [&responders, &count, &truncated](
const IoFrame &frame, int16_t rssi_dbm) {
1046 if (add_scan_responder(responders, count, SCAN_MAX_REPLIES, frame, rssi_dbm) == ScanAddResult::FULL) {
1057 const uint8_t new_count = count - before;
1058 ESP_LOGD(
detail::TAG,
"Roll-call attempt %u/%u (tx %" PRIu32
" Hz, %u ms window): %u repl%s heard, %u new",
1059 attempt + 1, SCAN_CHANNEL_COUNT, SCAN_CHANNELS[attempt], tuning_->pairing_discovery_wait_ms, heard,
1060 heard == 1 ?
"y" :
"ies", new_count);
1066 std::string known_body;
1067 std::string unknown_body;
1068 uint8_t unknown_count = 0;
1069 for (uint8_t i = 0; i < count; i++) {
1071 const bool known = registry_.get(device_id) !=
nullptr;
1081 if (!known_body.empty())
1082 body +=
"Known:\n" + known_body;
1083 if (!unknown_body.empty())
1084 body +=
"Unknown:\n" + unknown_body;
1086 result.success =
true;
1087 result.message =
"Roll-call: " + std::to_string(count) +
" device" + (count == 1 ?
"" :
"s") +
" detected (" +
1088 std::to_string(count - unknown_count) +
" known, " + std::to_string(unknown_count) +
" unknown)\n";
1092 result.message +=
"NOTE: more than " + std::to_string(SCAN_MAX_REPLIES) +
1093 " devices answered; the list below is truncated. Re-run to see whether other devices "
1094 "appear, and raise SCAN_MAX_REPLIES if this install really is larger.\n";
1096 result.message += body;
1101 const std::string &index) {
1106 const std::string &index) {
1111 auto *dev = resolve_device_(MANAGEMENT_ACTION_PROBE_DEVICE, device_id, resolve_result);
1112 if (dev ==
nullptr) {
1115 return resolve_result;
1122 if (!hub_->diagnostic_probes_enabled()) {
1123 result.
message =
"diagnostic probes are not enabled; set diagnostic_probes: true in YAML";
1130 result.
message =
"device is moving; refusing to probe mid-transaction";
1135 const ProbeDescriptor *descriptor = resolve_probe_descriptor(probe, result);
1136 if (descriptor ==
nullptr)
1139 uint8_t index_byte = 0;
1141 result.
message =
"index must be a decimal or 0x-prefixed byte value (0-255)";
1146 if (!descriptor->builder(request, node_id_, dev->node_id, index_byte, dev->low_power)) {
1147 result.
message =
"failed to build probe request";
1157 engine_.log_debug(result.
device_id.c_str());
1159 ?
"device accepted the probe request but sent no response"
1161 " attempts (device asleep, unreachable, or silently ignoring this opcode)";
1170 const uint8_t raw_len =
serialize(response, raw,
sizeof(raw));
1200 const std::string &first_index,
const std::string &last_index) {
1205 const std::string &first_index,
const std::string &last_index) {
1215 if (resolve_device_(MANAGEMENT_ACTION_PROBE_SWEEP, device_id, resolve_result) ==
nullptr) {
1219 const ProbeDescriptor *descriptor = resolve_probe_descriptor(probe, result);
1220 if (descriptor ==
nullptr)
1222 if (!descriptor->needs_index) {
1223 result.
message =
"probe \"" + probe +
"\" takes no index; use probe_device";
1230 result.
message =
"first_index/last_index must be decimal or 0x-prefixed byte values (0-255)";
1234 result.
message =
"last_index must be >= first_index";
1237 const uint32_t span =
static_cast<uint32_t
>(last) - first + 1;
1238 if (span > PROBE_SWEEP_MAX_INDICES) {
1240 "sweep range too wide: " + std::to_string(span) +
" indices, max " + std::to_string(PROBE_SWEEP_MAX_INDICES);
1245 uint32_t answered_count = 0;
1246 bool stopped_early =
false;
1247 for (uint32_t idx = first; idx <= last; idx++) {
1250 delay(PROBE_SWEEP_DELAY_MS);
1252 const std::string index_str = std::to_string(idx);
1254 report +=
"index=0x" +
format_hex_byte(
static_cast<uint8_t
>(idx)) +
": ";
1259 report += step.
message +
"\n";
1266 report +=
"(stopping sweep: " + step.
message +
")\n";
1267 stopped_early =
true;
1276 result.
success = !stopped_early;
1282 "]: " + std::to_string(answered_count) +
" answered\n" + report;
Owns the per-hub device table, update callbacks, and linked-remote associations.
The main IO-Homecontrol component.
void api_probe_device(const std::string &device_id, const std::string &probe, const std::string &index)
Native API callback: run a single diagnostic probe and publish the result as a HA event.
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.
void api_oneway_set_position(const std::string &controller_id, const std::string &position)
Native API callback: queue a 1W position for a controller identity.
ManagementActionResult scan_paired_devices()
Broadcast a roll-call and report every device that answers.
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.
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 api_oneway_remove_controller(const std::string &controller_id)
Native API callback: queue a standalone 1W un-enrollment (remove-controller, CMD 0x39) for a controll...
void api_heating_control(const std::string &device_id, const std::string &function, const std::string &value)
Native API callback: run a heating/climate function and publish the result as a HA event.
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_probe_sweep(const std::string &device_id, const std::string &probe, const std::string &first_index, const std::string &last_index)
Native API callback: run a bounded probe sweep and publish the result as a HA event.
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.
ManagementActionResult heating_control(const std::string &device_id, const std::string &function, const std::string &value)
Send one 2W heating/climate function (CMD_WRITE_PRIVATE 0x20) to a registered climate device.
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.
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.
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_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.
bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build an authenticated device-identify request (0x1E).
const char * manufacturer_name(uint8_t id)
Get a human-readable manufacturer name from the protocol manufacturer byte.
static bool parse_heating_temperature(const std::string &value, float &value_out, std::string &error)
Parse set_temperature's value into degrees Celsius, range-checked.
static bool parse_probe_index(const std::string &text, uint8_t &out)
Parse a probe_device()/probe_sweep() index argument into a 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.
bool create_set_name(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power, const uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE])
Build an authenticated set-name request (0x52) using a fixed zero-padded Latin-1 payload.
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.
static bool match_heating_named_value(const std::string &token, const HeatingNamedValue *table, size_t table_len, float &out)
Match token against table; on a hit set out and return true.
HeatingFunction
Heating functions, one per user-pressable radiator button in the reference.
@ MIDNIGHT_SYNC
Reads register 0x0130 — the comfort/eco/auto setpoint block (iohcCozyDevice2W.cpp:248; AtlanticThermo...
@ POWER_ON
Wake / retrieve paired devices (iohcCozyDevice2W.cpp:105).
@ SET_PRESENCE
Presence / absence (iohcCozyDevice2W.cpp:195).
@ SET_TEMPERATURE
Setpoint in degrees Celsius (iohcCozyDevice2W.cpp:125).
@ SET_MODE
Operating mode (iohcCozyDevice2W.cpp:155).
@ SET_WINDOW
Open-window / frost-protection (iohcCozyDevice2W.cpp:218).
const char * att_class_name(uint8_t att_class)
Get a human-readable turnaround time string for an ATT class value.
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.
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)
bool device_supports_climate_control(DeviceType type)
Does this device type support 2W climate/heating control (CMD_WRITE_PRIVATE 0x20)?
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
static constexpr uint32_t FREQ_CH3
Channel 3: 869.85 MHz (2W only).
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
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.
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...
const char * heating_function_name(HeatingFunction fn)
Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
static constexpr uint8_t FRAME_MAX_SIZE
Historical name for FRAME_MAX_DECLARED_SIZE, kept as an alias rather than a second literal so the two...
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.
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.
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 ...
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.
static std::string normalize_lower_argument(const std::string &value)
Lowercase + ASCII-trim a native-API string argument.
constexpr size_t FRAME_LOG_HEX_BUFFER_SIZE
Fits a full 32-byte frame rendered as spaced hex text.
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.
constexpr float HEATING_TEMP_MIN_C
Lowest setpoint this codec will encode.
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 uint8_t EXCHANGE_RETRY_COUNT
Attempts per command before reporting failure.
std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id, bool metadata_complete, bool inverted, bool low_power)
Build the ready-to-paste YAML block describing a device, for both a fully-decoded device and one whos...
static constexpr uint32_t FREQ_CH2
Channel 2: 868.95 MHz (1W and 2W, TX channel).
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
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.
@ OFF
Off / standby; note the value is 0x04, not 0x03 (that is the reference's commented-out "special" mode...
@ PROG
Program mode; device runs its own stored weekly schedule (iohcCozyDevice2W.cpp:160).
@ MANUAL
Manual mode; follows the last SET_TEMPERATURE setpoint (iohcCozyDevice2W.cpp:159).
@ AUTO
Automatic mode; device manages the setpoint itself (iohcCozyDevice2W.cpp:158).
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_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.
uint8_t discovery_att_class(uint8_t flags)
Extract the ATT class field from a discovery response's Multi Information Byte.
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...
static constexpr uint8_t POWER_SAVE_LOW_POWER
Device sleeps — needs long preamble to wake.
static std::string normalize_device_id_argument(const std::string &device_id)
static std::string format_hex_byte(uint8_t value)
Format a byte as two uppercase hex digits, no prefix.
static constexpr uint8_t ONEWAY_POSITION_FULLY_CLOSED
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...
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 bool parse_heating_arguments(const std::string &function, const std::string &value, HeatingFunction &fn_out, float &value_out, std::string &error)
Parse the (function, value) argument pair of the heating_control action.
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.
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.
A value token that maps to a fixed float (used by set_mode / set_presence / set_window).
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). Never includes mac.
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.
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.
std::string action
Action name, e.g. "rename_device".
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 applied_name
Verified cached UTF-8 name after a readback, when available.
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.
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.