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 // Not YAML-selectable (nullptr, so callers fall back to a raw numeric value). Listed
215 // explicitly instead of default: so -Wswitch flags any new DeviceType that skips this switch.
220 return nullptr;
221 }
222 return nullptr;
223}
224
226 switch (type) {
227 // Cover types (position-controlled)
244
245 // Binary and other capabilities
250 case DeviceType::LOCK:
257 // Binary ventilation on/off; treated as switch
263
266 }
267 // No default: -Wswitch flags any new DeviceType not handled above (a hard error in the host unit
268 // build via -Werror=switch, a warning in the firmware build), rather than letting it fall through
269 // silently to UNKNOWN (which would make known_device_matches_entity_class() accept everything for
270 // that type).
272}
273
275 switch (device_capability_class(type)) {
277 return "cover";
279 return "light";
281 return "switch";
283 return "sensor";
285 return "beacon";
287 return "climate";
289 return "lock";
291 return "unknown";
292 }
293 return "unknown";
294}
295
299
301 DeviceCapabilityClass const capability_class = device_capability_class(type);
302 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
303}
304
308
312
317
319 // No default: every DeviceType is listed so -Wswitch catches any new one (error in the host
320 // unit build, warning in the firmware build), forcing an explicit tilt/vent decision.
321 switch (type) {
326 return true;
335 case DeviceType::LOCK:
348 return false;
349 }
350 return false;
351}
352
354 // No default: every DeviceType is listed so -Wswitch catches any new one (error in the host
355 // unit build, warning in the firmware build), forcing an explicit tilt/vent decision.
356 switch (type) {
359 return true;
368 case DeviceType::LOCK:
383 return false;
384 }
385 return false;
386}
387
390 return device_supports_tilt(type) ? "cover_position_tilt" : "cover_position";
392 return "binary_on_off";
393
394 switch (device_capability_class(type)) {
396 return "lock";
398 return "climate";
400 return "sensor";
402 return "beacon";
403 // COVER/LIGHT/SWITCH are handled by the position/binary guards above and never reach here;
404 // listed explicitly (no default:) so -Wswitch covers any new capability class.
409 return "unknown";
410 }
411 return "unknown";
412}
413
415 const char *name = device_type_name(type);
416 std::string raw = format_device_type_hex(type);
417 if (name != nullptr && strcmp(name, "unknown") != 0) {
418 return std::string(name) + " (" + raw + ")";
419 }
420 return raw;
421}
422
424 const char *name = yaml_device_type_name(type);
425 if (name != nullptr) {
426 return std::string("\"") + name + "\"";
427 }
428 return format_device_type_hex(type);
429}
430
431std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
432 bool metadata_complete, bool inverted, bool low_power) {
433 // A device that self-reported POWER_SAVE_LOW_POWER needs `low_power: true` so directed commands
434 // wake it with the long preamble — emit it in both snippet shapes, since a low-power device that
435 // also withheld its type is exactly the case a user most needs told.
436 const std::string low_power_line = low_power ? " low_power: true\n" : "";
437
438 if (!metadata_complete) {
439 return " <cover|light|switch|lock>:\n"
440 " - platform: home_io_control\n"
441 " name: \"My Device\"\n"
442 " io_device_id: \"" +
443 device_id +
444 "\"\n"
445 " # io_device_type: left unset — this device didn't report its type during\n"
446 " # discovery, so the controller learns it automatically from the next status\n"
447 " # reply. Add it explicitly once you see it logged, to skip re-learning on\n"
448 " # every future boot.\n" +
449 low_power_line;
450 }
451
452 const auto capability_class = device_capability_class(type);
453 const char *platform = pairing_platform_name(capability_class);
454 if (platform == nullptr)
455 return "";
456
457 std::string extra_lines;
458 if (capability_class == DeviceCapabilityClass::COVER && inverted)
459 extra_lines += " invert_position: true\n";
460 extra_lines += low_power_line;
461
462 const std::string subtype_line = " io_subtype: " + std::to_string(subtype) + "\n";
463
464 return " " + std::string(platform) +
465 ":\n"
466 " - platform: home_io_control\n"
467 " name: \"My Device\"\n"
468 " io_device_id: \"" +
469 device_id + "\"\n" + " io_device_type: " + format_device_type_for_yaml(type) + "\n" + subtype_line +
470 extra_lines;
471}
472
473} // namespace home_io_control
474} // 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.
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?
bool device_supports_climate_control(DeviceType type)
Does this device type support 2W climate/heating control (CMD_WRITE_PRIVATE 0x20)?
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.
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...
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.