20#include "esphome/core/hal.h"
22#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
23#include "esphome/core/helpers.h"
45constexpr const char *MANAGEMENT_ACTION_RENAME_DEVICE =
"rename_device";
46constexpr const char *MANAGEMENT_ACTION_IDENTIFY_DEVICE =
"identify_device";
47constexpr const char *MANAGEMENT_ACTION_FORCE_OPEN_DEVICE =
"force_open_device";
48constexpr const char *MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES =
"scan_paired_devices";
49constexpr const char *MANAGEMENT_ACTION_ONEWAY_SET_POSITION =
"oneway_set_position";
50constexpr const char *MANAGEMENT_ACTION_ONEWAY_REMOVE_CONTROLLER =
"oneway_remove_controller";
51constexpr const char *MANAGEMENT_ACTION_HEATING_CONTROL =
"heating_control";
52constexpr const char *MANAGEMENT_RESULT_EVENT =
"esphome.home_io_control_action_result";
53constexpr size_t UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE = 64;
54constexpr size_t HEATING_RANGE_MESSAGE_BUFFER_SIZE = 64;
60#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
65template<
typename Base>
66concept EncodeTakesScratch =
requires(Base &base) { base.encode_list_service_response(std::span<char>{}); };
73template<
bool TakesScratch,
typename Base = api::UserServiceDescriptor>
class EncodeListAdapter :
public Base {
75 api::ListEntitiesServicesResponse encode_list_service_response()
override {
return this->build_list_response(); }
78 virtual api::ListEntitiesServicesResponse build_list_response() = 0;
81template<
typename Base>
class EncodeListAdapter<true, Base> :
public Base {
83 api::ListEntitiesServicesResponse encode_list_service_response(std::span<char> )
override {
84 return this->build_list_response();
88 virtual api::ListEntitiesServicesResponse build_list_response() = 0;
104class ManagementServiceDescriptor :
public EncodeListAdapter<EncodeTakesScratch<api::UserServiceDescriptor>> {
106 ManagementServiceDescriptor(
const char *name, std::vector<const char *> arg_names,
107 std::function<
void(
const api::ExecuteServiceRequest &)> callback)
108 : name_(name), key_(fnv1_hash(name)), arg_names_(std::move(arg_names)), callback_(std::move(callback)) {}
110 bool execute_service(
const api::ExecuteServiceRequest &request)
override {
111 if (request.key != this->key_ || request.args.size() != this->arg_names_.size())
113 this->callback_(request);
117#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
118 bool execute_service(
const api::ExecuteServiceRequest &request, uint32_t)
override {
119 return this->execute_service(request);
126 template<
typename T>
static T make_ref(
const char *s) {
127 if constexpr (std::is_assignable_v<T &, StringRef>) {
130 return T(StringRef(s));
136 api::ListEntitiesServicesResponse build_list_response()
override {
137 api::ListEntitiesServicesResponse response;
138 response.name = make_ref<
decltype(response.name)>(this->name_);
139 response.key = this->key_;
140 response.supports_response = api::enums::SUPPORTS_RESPONSE_NONE;
141 response.args.init(this->arg_names_.size());
142 for (
const char *arg_name : this->arg_names_) {
143 auto &arg = response.args.emplace_back();
144 arg.name = make_ref<
decltype(arg.name)>(arg_name);
145 arg.type = api::enums::SERVICE_ARG_TYPE_STRING;
152 std::vector<const char *> arg_names_;
153 std::function<void(
const api::ExecuteServiceRequest &)> callback_;
162static std::string
bool_to_string(
bool value) {
return value ?
"true" :
"false"; }
167 std::transform(normalized.begin(), normalized.end(), normalized.begin(),
168 [](
unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
182 for (
size_t i = 0; i < table_len; i++) {
183 if (token == table[i].token) {
184 out = table[i].
value;
197 const float parsed = std::strtof(text.c_str(), &end);
198 if (text.empty() || end != text.c_str() + text.size() || errno == ERANGE || !std::isfinite(parsed)) {
199 error =
"temperature must be a number in degrees Celsius";
203 std::array<char, HEATING_RANGE_MESSAGE_BUFFER_SIZE> buffer{};
204 std::snprintf(buffer.data(), buffer.size(),
"temperature must be between %.1f and %.1f",
206 error = buffer.data();
230 float &value_out, std::string &error) {
237 static constexpr HeatingNamedValue PRESENCE_VALUES[] = {{
"on", 1.0F}, {
"off", 0.0F}};
238 static constexpr HeatingNamedValue DETECTION_VALUES[] = {{
"on", 1.0F}, {
"off", 0.0F}};
243 if (fn ==
"power_on") {
247 if (fn ==
"midnight_sync") {
251 if (fn ==
"set_temperature") {
255 if (fn ==
"set_mode") {
259 error =
"mode must be one of auto, manual, prog, off";
262 if (fn ==
"set_presence") {
267 error =
"presence must be 'on' or 'off'";
270 if (fn ==
"set_open_window_detection") {
275 error =
"open-window detection must be 'on' or 'off'";
278 if (fn ==
"window_open") {
293 system_key_(system_key),
297 initialized_(initialized),
299 probes_(node_id, engine, registry, initialized,
301 [hub]() {
return hub->get_radio(); },
302 [hub](
const IoFrame &reply) {
return hub->record_probe_reply_event(reply); }}) {}
305#if defined(USE_API_USER_DEFINED_ACTIONS) && defined(USE_API_CUSTOM_SERVICES)
306 if (api::global_api_server ==
nullptr) {
307 ESP_LOGW(
detail::TAG,
"Native API server not available, management actions will not be registered");
317 std::vector<const char *> arg_names;
318 bool diagnostic_only;
319 std::function<void(
const api::ExecuteServiceRequest &)> callback;
321 const ActionReg actions[] = {
322 {MANAGEMENT_ACTION_RENAME_DEVICE,
323 {
"device_id",
"new_name"},
325 [
this](
const api::ExecuteServiceRequest &r) {
328 {MANAGEMENT_ACTION_IDENTIFY_DEVICE,
331 [
this](
const api::ExecuteServiceRequest &r) { this->
api_identify_device(r.args[0].string_.str()); }},
332 {MANAGEMENT_ACTION_FORCE_OPEN_DEVICE,
335 [
this](
const api::ExecuteServiceRequest &r) { this->
api_force_open_device(r.args[0].string_.str()); }},
336 {MANAGEMENT_ACTION_SCAN_PAIRED_DEVICES,
340 {MANAGEMENT_ACTION_ONEWAY_SET_POSITION,
341 {
"controller_id",
"position"},
343 [
this](
const api::ExecuteServiceRequest &r) {
346 {MANAGEMENT_ACTION_ONEWAY_REMOVE_CONTROLLER,
350 {MANAGEMENT_ACTION_HEATING_CONTROL,
351 {
"device_id",
"function",
"value"},
353 [
this](
const api::ExecuteServiceRequest &r) {
354 this->
api_heating_control(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str());
357 {
"device_id",
"probe",
"index"},
359 [
this](
const api::ExecuteServiceRequest &r) {
360 this->
api_probe_device(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str());
363 {
"device_id",
"probe",
"first_index",
"last_index"},
365 [
this](
const api::ExecuteServiceRequest &r) {
366 this->
api_probe_sweep(r.args[0].string_.str(), r.args[1].string_.str(), r.args[2].string_.str(),
367 r.args[3].string_.str());
376 const bool probes_enabled = hub_->diagnostic_probes_enabled();
377 for (
const auto &action : actions) {
378 if (action.diagnostic_only && !probes_enabled)
380 api::global_api_server->register_user_service(
381 new detail::ManagementServiceDescriptor(action.name, action.arg_names, action.callback));
386IoDevice *ManagementActions::resolve_device_(
const char *action,
const std::string &device_id,
391bool ManagementActions::send_authenticated_request_(
const IoFrame &request, IoFrame &response,
const char *action_verb,
392 ManagementActionResult &result) {
393 const ExchangeOutcome outcome = engine_.send_and_receive(request, response, FREQ_CH2);
394 if (outcome == ExchangeOutcome::SUCCESS_WITH_RESPONSE)
396 engine_.log_debug(result.device_id.c_str());
400 result.message = outcome == ExchangeOutcome::SUCCESS_UNCONFIRMED
401 ? std::string(
"device accepted the ") + action_verb +
" request but sent no response"
402 : std::string(
"no valid response to ") + action_verb +
" request";
413 const bool has_device = !result.
device_id.empty();
414 std::string prefix =
"Management action " + result.
action;
416 prefix +=
" for device " + result.
device_id;
417 prefix += result.
success ?
": " :
" failed: ";
421 if (!hub_->is_connected())
424 std::map<std::string, std::string> event_data{{
"action", result.
action},
453 hub_->fire_homeassistant_event(MANAGEMENT_RESULT_EVENT, event_data);
458 auto *dev = resolve_device_(MANAGEMENT_ACTION_RENAME_DEVICE, device_id, result);
462 uint8_t payload[DEVICE_NAME_WRITE_PAYLOAD_SIZE];
463 std::string normalized_name;
472 if (!
create_set_name(request, node_id_, dev->node_id, dev->low_power, payload)) {
473 result.
message =
"failed to build rename request";
478 if (!send_authenticated_request_(request, response,
"rename", result))
481 if (response.
cmd == CMD_ERROR_RESP) {
489 if (response.
cmd != CMD_SET_NAME_RESP) {
490 std::array<char, UNEXPECTED_RESPONSE_MESSAGE_BUFFER_SIZE> buffer{};
491 std::snprintf(buffer.data(), buffer.size(),
"unexpected rename response 0x%02X", response.
cmd);
492 result.
message = buffer.data();
497 result.
message =
"rename acknowledged by device";
499 if (!hub_->request_device_name(result.
device_id)) {
500 result.
message =
"rename acknowledged but verification readback failed";
504 auto *updated_device = registry_.get(result.
device_id);
505 if (updated_device !=
nullptr)
510 result.
message =
"rename verified by device readback";
514 result.
message =
"rename acknowledged but readback did not match the requested name";
526 auto *dev = resolve_device_(MANAGEMENT_ACTION_IDENTIFY_DEVICE, device_id, result);
531 if (!
create_identify(request, node_id_, dev->node_id, dev->low_power)) {
532 result.
message =
"failed to build identify request";
537 if (!send_authenticated_request_(request, response,
"identify", result))
540 if (response.
cmd == CMD_ERROR_RESP) {
548 result.
message =
"identify triggered (device returned an empty error response)";
556 result.
message =
"identify acknowledged by device";
566 auto *dev = resolve_device_(MANAGEMENT_ACTION_FORCE_OPEN_DEVICE, device_id, result);
573 result.
message =
"device does not accept cover commands";
579 "force open queued (elevated-priority open; wind/rain lock bypass unconfirmed; movement result arrives via "
585 const std::string &value) {
590 const std::string &value) {
592 auto *dev = resolve_device_(MANAGEMENT_ACTION_HEATING_CONTROL, device_id, result);
597 float encoded_value = 0.0F;
598 std::string parse_error;
606 result.
message =
"device is not a climate device";
610 result.
message =
"device is commanded with heating levels, not private registers";
616 const uint8_t prior_result_code = dev->last_result_code;
617 const uint32_t prior_result_at_ms = dev->last_result_at_ms;
622 float read_value = 0.0F;
624 const bool acknowledged = read_with_value
625 ? hub_->read_heating_register(result.
device_id, fn, &read_value, EXCHANGE_RETRY_COUNT)
626 : hub_->send_heating_command(result.
device_id, fn, encoded_value);
629 if (
const auto *updated = registry_.get(result.
device_id);
630 updated !=
nullptr && updated->last_result_code != 0 &&
631 (updated->last_result_code != prior_result_code || updated->last_result_at_ms != prior_result_at_ms)) {
643 if (read_with_value) {
645 std::string(
"heating ") +
heating_function_name(fn) +
": window " + (read_value != 0.0F ?
"open" :
"closed");
659 if (hub_->oneway_controllers().get(controller_id) ==
nullptr) {
660 result.
message =
"no oneway_controllers identity with that id";
669 if (trimmed.empty() || trimmed.find_first_not_of(
"0123456789") != std::string::npos) {
670 result.
message =
"position must be a whole number between 0 and 100";
674 const unsigned long parsed = strtoul(trimmed.c_str(),
nullptr, 10);
675 if (parsed > ONEWAY_POSITION_FULLY_CLOSED) {
676 result.
message =
"position must be between 0 and 100";
681 hub_->send_oneway_position(controller_id,
static_cast<uint8_t
>(parsed));
685 result.
message =
"1W position command queued";
693 if (hub_->oneway_controllers().get(controller_id) ==
nullptr) {
694 result.
message =
"no oneway_controllers identity with that id";
699 hub_->send_oneway_unenroll(controller_id);
703 result.
message =
"1W remove-controller (0x39) queued";
708using ScanKeys = std::vector<std::array<uint8_t, AES_KEY_SIZE>>;
715 memcpy(keys[0].data(), hub_key, AES_KEY_SIZE);
716 for (
auto &entry : registry) {
720 const bool known = std::ranges::any_of(
721 keys, [&dev](
const auto &key) {
return memcmp(key.data(), dev.
system_key, AES_KEY_SIZE) == 0; });
725 memcpy(keys.back().data(), dev.
system_key, AES_KEY_SIZE);
733 const bool is_hub_key = memcmp(key, hub_key, AES_KEY_SIZE) == 0;
735 for (
auto &entry : registry) {
738 if (memcmp(device_key, key, AES_KEY_SIZE) == 0 && (is_hub_key || dev.
has_system_key))
747 const ScanKeys &keys,
bool reveal_keys) {
751 std::string known_body;
752 std::string unknown_body;
753 uint8_t unknown_count = 0;
754 for (uint8_t i = 0; i < state.
count; i++) {
759 const RollCallReplyKeys reply_keys{keys[slot].data(), keys[0].data(), slot != 0};
761 if (registered !=
nullptr) {
770 if (!known_body.empty())
771 body +=
"Known:\n" + known_body;
772 if (!unknown_body.empty())
773 body +=
"Unknown:\n" + unknown_body;
775 std::string message =
"Roll-call: " + std::to_string(state.
count) +
" device" + (state.
count == 1 ?
"" :
"s") +
776 " detected (" + std::to_string(state.
count - unknown_count) +
" known, " +
777 std::to_string(unknown_count) +
" unknown)\n";
794 " devices answered; the list below is truncated. Re-run to see whether other devices "
795 "appear, and raise SCAN_MAX_REPLIES if this install really is larger.\n";
804 if (!*initialized_) {
805 result.
message =
"hub is not initialized";
819 for (
size_t slot = 0; slot < keys.size(); slot++) {
821 ESP_LOGI(
detail::TAG,
"Roll-call with key check value %s (%u registered device%s)",
823 using_key == 1 ?
"" :
"s");
824 if (
run_roll_call_sweep(engine_, node_id_, keys[slot].data(),
static_cast<uint8_t
>(slot), *tuning_, state, attempt,
826 result.
message =
"failed to build roll-call request";
841 const std::string &index) {
846 const std::string &index) {
847 return probes_.probe_device(device_id, probe, index);
851 const std::string &first_index,
const std::string &last_index) {
856 const std::string &first_index,
const std::string &last_index) {
857 return probes_.probe_sweep(device_id, probe, first_index, last_index);
Owns the per-hub device table, update callbacks, and linked-remote associations.
IoDevice * get_declared(const std::string &device_id)
Retrieve a device the YAML declares, ignoring one a pairing added in this boot.
The main IO-Homecontrol component.
bool diagnostic_probes_enabled() const
Whether diagnostic probes are enabled for this build.
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 a paired device; delegates to DiagnosticProbes::probe_device(...
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 per index; delegates to DiagnosticProbes::probe_sweep().
Conversions and renderers shared by the hub and its Home Assistant entities.
IO-Homecontrol ESPHome component — protocol controller.
Hub-layer log tag and log/format helpers shared by the hub and its collaborators.
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...
std::string format_key_check_value(const uint8_t key[AES_KEY_SIZE])
A key check value: the first three bytes of the key's AES encryption of a zero block.
RollCallAttemptResult run_roll_call_sweep(ExchangeEngine &engine, const uint8_t *node_id, const uint8_t *key, uint8_t key_slot, const TuningConfig &tuning, ScanState &state, uint8_t &attempt, uint8_t total_attempts)
Run the full roll-call sweep under one key: every enabled pass on every channel.
bool create_identify(IoFrame &f, const uint8_t *own, const uint8_t *dst, bool low_power)
Build an authenticated device-identify request (0x1E).
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.
uint8_t count_enabled_passes(ScanPowerClasses selection)
How many of ROLL_CALL_PASSES selection actually calls.
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 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
Registers and functions of a 2W heater, one row of the descriptor table each.
@ MIDNIGHT_SYNC
Register 0x0130, read: the comfort/eco/auto setpoint block. Logged, never decoded.
@ POWER_ON
Register 0x012C, read: the paired-device list. Logged, never decoded.
@ SET_PRESENCE
Register 0x0110, write-only: someone is in the room (1) or not (0).
@ WINDOW_OPEN
Register 0x010E, read-only: the heater's window sensor, 0 closed / 1 open.
@ SET_OPEN_WINDOW_DETECTION
Register 0x0106, read/write: open-window detection on (1) or off (0).
@ SET_TEMPERATURE
Register 0x0103, read/write: comfort setpoint, tenths of a degree.
@ SET_MODE
Register 0x0100, read/write: operating mode (HeatingMode).
static ScanKeys collect_scan_keys(const uint8_t *hub_key, DeviceRegistry ®istry)
Collect the keys to sweep: the hub key, then each per-device key that differs from it and from every ...
static std::string build_scan_report(const ScanState &state, DeviceRegistry ®istry, ScanPowerClasses selection, const ScanKeys &keys, bool reveal_keys)
Build the final report: known/unknown grouping, header, the selection NOTE, and the truncation NOTE.
const char * power_save_mode_name(uint8_t mode)
Get a human-readable power save mode name.
@ FORCE_OPEN
Move to fully open at elevated priority; intended to bypass soft locks and environmental limits (conf...
static std::string bool_to_string(bool value)
constexpr float HEATING_TEMP_MAX_C
Highest setpoint this codec will encode.
constexpr uint8_t SCAN_CHANNEL_COUNT
const char * command_name(uint8_t cmd)
Get a human-readable name for any IO-Homecontrol command ID.
constexpr RollCallPass ROLL_CALL_PASSES[]
The roll-call's two fixed frame shapes, low-power first.
ScanPowerClasses
Which power classes scan_paired_devices() calls.
@ BOTH
Low-power pass, then always-alive pass (default).
const char * heating_function_name(HeatingFunction fn)
Stable lowercase name for a heating function ("power_on", "set_temperature", ...).
static size_t count_devices_using_key(DeviceRegistry ®istry, const uint8_t *hub_key, const uint8_t *key)
How many registered devices exchange with key: those with it as their own key, or,...
bool scan_power_classes_include(ScanPowerClasses selection, uint8_t power_save_mode)
Whether selection calls the roll-call pass for power_save_mode.
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 device_supports_climate_control(DeviceType type, uint8_t subtype)
Can a climate entity drive this device?
std::string format_hex_byte(uint8_t value)
Format a byte as two uppercase hex digits, no prefix.
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.
static std::string normalize_lower_argument(const std::string &value)
Lowercase + ASCII-trim a native-API string argument.
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 format_roll_call_responder(const ScanResponder &responder, const std::string &device_id, const IoDevice *registered, const RollCallReplyKeys &keys, RollCallKeyDisplay display)
Format one roll-call responder's report line(s).
@ BUILD_FAILED
create_discovery_request() failed; the caller should abort the whole scan.
std::string trim_ascii_whitespace(const std::string &value)
Trim leading and trailing ASCII whitespace from a string.
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.
@ PROG
Program mode; the device runs its own stored weekly schedule.
@ MANUAL
Manual mode; follows the last written setpoint.
@ AUTO
Automatic mode; the device manages the setpoint itself.
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.
std::vector< std::array< uint8_t, AES_KEY_SIZE > > ScanKeys
The keys one scan sweeps with: slot 0 is the hub key, the rest are the distinct per-device keys.
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 device_uses_heating_registers(DeviceType type, uint8_t subtype)
Does a climate entity drive this device over the private register protocol?
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.
constexpr uint8_t SCAN_MAX_REPLIES
Maximum distinct responders reported by one scan_paired_devices() call.
constexpr const char * MANAGEMENT_ACTION_PROBE_SWEEP
const char * device_name_validation_error_description(DeviceNameValidationError error)
Return a human-readable explanation for a device-name validation result.
RollCallKeyDisplay
How a responder line shows the key it answered under.
@ HIDDEN
Never print the key: its check value only. For anything Home Assistant records.
@ BANNERED
Print the key in a snippet, wrapped in the line's own DO-NOT-SHARE block.
std::string scan_power_classes_to_string(ScanPowerClasses value)
Format a ScanPowerClasses value for YAML/logs.
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.
Command builders for the IO‑Homecontrol protocol.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
The roll-call sweep: one keyed 0x2A broadcast, repeated over channels and power classes.
The only three things a probe needs from the hub, injected by ManagementActions.
A value token that maps to a fixed float (used by set_mode / set_presence / set_open_window_detection...
Runtime state of a paired IO‑Homecontrol device.
uint8_t system_key[AES_KEY_SIZE]
The device's own key; meaningful only when has_system_key.
bool has_system_key
YAML-declared (system_key:): this device keeps a key of its own.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + 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...
std::string log_message
When non-empty, the device log shows this instead of message.
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.
One roll-call frame shape, and how to listen for its replies.
The keys a responder line needs: the one a responder answered under, and the hub's.
uint8_t src[NODE_ID_SIZE]
Responder's node ID; also the dedup key.
uint8_t key_slot
Which key of the sweep it answered under: 0 is the hub key, n the n-th per-device key (RollCallKeys).
Accumulated state of one scan, threaded through every sweep.
uint8_t count
Entries in use.
bool truncated
A responder was dropped because the array was full.
ScanResponder responders[SCAN_MAX_REPLIES]
Distinct responders, in arrival order.
All runtime tunable parameters for pairing and radio diagnostics.