Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_device_model.cpp
Go to the documentation of this file.
1/// @file proto_device_model.cpp
2/// @brief Device-type capabilities, packed-metadata decoding and position reports.
3/// @ingroup hioc_protocol
4
6
7#include <cmath>
8#include <cstdio>
9#include <cstring>
10
11namespace esphome {
12namespace home_io_control {
13
14namespace {
15
16constexpr size_t DEVICE_TYPE_HEX_STRING_BUFFER_SIZE = 8; ///< Buffer for strings such as "0x11" plus terminator.
17
18/// Format a raw device type as hexadecimal, for diagnostics and as the YAML fallback.
19std::string format_device_type_hex(DeviceType type) {
20 char buf[DEVICE_TYPE_HEX_STRING_BUFFER_SIZE];
21 snprintf(buf, sizeof(buf), "0x%02X", static_cast<uint8_t>(type));
22 return std::string(buf);
23}
24
25/// Map a capability class to the corresponding ESPHome platform name, or nullptr when the
26/// class has no dedicated platform yet.
27const char *pairing_platform_name(DeviceCapabilityClass capability_class) {
28 switch (capability_class) {
30 return "cover";
32 return "light";
34 return "switch";
36 return "lock";
37 default:
38 return nullptr;
39 }
40}
41
42} // namespace
43
45
47 switch (cmd) {
49 return "STOP";
51 return "FAVORITE";
53 return "VENT";
55 return "FORCE_OPEN";
56 default:
57 return "UNKNOWN_COVER_CMD";
58 }
59}
60
61DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype) {
62 return static_cast<DeviceType>((type_msb << DEVICE_TYPE_LOW_BITS_SHIFT) |
63 (type_subtype >> DEVICE_TYPE_HIGH_BITS_SHIFT));
64}
65
66uint8_t decode_packed_device_subtype(uint8_t type_subtype) { return type_subtype & DEVICE_SUBTYPE_MASK; }
67
68void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype) {
69 const auto raw = static_cast<uint8_t>(type);
70 type_msb = static_cast<uint8_t>(raw >> DEVICE_TYPE_LOW_BITS_SHIFT);
71 type_subtype = static_cast<uint8_t>((raw << DEVICE_TYPE_HIGH_BITS_SHIFT) | (subtype & DEVICE_SUBTYPE_MASK));
72}
73
74void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target,
75 float &position) {
76 bool const target_valid = target_raw <= STATUS_POS_MAX;
77 bool const current_valid = current_raw <= STATUS_POS_MAX;
78 float const decoded_current = current_valid ? current_raw * 100.0F / STATUS_POS_MAX : UNKNOWN_POSITION;
79
80 if (target_valid) {
81 target = target_raw * 100.0F / STATUS_POS_MAX;
82 } else if (is_stopped && current_valid) {
83 // Marker values such as D2 (stop) and D4 (keep position during tilt) exceed STATUS_POS_MAX.
84 // When the device says it is stopped and still gives a valid current position, use that as
85 // the effective target instead of discarding the target entirely.
86 target = decoded_current;
87 } else {
88 target = UNKNOWN_POSITION;
89 }
90
91 if (current_valid) {
92 position = decoded_current;
93 } else if (is_stopped && target_valid) {
94 position = target;
95 } else {
96 position = UNKNOWN_POSITION;
97 }
98}
99
100bool has_reached_target_position(float target, float position) {
101 if (target == UNKNOWN_POSITION || position == UNKNOWN_POSITION)
102 return false;
103 float const tolerance = STATUS_POS_TOLERANCE_RAW * 100.0F / STATUS_POS_MAX;
104 return std::fabs(target - position) <= tolerance;
105}
106
107float decode_tilt_report(uint16_t tilt_raw) {
108 if (tilt_raw > STATUS_POS_MAX)
109 return UNKNOWN_POSITION;
110 return 100.0F - (tilt_raw * 100.0F / STATUS_POS_MAX);
111}
112
113const char *device_type_name(DeviceType type) {
114 switch (type) {
116 return "unknown";
118 return "venetian_blind";
120 return "roller_shutter";
122 return "screen";
124 return "awning";
126 return "window_opener";
128 return "garage_opener";
130 return "light";
132 return "gate_opener";
134 return "rolling_door_opener";
136 return "blind";
138 return "dual_shutter";
140 return "on_off_switch";
142 return "horizontal_awning";
144 return "external_venetian_blind";
146 return "louvre_blind";
148 return "curtain_track";
150 return "swinging_shutter";
151 case DeviceType::LOCK:
152 return "lock";
154 return "beacon";
156 return "heating_temperature_interface";
158 return "ventilation_point";
160 return "exterior_heating";
162 return "heat_pump";
164 return "intrusion_alarm";
165 }
166
167 return "unknown";
168}
169
171 switch (type) {
173 return "unknown";
175 return "venetian_blind";
177 return "roller_shutter";
179 return "awning";
181 return "window_opener";
183 return "garage_opener";
185 return "light";
187 return "gate_opener";
189 return "rolling_door_opener";
190 case DeviceType::LOCK:
191 return "lock";
193 return "blind";
195 return "screen";
197 return "dual_shutter";
199 return "heating_temperature_interface";
201 return "on_off_switch";
203 return "horizontal_awning";
205 return "external_venetian_blind";
207 return "louvre_blind";
209 return "curtain_track";
211 return "intrusion_alarm";
213 return "swinging_shutter";
214 default:
215 return nullptr;
216 }
217}
218
220 switch (type) {
221 // Cover types (position-controlled)
238
239 // Binary and other capabilities
244 case DeviceType::LOCK:
251 // Binary ventilation on/off; treated as switch
257
259 default:
261 }
262}
263
265 switch (device_capability_class(type)) {
267 return "cover";
269 return "light";
271 return "switch";
273 return "sensor";
275 return "beacon";
277 return "climate";
279 return "lock";
281 default:
282 return "unknown";
283 }
284}
285
289
291 DeviceCapabilityClass const capability_class = device_capability_class(type);
292 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
293}
294
298
303
305 switch (type) {
310 return true;
311 default:
312 return false;
313 }
314}
315
317 switch (type) {
320 return true;
321 default:
322 return false;
323 }
324}
325
328 return device_supports_tilt(type) ? "cover_position_tilt" : "cover_position";
330 return "binary_on_off";
331
332 switch (device_capability_class(type)) {
334 return "lock";
336 return "climate";
338 return "sensor";
340 return "beacon";
342 default:
343 return "unknown";
344 }
345}
346
348 const char *name = device_type_name(type);
349 std::string raw = format_device_type_hex(type);
350 if (name != nullptr && strcmp(name, "unknown") != 0) {
351 return std::string(name) + " (" + raw + ")";
352 }
353 return raw;
354}
355
357 const char *name = yaml_device_type_name(type);
358 if (name != nullptr) {
359 return std::string("\"") + name + "\"";
360 }
361 return format_device_type_hex(type);
362}
363
364std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
365 bool metadata_complete, bool inverted) {
366 if (!metadata_complete) {
367 return " <cover|light|switch|lock>:\n"
368 " - platform: home_io_control\n"
369 " name: \"My Device\"\n"
370 " io_device_id: \"" +
371 device_id +
372 "\"\n"
373 " # io_device_type: left unset — this device didn't report its type during\n"
374 " # discovery, so the controller learns it automatically from the next status\n"
375 " # reply. Add it explicitly once you see it logged, to skip re-learning on\n"
376 " # every future boot.\n";
377 }
378
379 const auto capability_class = device_capability_class(type);
380 const char *platform = pairing_platform_name(capability_class);
381 if (platform == nullptr)
382 return "";
383
384 std::string extra_lines;
385 if (capability_class == DeviceCapabilityClass::COVER && inverted)
386 extra_lines += " invert_position: true\n";
387
388 const std::string subtype_line = " io_subtype: " + std::to_string(subtype) + "\n";
389
390 return " " + std::string(platform) +
391 ":\n"
392 " - platform: home_io_control\n"
393 " name: \"My Device\"\n"
394 " io_device_id: \"" +
395 device_id + "\"\n" + " io_device_type: " + format_device_type_for_yaml(type) + "\n" + subtype_line +
396 extra_lines;
397}
398
399} // namespace home_io_control
400} // namespace esphome
const char * device_operation_profile_name(DeviceType type)
Human‑readable operation profile name for a device type.
void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype)
Encode a DeviceType/subtype pair into the two-byte packed metadata format used by discovery responses...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint16_t STATUS_POS_MAX
In status responses, position is encoded as a 16-bit value where 0x0000 = fully open (0%) and 0xC800 ...
std::string format_device_type_for_yaml(DeviceType type)
Build the YAML value for a device's io_device_type key.
static constexpr uint8_t DEVICE_TYPE_HIGH_BITS_SHIFT
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ BEACON
Beacon (unpaired/announcement).
@ EXTERNAL_VENETIAN_BLIND
External venetian blind.
@ UNKNOWN
Unknown/unspecified device.
@ WINDOW_OPENER
Window opening actuator.
@ HORIZONTAL_AWNING
Horizontal awning (open/close inverted).
static constexpr uint16_t STATUS_POS_TOLERANCE_RAW
Target-reached tolerance expressed in raw IO-homecontrol position units.
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...
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
bool default_inverted_for_type(DeviceType type)
Determine whether a device type has inverted position mapping by default.
CoverCommand
Named device commands for cover-type actuators.
@ FAVORITE
Move to stored favorite/"My" position.
@ VENT
Move to ventilation position (window-type devices).
@ FORCE_OPEN
Move to fully open at elevated priority; intended to bypass soft locks and environmental limits (conf...
bool device_supports_position_control(DeviceType type)
Does this device type support precise position control (0–100)?
bool device_supports_vent(DeviceType type)
Does this device type support the ventilation position command?
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
bool device_supports_binary_control(DeviceType type)
Does this device type support binary on/off control?
float decode_tilt_report(uint16_t tilt_raw)
Decode tilt angle from raw 16‑bit value.
bool device_supports_lock_control(DeviceType type)
Does this device type support binary lock/unlock control via execute commands?
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode a protocol-packed device type from two metadata bytes.
const char * cover_command_name(CoverCommand cmd)
Get a human-readable name for a CoverCommand.
const char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
bool device_supports_status_requests(DeviceType type)
Does this device type support status request commands (0x03)?
static constexpr uint8_t DEVICE_SUBTYPE_MASK
bool has_reached_target_position(float target, float position)
Has the device reached its target within tolerance?
const char * yaml_device_type_name(DeviceType type)
Return the YAML-friendly device-type name for types exposed in the Python schema.
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
@ CLIMATE
Climate device (heating/cooling).
@ COVER
Position‑controlled cover (shutter/blind/awning).
static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT
std::string format_device_type_diagnostic(DeviceType type)
Human-readable device type string for diagnostics, including the raw numeric value.
void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position)
Decode target/current position values from a status frame.
bool device_supports_tilt(DeviceType type)
Does this device type support tilt (slat angle) control?
IO-Homecontrol device-type model, capabilities and runtime device state.