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 switch (state) {
35 return "unknown";
37 return "dry";
39 return "rain";
40 }
41 return "unknown";
42}
43
44ExecutionState execution_state_of(uint8_t status_byte) {
45 const uint8_t state = error_resp_state(status_byte);
46 return state <= static_cast<uint8_t>(ExecutionState::DONE) ? static_cast<ExecutionState>(state)
48}
49
51 switch (state) {
53 return "non_executing";
55 return "error";
57 return "waiting_for_executing";
59 return "waiting_for_power";
61 return "executing";
63 return "done";
65 default:
66 return "undefined";
67 }
68}
69
70const char *battery_state_name(BatteryState state) {
71 switch (state) {
73 return "not_reported";
75 return "low";
77 return "normal";
79 return "full";
80 }
81 return "not_reported";
82}
83
84const char *travel_speed_name(TravelSpeed speed) {
85 switch (speed) {
87 return "default";
89 return "silent";
91 return "fast";
92 }
93 return "fast";
94}
95
96bool parse_travel_speed(const std::string &name, TravelSpeed &speed) {
98 if (name == travel_speed_name(candidate)) {
99 speed = candidate;
100 return true;
101 }
102 }
103 return false;
104}
105
106BatteryState decode_battery_state(uint8_t status_byte) {
107 // The mask leaves exactly two bits, so every result is a named enumerator.
108 return static_cast<BatteryState>((status_byte & STATUS_BATTERY_MASK) >> STATUS_BATTERY_SHIFT);
109}
110
112 switch (cmd) {
114 return "STOP";
116 return "FORCE_OPEN";
117 default: {
118 const NamedPosition *named = find_named_position(cmd);
119 return named != nullptr ? named->name : "UNKNOWN_COVER_CMD";
120 }
121 }
122}
123
124DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype) {
125 return static_cast<DeviceType>((type_msb << DEVICE_TYPE_LOW_BITS_SHIFT) |
126 (type_subtype >> DEVICE_TYPE_HIGH_BITS_SHIFT));
127}
128
129uint8_t decode_packed_device_subtype(uint8_t type_subtype) { return type_subtype & DEVICE_SUBTYPE_MASK; }
130
131void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype) {
132 // The wire field holds ten type bits; anything above is dropped here rather than aliasing the
133 // subtype bits below.
134 const auto raw = static_cast<uint16_t>(static_cast<uint16_t>(type) & DEVICE_TYPE_WIRE_MASK);
135 type_msb = static_cast<uint8_t>(raw >> DEVICE_TYPE_LOW_BITS_SHIFT);
136 type_subtype = static_cast<uint8_t>((raw << DEVICE_TYPE_HIGH_BITS_SHIFT) | (subtype & DEVICE_SUBTYPE_MASK));
137}
138
139void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target,
140 float &position) {
141 bool const target_valid = target_raw <= STATUS_POS_MAX;
142 bool const current_valid = current_raw <= STATUS_POS_MAX;
143 float const decoded_current = current_valid ? current_raw * 100.0F / STATUS_POS_MAX : UNKNOWN_POSITION;
144
145 if (target_valid) {
146 target = target_raw * 100.0F / STATUS_POS_MAX;
147 } else if (is_stopped && current_valid) {
148 // Marker values such as D2 (stop) and D4 (keep position during tilt) exceed STATUS_POS_MAX.
149 // When the device says it is stopped and still gives a valid current position, use that as
150 // the effective target instead of discarding the target entirely.
151 target = decoded_current;
152 } else {
153 target = UNKNOWN_POSITION;
154 }
155
156 if (current_valid) {
157 position = decoded_current;
158 } else if (is_stopped && target_valid) {
159 position = target;
160 } else {
161 position = UNKNOWN_POSITION;
162 }
163}
164
165bool has_reached_target_position(float target, float position) {
166 if (target == UNKNOWN_POSITION || position == UNKNOWN_POSITION)
167 return false;
168 float const tolerance = STATUS_POS_TOLERANCE_RAW * 100.0F / STATUS_POS_MAX;
169 return std::fabs(target - position) <= tolerance;
170}
171
172float decode_tilt_report(uint16_t tilt_raw) {
173 if (tilt_raw > STATUS_POS_MAX)
174 return UNKNOWN_POSITION;
175 return 100.0F - (tilt_raw * 100.0F / STATUS_POS_MAX);
176}
177
178bool is_known_device_type(uint16_t raw) {
179 return raw != static_cast<uint16_t>(DeviceType::UNKNOWN) &&
180 find_exact_device_type_info(static_cast<DeviceType>(raw), 0) != nullptr;
181}
182
183const char *device_type_name(DeviceType type) {
184 const DeviceTypeInfo *row = find_exact_device_type_info(type, 0);
185 return row != nullptr ? row->name : "unknown";
186}
187
189 const DeviceTypeInfo *row = find_exact_device_type_info(type, 0);
190 return row != nullptr && (row->flags & device_type_flags::YAML_SELECTABLE) != 0 ? row->name : nullptr;
191}
192
194
196 switch (device_capability_class(type)) {
198 return "cover";
200 return "light";
202 return "switch";
204 return "sensor";
206 return "beacon";
208 return "climate";
210 return "lock";
212 return "heater_level";
214 return "water_heater";
216 return "unknown";
217 }
218 return "unknown";
219}
220
224
226 DeviceCapabilityClass const capability_class = device_capability_class(type);
227 return capability_class == DeviceCapabilityClass::LIGHT || capability_class == DeviceCapabilityClass::SWITCH;
228}
229
233
237
238bool device_has_no_motion(DeviceType type, uint8_t subtype) {
239 // The control path is a fact of the (type, subtype); a model's profile keeps it.
240 const HeatingProfile *profile = find_heating_profile(type, subtype, DeviceModel::NONE);
241 // A heater commanded with an EXECUTE (levels, temperatures) never moves; a register heater is not
242 // polled with 0x03 at all. A water heater answers a status read but never moves either.
243 return (profile != nullptr && profile->path != HeatingControlPath::PRIVATE_REGISTERS) ||
245 has_feature(device_features(type, subtype, DeviceModel::NONE), device_feature::WATER_HEATER);
246}
247
248bool device_supports_climate_control(DeviceType type, uint8_t subtype) {
249 // A model never adds or removes a profile: it only replaces the (type, subtype) one.
250 return find_heating_profile(type, subtype, DeviceModel::NONE) != nullptr;
251}
252
257
259
260const char *device_operation_profile_name(DeviceType type, uint8_t subtype) {
262 // A cover has no model rows; the operation profile names the wire type.
263 const DeviceFeatures features = device_features(type, subtype, DeviceModel::NONE);
264 if (!has_feature(features, device_feature::LEVELS))
265 return "cover_discrete";
266 return has_feature(features, device_feature::TILT) ? "cover_position_tilt" : "cover_position";
267 }
269 return "binary_on_off";
270
271 switch (device_capability_class(type)) {
273 return "lock";
275 return "heat_level";
277 return "water_heater";
279 return "climate";
281 return "sensor";
283 return "beacon";
284 // COVER/LIGHT/SWITCH are handled by the position/binary guards above and never reach here;
285 // listed explicitly (no default:) so -Wswitch covers any new capability class.
290 return "unknown";
291 }
292 return "unknown";
293}
294
297 ? dev.entity_class
299 DeviceFeatures features;
300 if (dev.type == DeviceType::UNKNOWN || find_exact_device_type_info(dev.type, 0) == nullptr) {
301 features = undeclared_kind_features(entity_class);
302 if (dev.subtype == SUBTYPE_MAIN_PARAMETER_ONLY)
303 features = main_parameter_only_variant(features);
304 } else {
305 features = device_features(dev.type, dev.subtype, dev.model);
306 }
307 // `dimmable:` has a schema default of false, so it decides a light's levels outright; the row's
308 // LEVELS only drives the paste-ready snippet and the on/off-only rejection.
309 if (entity_class == DeviceCapabilityClass::LIGHT) {
310 features &= static_cast<DeviceFeatures>(~device_feature::LEVELS);
311 if (dev.dimmable && !has_feature(features, device_feature::MAIN_PARAMETER_ONLY))
312 features |= device_feature::LEVELS;
313 }
314 // The water heater path (its poll, its register reads and writes) belongs to a water heater entity. A
315 // device of that kind bound to another platform (a `switch:` some configs declare only to reach the
316 // register probes) keeps none of it, so its frames stay what they were before that path existed.
317 if (entity_class != DeviceCapabilityClass::WATER_HEATER)
318 features &= static_cast<DeviceFeatures>(~device_feature::WATER_HEATER);
319 dev.features = features;
320 dev.inverted = dev.invert_override.value_or(has_feature(features, device_feature::INVERTED));
321}
322
324 const char *name = device_type_name(type);
325 std::string raw = format_device_type_hex(type);
326 if (name != nullptr && strcmp(name, "unknown") != 0) {
327 return std::string(name) + " (" + raw + ")";
328 }
329 return raw;
330}
331
333 const char *name = yaml_device_type_name(type);
334 if (name != nullptr) {
335 return std::string("\"") + name + "\"";
336 }
337 return format_device_type_hex(type);
338}
339
340std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
341 bool metadata_complete, bool low_power, const std::string &system_key_hex) {
342 // A device that self-reported POWER_SAVE_LOW_POWER needs `low_power: true` so directed commands
343 // wake it with the long preamble — emit it in both snippet shapes, since a low-power device that
344 // also withheld its type is exactly the case a user most needs told.
345 const std::string low_power_line = low_power ? " low_power: true\n" : "";
346 const std::string system_key_line = system_key_hex.empty() ? "" : " system_key: \"" + system_key_hex + "\"\n";
347
348 if (!metadata_complete) {
349 return " <cover|light|switch|lock>:\n"
350 " - platform: home_io_control\n"
351 " name: \"My Device\"\n"
352 " io_device_id: \"" +
353 device_id +
354 "\"\n"
355 " # io_device_type: left unset — this device didn't report its type during\n"
356 " # discovery, so the controller learns it automatically from the next status\n"
357 " # reply. Add it explicitly once you see it logged, to skip re-learning on\n"
358 " # every future boot.\n" +
359 low_power_line + system_key_line;
360 }
361
362 const char *platform = device_platform_name(type, subtype);
363 if (platform == nullptr)
364 return "";
365
366 std::string extra_lines;
367 // The snippet is built from what the device reported, which carries no model.
368 const DeviceFeatures features = device_features(type, subtype, DeviceModel::NONE);
369 if (has_feature(features, device_feature::INVERTED))
370 extra_lines += " invert_position: true\n";
372 has_feature(features, device_feature::LEVELS))
373 extra_lines += " dimmable: true\n";
374 extra_lines += low_power_line;
375 extra_lines += system_key_line;
376
377 const std::string subtype_line = " io_subtype: " + std::to_string(subtype) + "\n";
378
379 return " " + std::string(platform) +
380 ":\n"
381 " - platform: home_io_control\n"
382 " name: \"My Device\"\n"
383 " io_device_id: \"" +
384 device_id + "\"\n" + " io_device_type: " + format_device_type_for_yaml(type) + "\n" + subtype_line +
385 extra_lines;
386}
387
388} // namespace home_io_control
389} // namespace esphome
Capability bits carried by a DeviceTypeInfo row and resolved onto a device record.
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...
DeviceFeatures device_features(DeviceType type, uint8_t subtype, DeviceModel model)
Capability bits of a (type, subtype, model).
ExecutionState execution_state_of(uint8_t status_byte)
Read the execution state out of byte 0 of a status reply.
const char * travel_speed_name(TravelSpeed speed)
Get the name a travel speed is configured and published under.
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.
DeviceFeatures undeclared_kind_features(DeviceCapabilityClass entity_class)
Features of a device kind that has no table row, or that is not declared at all.
const HeatingProfile * find_heating_profile(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the heating profile for a (type, subtype, model).
const char * rain_sensor_state_name(RainSensorState state)
Get a human-readable name for a RainSensorState.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
CoverCommand
Named device commands for cover-type actuators.
@ 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)?
@ NONE
No model: the (type, subtype) row.
void resolve_device_features(IoDevice &dev)
Resolve a device's capability bits and inversion from its kind and its YAML overrides.
const char * execution_state_name(ExecutionState state)
Get a human-readable name for an ExecutionState.
const char * device_type_name(DeviceType type)
Convert a DeviceType to a lowercase string identifier.
const NamedPosition * find_named_position(CoverCommand command)
The named position a command sends.
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
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.
const char * device_operation_profile_name(DeviceType type, uint8_t subtype)
Human‑readable operation profile name for a device kind.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
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 parse_travel_speed(const std::string &name, TravelSpeed &speed)
Look up a travel speed by the name travel_speed_name() gives it.
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.
constexpr DeviceFeatures main_parameter_only_variant(DeviceFeatures type_features)
The features of a subtype-58 variant of a kind.
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)?
std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id, bool metadata_complete, 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 char * device_capability_class_name(DeviceType type)
Get a human‑readable name for a capability class.
ExecutionState
What a device says it is doing, from bits [2:0] of byte 0 of a status reply (CMD_PRIVATE_RESP,...
@ WAITING_FOR_POWER
Accepted, waiting for power (queued behind another device on a shared supply).
@ UNDEFINED
Bits [2:0] hold 6 or 7, which no source defines.
@ DONE
Not executing, no error, ready.
@ NON_EXECUTING
Unable to execute under the given conditions (example: temperature too high).
@ WAITING_FOR_EXECUTING
Accepted, waiting for its turn to run.
@ EXECUTION_ERROR
An error occurred during the previous execution; cleared when the next starts.
bool is_known_device_type(uint16_t raw)
True when a raw 10-bit device-type value is a named DeviceType enumerator.
uint16_t DeviceFeatures
Capability bits of a device kind; see device_feature.
uint8_t decode_packed_device_subtype(uint8_t type_subtype)
Decode a protocol-packed device subtype from the second metadata byte.
constexpr uint8_t error_resp_state(uint8_t result)
Execution state (bits [2:0]) of a CMD_ERROR_RESP 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).
@ WATER_HEATER
Domestic hot water heater (device_feature::WATER_HEATER).
bool device_takes_execute_position(DeviceType type)
Does this device type take position EXECUTEs (a percent, on / off, or stop in the main parameter)?
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.
TravelSpeed
The speed a cover's position moves ask the motor for (ADR 0065).
@ SILENT
Profile 0x05, the manufacturer app's silent operation: slower than the motor's own.
@ MOTOR_DEFAULT
The 6-byte frame with no profile: the motor's own speed, as its wall pad moves it.
@ FAST
Profile 0x06, what the hub sends unless a cover says otherwise.
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.
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.
Runtime state of a paired IO‑Homecontrol device.
DeviceCapabilityClass entity_class
Class of the entity that declared the device; UNKNOWN for a device no entity declared (the type's cla...
DeviceFeatures features
What this device can do, resolved by resolve_device_features().
std::optional< bool > invert_override
YAML invert_position: when declared; wins over the kind's default.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
uint8_t subtype
Device subtype (manufacturer‑specific).
DeviceModel model
YAML-declared product model below the subtype (io_model:); never on the wire.
bool dimmable
YAML-declared dimmable: true of a LIGHT entity.
DeviceType type
Device type (shutter, awning, etc.).
One named position: its feature bit, its main-parameter value and the command that sends it.
const char * name
cover_command_name() of the command ("FAVORITE", "VENT", "PEDESTRIAN", "PARTIAL").