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
8
9#include "proto_constants.h"
10#include "proto_device_types.h"
11
12#include <cmath>
13#include <cstdio>
14#include <cstring>
15
16namespace esphome {
17namespace home_io_control {
18
19namespace {
20
21constexpr size_t DEVICE_TYPE_HEX_STRING_BUFFER_SIZE = 8; ///< Buffer for strings such as "0x3FF" plus terminator.
22
23/// Format a raw device type as hexadecimal, for diagnostics and as the YAML fallback.
24std::string format_device_type_hex(DeviceType type) {
25 char buf[DEVICE_TYPE_HEX_STRING_BUFFER_SIZE];
26 snprintf(buf, sizeof(buf), "0x%02X", static_cast<unsigned>(type));
27 return std::string(buf);
28}
29
30} // namespace
31
33
35 switch (state) {
37 return "unknown";
39 return "dry";
41 return "rain";
42 }
43 return "unknown";
44}
45
46const char *battery_state_name(BatteryState state) {
47 switch (state) {
49 return "not_reported";
51 return "low";
53 return "normal";
55 return "full";
56 }
57 return "not_reported";
58}
59
60BatteryState decode_battery_state(uint8_t status_byte) {
61 // The mask leaves exactly two bits, so every result is a named enumerator.
62 return static_cast<BatteryState>((status_byte & STATUS_BATTERY_MASK) >> STATUS_BATTERY_SHIFT);
63}
64
66 switch (cmd) {
68 return "STOP";
70 return "FAVORITE";
72 return "VENT";
74 return "FORCE_OPEN";
75 default:
76 return "UNKNOWN_COVER_CMD";
77 }
78}
79
80DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype) {
81 return static_cast<DeviceType>((type_msb << DEVICE_TYPE_LOW_BITS_SHIFT) |
82 (type_subtype >> DEVICE_TYPE_HIGH_BITS_SHIFT));
83}
84
85uint8_t decode_packed_device_subtype(uint8_t type_subtype) { return type_subtype & DEVICE_SUBTYPE_MASK; }
86
87void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype) {
88 // The wire field holds ten type bits; anything above is dropped here rather than aliasing the
89 // subtype bits below.
90 const auto raw = static_cast<uint16_t>(static_cast<uint16_t>(type) & DEVICE_TYPE_WIRE_MASK);
91 type_msb = static_cast<uint8_t>(raw >> DEVICE_TYPE_LOW_BITS_SHIFT);
92 type_subtype = static_cast<uint8_t>((raw << DEVICE_TYPE_HIGH_BITS_SHIFT) | (subtype & DEVICE_SUBTYPE_MASK));
93}
94
95void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target,
96 float &position) {
97 bool const target_valid = target_raw <= STATUS_POS_MAX;
98 bool const current_valid = current_raw <= STATUS_POS_MAX;
99 float const decoded_current = current_valid ? current_raw * 100.0F / STATUS_POS_MAX : UNKNOWN_POSITION;
100
101 if (target_valid) {
102 target = target_raw * 100.0F / STATUS_POS_MAX;
103 } else if (is_stopped && current_valid) {
104 // Marker values such as D2 (stop) and D4 (keep position during tilt) exceed STATUS_POS_MAX.
105 // When the device says it is stopped and still gives a valid current position, use that as
106 // the effective target instead of discarding the target entirely.
107 target = decoded_current;
108 } else {
109 target = UNKNOWN_POSITION;
110 }
111
112 if (current_valid) {
113 position = decoded_current;
114 } else if (is_stopped && target_valid) {
115 position = target;
116 } else {
117 position = UNKNOWN_POSITION;
118 }
119}
120
121bool has_reached_target_position(float target, float position) {
122 if (target == UNKNOWN_POSITION || position == UNKNOWN_POSITION)
123 return false;
124 float const tolerance = STATUS_POS_TOLERANCE_RAW * 100.0F / STATUS_POS_MAX;
125 return std::fabs(target - position) <= tolerance;
126}
127
128float decode_tilt_report(uint16_t tilt_raw) {
129 if (tilt_raw > STATUS_POS_MAX)
130 return UNKNOWN_POSITION;
131 return 100.0F - (tilt_raw * 100.0F / STATUS_POS_MAX);
132}
133
134bool is_known_device_type(uint16_t raw) {
135 return raw != static_cast<uint16_t>(DeviceType::UNKNOWN) &&
136 find_exact_device_type_info(static_cast<DeviceType>(raw), 0) != nullptr;
137}
138
139const char *device_type_name(DeviceType type) {
140 const DeviceTypeInfo *row = find_exact_device_type_info(type, 0);
141 return row != nullptr ? row->name : "unknown";
142}
143
145 const DeviceTypeInfo *row = find_exact_device_type_info(type, 0);
146 return row != nullptr && (row->flags & device_type_flags::YAML_SELECTABLE) != 0 ? row->name : nullptr;
147}
148
150
152 switch (device_capability_class(type)) {
154 return "cover";
156 return "light";
158 return "switch";
160 return "sensor";
162 return "beacon";
164 return "climate";
166 return "lock";
168 return "heater_level";
170 return "unknown";
171 }
172 return "unknown";
173}
174
178
180 DeviceCapabilityClass const capability_class = device_capability_class(type);
181 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
182}
183
187
191
192bool device_has_no_motion(DeviceType type, uint8_t subtype) {
193 const HeatingProfile *profile = find_heating_profile(type, subtype);
194 // A heater commanded with an EXECUTE (levels, temperatures) never moves; a register heater is not
195 // polled with 0x03 at all.
196 return (profile != nullptr && profile->path != HeatingControlPath::PRIVATE_REGISTERS) ||
198}
199
200bool device_supports_climate_control(DeviceType type, uint8_t subtype) {
201 return find_heating_profile(type, subtype) != nullptr;
202}
203
208
210 // No default: every DeviceType is listed so -Wswitch catches any new one (error in the host
211 // unit build, warning in the firmware build), forcing an explicit tilt/vent decision.
212 switch (type) {
217 return true;
226 case DeviceType::LOCK:
249 return false;
250 }
251 return false;
252}
253
254bool device_takes_main_parameter_only(DeviceType type, uint8_t subtype) {
255 if (subtype == SUBTYPE_MAIN_PARAMETER_ONLY)
256 return true;
257 const DeviceTypeInfo *row = find_device_type_info(type, subtype);
258 return row != nullptr && (row->flags & device_type_flags::MAIN_PARAMETER_ONLY) != 0;
259}
260
262 // No default: every DeviceType is listed so -Wswitch catches any new one (error in the host
263 // unit build, warning in the firmware build), forcing an explicit tilt/vent decision.
264 switch (type) {
267 return true;
276 case DeviceType::LOCK:
301 return false;
302 }
303 return false;
304}
305
308 return device_supports_tilt(type) ? "cover_position_tilt" : "cover_position";
310 return "binary_on_off";
311
312 switch (device_capability_class(type)) {
314 return "lock";
316 return "heat_level";
318 return "climate";
320 return "sensor";
322 return "beacon";
323 // COVER/LIGHT/SWITCH are handled by the position/binary guards above and never reach here;
324 // listed explicitly (no default:) so -Wswitch covers any new capability class.
329 return "unknown";
330 }
331 return "unknown";
332}
333
335 const char *name = device_type_name(type);
336 std::string raw = format_device_type_hex(type);
337 if (name != nullptr && strcmp(name, "unknown") != 0) {
338 return std::string(name) + " (" + raw + ")";
339 }
340 return raw;
341}
342
344 const char *name = yaml_device_type_name(type);
345 if (name != nullptr) {
346 return std::string("\"") + name + "\"";
347 }
348 return format_device_type_hex(type);
349}
350
351std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
352 bool metadata_complete, bool inverted, bool low_power,
353 const std::string &system_key_hex) {
354 // A device that self-reported POWER_SAVE_LOW_POWER needs `low_power: true` so directed commands
355 // wake it with the long preamble — emit it in both snippet shapes, since a low-power device that
356 // also withheld its type is exactly the case a user most needs told.
357 const std::string low_power_line = low_power ? " low_power: true\n" : "";
358 const std::string system_key_line = system_key_hex.empty() ? "" : " system_key: \"" + system_key_hex + "\"\n";
359
360 if (!metadata_complete) {
361 return " <cover|light|switch|lock>:\n"
362 " - platform: home_io_control\n"
363 " name: \"My Device\"\n"
364 " io_device_id: \"" +
365 device_id +
366 "\"\n"
367 " # io_device_type: left unset — this device didn't report its type during\n"
368 " # discovery, so the controller learns it automatically from the next status\n"
369 " # reply. Add it explicitly once you see it logged, to skip re-learning on\n"
370 " # every future boot.\n" +
371 low_power_line + system_key_line;
372 }
373
374 const char *platform = device_platform_name(type, subtype);
375 if (platform == nullptr)
376 return "";
377
378 std::string extra_lines;
379 if (device_capability_class(type, subtype) == DeviceCapabilityClass::COVER && inverted)
380 extra_lines += " invert_position: true\n";
381 if (device_light_is_brightness_only(type, subtype))
382 extra_lines += " dimmable: true\n";
383 extra_lines += low_power_line;
384 extra_lines += system_key_line;
385
386 const std::string subtype_line = " io_subtype: " + std::to_string(subtype) + "\n";
387
388 return " " + std::string(platform) +
389 ":\n"
390 " - platform: home_io_control\n"
391 " name: \"My Device\"\n"
392 " io_device_id: \"" +
393 device_id + "\"\n" + " io_device_type: " + format_device_type_for_yaml(type) + "\n" + subtype_line +
394 extra_lines;
395}
396
397} // namespace home_io_control
398} // 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...
bool device_takes_main_parameter_only(DeviceType type, uint8_t subtype)
Does this device take movement commands as the main parameter only?
std::string format_device_type_for_yaml(DeviceType type)
Build the YAML value for a device's io_device_type key.
RainSensorState
What the hub's latest minimum-limitation read says about a rain-protection limit.
@ UNKNOWN
No usable reading: never read, or the last reads went unanswered.
@ DRY
The device answered and reports no rain-sized opening limit.
@ RAIN
The device answered and reports an opening limit of rain size.
const char * rain_sensor_state_name(RainSensorState state)
Get a human-readable name for a RainSensorState.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype)
Look up the heating profile for a (type, subtype).
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.
DeviceType
IO-Homecontrol node types.
@ EXTERIOR_VENETIAN_BLIND
Exterior venetian blind.
@ VERTICAL_INTERIOR_BLIND
Vertical interior blind.
@ CENTRAL_HOUSE_CONTROL
Central house control.
@ VERTICAL_EXTERIOR_AWNING
Vertical exterior awning.
@ HEATING_TEMPERATURE_INTERFACE
Heating temperature interface.
@ BEACON
Beacon (unpaired/announcement).
@ SECURE_CONFIGURATION_DEVICE
Secure configuration device (a commissioning tool, not an actuator).
@ INTERIOR_VENETIAN_BLIND
Interior venetian blind.
@ UNKNOWN
Unknown/unspecified device.
@ WINDOW_OPENER
Window opening actuator.
@ HEAT_RECOVERY_VENTILATION
Heat recovery ventilation.
@ HORIZONTAL_AWNING
Horizontal awning (open/close inverted).
@ TEST_AND_EVALUATION
Test and evaluation equipment.
@ DUAL_ROLLER_SHUTTER
Dual-section roller shutter.
@ REMOTE_CONTROLLER
Remote controller (all ten type bits set, packed bytes FF C0).
@ BIOCLIMATIC_PERGOLA
Bioclimatic pergola (louvre opening as the position).
bool device_supports_binary_control(DeviceType type)
Does this device type support binary on/off control?
@ PRIVATE_REGISTERS
CMD_WRITE_PRIVATE register reads and writes (proto_heating.h).
static constexpr uint8_t STATUS_BATTERY_MASK
Byte 1 bits 5-6: the device's coarse battery level (BatteryState).
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?
const DeviceTypeInfo * find_exact_device_type_info(DeviceType type, uint8_t subtype)
Look up the table row with exactly this (type, subtype), without falling back.
bool device_supports_climate_control(DeviceType type, uint8_t subtype)
Can a climate entity drive this device?
DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype)
Decode the 10-bit node type from two packed metadata bytes.
BatteryState
A device's coarse battery level, as it reports it in byte 1 of its status payload.
@ NORMAL_CHARGE
Battery neither low nor full.
@ NOT_REPORTED
The device does not report a battery (mains powered), or no status was read yet.
const char * cover_command_name(CoverCommand cmd)
Get a human-readable name for a CoverCommand.
const char * device_platform_name(DeviceType type, uint8_t subtype)
Name of the ESPHome platform that drives this (type, subtype).
bool device_supports_heat_level_control(DeviceType type)
Does this device take a heat level as its position (exterior heating)?
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, const std::string &system_key_hex)
Build the ready-to-paste YAML block describing a device, for both a fully-decoded device and one whos...
const DeviceTypeInfo * find_device_type_info(DeviceType type, uint8_t subtype)
Look up the table row for a (type, subtype) pair.
bool is_known_device_type(uint16_t raw)
True when a raw 10-bit device-type value is a named DeviceType enumerator.
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 STATUS_BATTERY_SHIFT
Right shift that brings STATUS_BATTERY_MASK down to 0-3.
bool device_has_no_motion(DeviceType type, uint8_t subtype)
Does this device never move, so a status reply must not start the movement settle loop?
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).
@ HEATER_LEVEL
Heater with a heat level in the position field (exterior heating).
@ COVER
Position‑controlled cover (shutter/blind/awning).
BatteryState decode_battery_state(uint8_t status_byte)
Decode the coarse battery level from byte 1 of a status payload (CMD_PRIVATE_RESP to a status poll,...
const char * battery_state_name(BatteryState state)
Get the name a battery level is published under.
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?
bool device_light_is_brightness_only(DeviceType type, uint8_t subtype)
Is this light limited to brightness (no colour, no colour temperature)?
IO-Homecontrol command IDs, result codes and protocol enumerations.
IO-Homecontrol device-type model, capabilities and runtime device state.
The IO node-type and subtype table: names, entity class, platform and behaviour flags.
Heating profiles: what a climate card offers for each kind of heater, and how.
One row of the node-type / subtype table.
uint8_t flags
device_type_flags bits.
const char * name
Type row: lowercase identifier ("roller_shutter").
HeatingControlPath path
How the heater is commanded and read.