Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
entity_helpers.h
Go to the documentation of this file.
1#pragma once
2
3/// @file entity_helpers.h
4/// @brief Conversions and renderers shared by the hub and its Home Assistant entities.
5/// @ingroup hioc_hub
6///
7/// Carries no dependency on the hub itself, so an entity can include this header instead of the
8/// hub's private helpers (make include-graph enforces that split).
9
10#include "log_helpers.h"
11#include "proto_codecs.h"
12#include "proto_constants.h"
13#include "proto_device_model.h"
14#include "proto_frame.h"
15#include "proto_sizes.h"
16
17#include <array>
18#include <cinttypes>
19#include <cmath>
20#include <cstdint>
21#include <cstdio>
22#include <cstring>
23#include <string>
24
25namespace esphome {
26namespace home_io_control {
27namespace detail {
28
29// ============================================================================
30// Shared constants
31// ============================================================================
32
34 50.0F; ///< Shared 0-100 cutoff: values below this mean binary "on".
35
36// ============================================================================
37// Percent conversion helpers
38// ============================================================================
39
40/// @brief Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
41///
42/// Rounds rather than truncates: HA quantizes call values to 0-255 before they ever reach us, so
43/// its "50%" is 128/255=0.50196, not exactly 0.5 — a truncating cast compounds that quantization
44/// into a consistent ~1% bias, caught on real hardware in both platform_cover.cpp (position and
45/// tilt) and platform_light.cpp (brightness). Callers apply their own invert/complement logic
46/// (e.g. `1.0F - fraction`) before calling this; it only owns the rounding.
47/// @param fraction Value in [0.0, 1.0].
48/// @return Rounded 0-100 percent.
49inline uint8_t round_percent(float fraction) { return static_cast<uint8_t>(std::lround(fraction * 100.0F)); }
50
51// ============================================================================
52// Last-command record rendering
53// ============================================================================
54
55/// @brief Render a Command Originator byte as "name(0xXX)", e.g. "user_remote(0x01)".
56///
57/// The one rendering every originator display shares (the "Last Command Source" sensor and the 0x71
58/// status-update log line), so a byte with no ORIGINATOR_* case reads "unknown(0x0A)" everywhere
59/// rather than being silently dropped or mislabelled.
60/// @param originator Command Originator byte (ORIGINATOR_*).
61/// @return "name(0xXX)".
62inline std::string format_originator(uint8_t originator) {
63 return format_name_and_hex(originator_name(originator), originator);
64}
65
66/// @brief Render the "Last Commanded By" sensor string.
67///
68/// Always leads with the raw node ID — that is the diagnostic value, and the only thing a user can
69/// match against a remote they own. The qualifier is additive, never a substitute: a device naming
70/// its own ID is NOT reliably "the button on the motor" (the one non-shutter this project has data
71/// on, a mains gate, names its own ID with an undefined originator), so the cause belongs to the
72/// separate originator sensor, not to this one's wording.
73/// @param dev Device record to read.
74/// @param hub_node_id This hub's own 3-byte node ID.
75/// @return e.g. "3B74DC", "C0FFEE (this hub)", "2FE2D2 (this device)"; empty before the first record.
76inline std::string describe_last_commander(const IoDevice &dev, const uint8_t *hub_node_id) {
77 if (!dev.has_last_command)
78 return {};
79 std::string out = node_id_to_string(dev.last_commander);
80 if (memcmp(dev.last_commander, hub_node_id, NODE_ID_SIZE) == 0) {
81 out += " (this hub)";
82 } else if (memcmp(dev.last_commander, dev.node_id, NODE_ID_SIZE) == 0) {
83 out += " (this device)";
84 }
85 return out;
86}
87
88/// @brief Render the "Last Command Source" sensor string.
89///
90/// Rendered by format_originator(), so an undecoded byte keeps its raw hex. The decode is
91/// field-validated for roller shutters (a clean 0x00/0x01 split, remote vs. motor
92/// button); gates, lights and multi-channel units are not validated and are expected to surface
93/// undecoded values here — which is the point of keeping the raw hex in the string.
94/// @param dev Device record to read.
95/// @return e.g. "user_remote(0x01)"; empty before the first record.
96inline std::string describe_last_command_source(const IoDevice &dev) {
97 if (!dev.has_last_command)
98 return {};
100}
101
102// ============================================================================
103// Limitation read rendering
104// ============================================================================
105
106/// Buffer size for the limitation fragments below: the longest is "selector (D8 01)" or "0xFD (unlimited)".
107inline constexpr size_t LIMITATION_TEXT_BUFFER_SIZE = 32;
108
109/// @brief Render a limitation parameter id: "MP" for the main parameter, "FP<n>" for 1-16, else hex.
110inline std::string format_limitation_param(uint8_t parameter_id) {
111 if (parameter_id == LIMITATION_PARAM_MP) {
112 return "MP";
113 }
114 std::array<char, LIMITATION_TEXT_BUFFER_SIZE> buffer{};
115 if (parameter_id >= LIMITATION_PARAM_FP_FIRST && parameter_id <= LIMITATION_PARAM_FP_LAST) {
116 std::snprintf(buffer.data(), buffer.size(), "FP%u", parameter_id);
117 } else {
118 std::snprintf(buffer.data(), buffer.size(), "0x%02X", parameter_id);
119 }
120 return std::string(buffer.data());
121}
122
123/// @brief Render a limitation value: a percentage, or the raw selector when it is not a position.
124///
125/// Values above STATUS_POS_MAX are position selectors (stop, unknown, ...), never percentages, so
126/// they print as `selector (D8 01)`. A percentage always keeps its raw bytes beside it, so a wrong
127/// layout assumption stays visible in the log.
128/// @param value_raw Big-endian raw value (LimitationStatus::value_raw).
129/// @return e.g. "93% (BA 00)", "46.5% (5D 00)" or "selector (D8 01)".
130inline std::string format_limitation_value(uint16_t value_raw) {
131 std::array<char, LIMITATION_TEXT_BUFFER_SIZE> buffer{};
132 const unsigned hi = value_raw >> BITS_PER_BYTE;
133 const unsigned lo = value_raw & 0xFFU;
134 if (value_raw > STATUS_POS_MAX) {
135 std::snprintf(buffer.data(), buffer.size(), "selector (%02X %02X)", hi, lo);
136 return std::string(buffer.data());
137 }
138 const double percent = value_raw * 100.0 / STATUS_POS_MAX;
139 const bool whole = (value_raw * 100U) % STATUS_POS_MAX == 0;
140 std::snprintf(buffer.data(), buffer.size(), whole ? "%.0f%% (%02X %02X)" : "%.1f%% (%02X %02X)", percent, hi, lo);
141 return std::string(buffer.data());
142}
143
144/// @brief Render a limitation time code with its raw byte: "0x1D (900 s)", "0xFD (unlimited)", "0xFE (code 254)".
145inline std::string format_limitation_time(uint8_t time_raw) {
146 std::array<char, LIMITATION_TEXT_BUFFER_SIZE> buffer{};
147 if (time_raw == LIMITATION_TIME_UNLIMITED) {
148 std::snprintf(buffer.data(), buffer.size(), "0x%02X (unlimited)", time_raw);
149 } else if (time_raw > LIMITATION_TIME_MAX_COUNTED) {
150 std::snprintf(buffer.data(), buffer.size(), "0x%02X (code %u)", time_raw, time_raw);
151 } else {
152 std::snprintf(buffer.data(), buffer.size(), "0x%02X (%" PRIu32 " s)", time_raw, limitation_time_seconds(time_raw));
153 }
154 return std::string(buffer.data());
155}
156
157/// @brief Render a decoded limitation reply on one line, raw bytes beside every decoded value.
158/// @return e.g. "limitation (assumed layout): param=MP value=93% (BA 00) originator=rain_sensor(0x02) time=0x1D (900
159/// s)".
160inline std::string format_limitation_status(const LimitationStatus &status) {
161 return "limitation (assumed layout): param=" + format_limitation_param(status.parameter_id) +
162 " value=" + format_limitation_value(status.value_raw) + " originator=" + format_originator(status.originator) +
163 " time=" + format_limitation_time(status.time_raw);
164}
165
166/// @brief Render a CMD_LIMITATION_STATUS_RESP frame for a log line or probe report.
167/// @param frame A parsed CMD_LIMITATION_STATUS_RESP frame.
168/// @return format_limitation_status() of the decode, or "limitation (assumed layout): unexpected
169/// length N" when the payload is not the assumed five bytes.
170inline std::string describe_limitation_reply(const IoFrame &frame) {
171 LimitationStatus status;
172 if (!decode_limitation_status(frame, status))
173 return "limitation (assumed layout): unexpected length " + std::to_string(frame.data_len);
174 return format_limitation_status(status);
175}
176
177/// @brief Render a CMD_LIMITATION_STATUS_REQ frame for a log line.
178/// @param frame A parsed CMD_LIMITATION_STATUS_REQ frame.
179/// @return e.g. "limitation request (assumed layout): type=minimum param=MP", or "... unexpected payload"
180/// when the length or selector is not one of the known shapes.
181inline std::string describe_limitation_request(const IoFrame &frame) {
182 LimitationType type;
183 uint8_t parameter_id = 0;
184 if (!decode_limitation_request(frame, type, parameter_id))
185 return "limitation request (assumed layout): unexpected payload";
186 return std::string("limitation request (assumed layout): type=") +
187 (type == LimitationType::MINIMUM ? "minimum" : "maximum") + " param=" + format_limitation_param(parameter_id);
188}
189
190} // namespace detail
191} // namespace home_io_control
192} // namespace esphome
Hub-layer log tag and log/format helpers shared by the hub and its collaborators.
constexpr size_t LIMITATION_TEXT_BUFFER_SIZE
Buffer size for the limitation fragments below: the longest is "selector (D8 01)" or "0xFD (unlimited...
std::string describe_limitation_reply(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_RESP frame for a log line or probe report.
std::string format_name_and_hex(const char *name, uint8_t value)
Format a name/value pair as "name(0xXX)", e.g. "execute(0x00)".
Definition log_helpers.h:42
std::string format_limitation_param(uint8_t parameter_id)
Render a limitation parameter id: "MP" for the main parameter, "FP<n>" for 1-16, else hex.
constexpr float BINARY_ENTITY_ON_POSITION_THRESHOLD
Shared 0-100 cutoff: values below this mean binary "on".
std::string describe_last_command_source(const IoDevice &dev)
Render the "Last Command Source" sensor string.
std::string format_limitation_value(uint16_t value_raw)
Render a limitation value: a percentage, or the raw selector when it is not a position.
std::string describe_limitation_request(const IoFrame &frame)
Render a CMD_LIMITATION_STATUS_REQ frame for a log line.
uint8_t round_percent(float fraction)
Convert a 0.0-1.0 HA fraction (position, tilt, or brightness) to a 0-100 IO percent.
std::string format_limitation_status(const LimitationStatus &status)
Render a decoded limitation reply on one line, raw bytes beside every decoded value.
std::string format_limitation_time(uint8_t time_raw)
Render a limitation time code with its raw byte: "0x1D (900 s)", "0xFD (unlimited)",...
std::string describe_last_commander(const IoDevice &dev, const uint8_t *hub_node_id)
Render the "Last Commanded By" sensor string.
std::string format_originator(uint8_t originator)
Render a Command Originator byte as "name(0xXX)", e.g.
bool decode_limitation_status(const IoFrame &f, LimitationStatus &out)
Decode a limitation reply (assumed layout, see LimitationStatus).
uint32_t limitation_time_seconds(uint8_t time_raw)
Remaining time of a limitation in seconds.
bool decode_limitation_request(const IoFrame &f, LimitationType &type, uint8_t &parameter_id)
Decode a limitation request (assumed layout [selector] [parameter id] [00]).
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.
LimitationType
On-air selector byte (request data[0]) choosing which resulting limit to read.
@ MINIMUM
Resulting minimum limit; rain protection raises it (a floor on the position value).
const char * originator_name(uint8_t originator)
Get a human-readable name for a command originator byte.
Device-name, address-classification and 1W-frame codecs.
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol device-type model, capabilities and runtime device state.
IO-Homecontrol 2W frame container: control bytes, IoFrame and (de)serialization.
Fundamental IO-Homecontrol frame and crypto size constants.
Runtime state of a paired IO‑Homecontrol device.
uint8_t last_commander[NODE_ID_SIZE]
Node ID of the controller that last commanded this device, as reported verbatim by the device in its ...
uint8_t last_command_originator
That command's Command Originator byte (ORIGINATOR_* in proto_constants.h).
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
bool has_last_command
True once a status reply carried a well-formed last-command record.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
Definition proto_frame.h:93
uint8_t data_len
Actual length of data.
One decoded CMD_LIMITATION_STATUS_RESP payload.
uint8_t parameter_id
data[0]; LIMITATION_PARAM_MP for the main parameter.
uint8_t originator
data[3], Command Originator that imposed the limit (ORIGINATOR_*).
uint16_t value_raw
data[1..2], big-endian, position coding (STATUS_POS_MAX = 100 %).
uint8_t time_raw
data[4], remaining-time code; see limitation_time_seconds().