Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_device_model.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_device_model.h
4/// @brief IO-Homecontrol device-type model, capabilities and runtime device state.
5/// @ingroup hioc_protocol
6///
7/// DeviceType/DeviceCapabilityClass, the capability predicates, packed-metadata
8/// decoders, cover commands, position/tilt report decoding and the IoDevice
9/// runtime struct.
10
11#include "proto_sizes.h"
12
13#include <cstdint>
14#include <string>
15
16namespace esphome {
17namespace home_io_control {
18
19// ============================================================================
20// Device Types — from the IO-Homecontrol specification
21// ============================================================================
22
23/// @brief Device type identifiers reported by IO‑Homecontrol products.
24/// The numeric values follow the official specification. Do not reassign or reorder these.
25enum class DeviceType : uint8_t {
26 UNKNOWN = 0x00, ///< Unknown/unspecified device.
27 VENETIAN_BLIND = 0x01, ///< Venetian blind.
28 ROLLER_SHUTTER = 0x02, ///< Roller shutter.
29 AWNING = 0x03, ///< Awning.
30 WINDOW_OPENER = 0x04, ///< Window opening actuator.
31 GARAGE_OPENER = 0x05, ///< Garage door opener.
32 LIGHT = 0x06, ///< Binary light.
33 GATE_OPENER = 0x07, ///< Gate opener.
34 ROLLING_DOOR_OPENER = 0x08, ///< Rolling door opener.
35 LOCK = 0x09, ///< Lock.
36 BLIND = 0x0A, ///< Generic blind.
37 SCREEN = 0x0B, ///< Insect/privacy screen.
38 BEACON = 0x0C, ///< Beacon (unpaired/announcement).
39 DUAL_SHUTTER = 0x0D, ///< Dual-section shutter.
40 HEATING_TEMPERATURE_INTERFACE = 0x0E, ///< Heating temperature interface.
41 ON_OFF_SWITCH = 0x0F, ///< Generic on/off switch.
42 HORIZONTAL_AWNING = 0x10, ///< Horizontal awning (open/close inverted).
43 EXTERNAL_VENETIAN_BLIND = 0x11, ///< External venetian blind.
44 LOUVRE_BLIND = 0x12, ///< Louvre blind.
45 CURTAIN_TRACK = 0x13, ///< Curtain track.
46 VENTILATION_POINT = 0x14, ///< Ventilation point.
47 EXTERIOR_HEATING = 0x15, ///< Exterior heating.
48 HEAT_PUMP = 0x16, ///< Heat pump.
49 INTRUSION_ALARM = 0x17, ///< Intrusion alarm.
50 SWINGING_SHUTTER = 0x18, ///< Swinging shutter.
51};
52
53/// @brief High‑level capability class derived from DeviceType.
54enum class DeviceCapabilityClass : uint8_t {
55 UNKNOWN = 0x00, ///< Unknown capability.
56 COVER = 0x01, ///< Position‑controlled cover (shutter/blind/awning).
57 LIGHT = 0x02, ///< Binary on/off light.
58 SWITCH = 0x03, ///< Binary on/off switch.
59 SENSOR = 0x04, ///< Sensor device.
60 BEACON = 0x05, ///< Beacon.
61 CLIMATE = 0x06, ///< Climate device (heating/cooling).
62 LOCK = 0x07, ///< Lock.
63};
64
65/// @brief Convert a DeviceType to a lowercase string identifier.
66/// @param type Device type enum.
67/// @return Null‑terminated string name (e.g., "roller_shutter").
68const char *device_type_name(DeviceType type);
69
70/// @brief Return the YAML-friendly device-type name for types exposed in the Python schema.
71/// @param type Device type enum.
72/// @return Null‑terminated string (e.g., "external_venetian_blind"), or nullptr if the type
73/// has no symbolic YAML alias (user must use a raw numeric value).
74const char *yaml_device_type_name(DeviceType type);
75
76/// @brief Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
77/// @param type Raw device type.
78/// @return Capability class (COVER, LIGHT, SWITCH, etc.).
80
81/// @brief Get a human‑readable name for a capability class.
82/// @param type Device type (unused, kept for signature compatibility).
83/// @return String like "cover", "light", "switch", "unknown".
85
86/// @brief Does this device type support precise position control (0–100)?
87/// @param type Device type.
88/// @return true for cover‑family devices.
90
91/// @brief Does this device type support binary on/off control?
92/// @param type Device type.
93/// @return true for lights and switches.
95
96/// @brief Does this device type support status request commands (0x03)?
97/// @param type Device type.
98/// @return true for covers, binary devices, and lock devices.
100
101/// @brief Does this device type support binary lock/unlock control via execute commands?
102/// @param type Device type.
103/// @return true for lock devices.
105
106/// @brief Does this device type support tilt (slat angle) control?
107/// @param type Device type.
108/// @return true for venetian blinds, blinds, external venetian blinds, louvre blinds.
110
111/// @brief Does this device type support the ventilation position command?
112///
113/// The ventilation command moves window-type actuators to a predefined
114/// partially-open position suitable for air exchange without fully opening.
115/// @param type Device type.
116/// @return true for WINDOW_OPENER and VENTILATION_POINT.
118
119/// @brief Decode a protocol-packed device type from two metadata bytes.
120/// @param type_msb First metadata byte.
121/// @param type_subtype Second metadata byte containing the remaining type bits and subtype.
122/// @return Decoded device type.
123DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype);
124
125/// @brief Decode a protocol-packed device subtype from the second metadata byte.
126/// @param type_subtype Second metadata byte containing subtype in bits [5:0].
127/// @return Manufacturer-specific subtype.
128uint8_t decode_packed_device_subtype(uint8_t type_subtype);
129
130/// @brief Encode a DeviceType/subtype pair into the two-byte packed metadata format used by
131/// discovery responses — the inverse of decode_packed_device_type()/decode_packed_device_subtype().
132/// @param type Device type to encode.
133/// @param subtype Manufacturer-specific subtype; only bits [5:0] are used.
134/// @param type_msb Output: first metadata byte.
135/// @param type_subtype Output: second metadata byte (top 2 type bits + 6-bit subtype).
136void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype);
137
138/// @brief Human‑readable operation profile name for a device type.
139/// Used for logging and diagnostics.
140/// @param type Device type.
141/// @return String such as "cover_position", "cover_position_tilt", "binary_on_off", "lock", etc.
143
144/// @brief Human-readable device type string for diagnostics, including the raw numeric value.
145/// @param type Device type.
146/// @return String such as "horizontal_awning (0x10)", or the raw hex form ("0x1A") when the
147/// type has no symbolic name.
149
150/// @brief Build the YAML value for a device's `io_device_type` key.
151/// @param type Device type.
152/// @return A quoted symbolic name (e.g. `"horizontal_awning"`) when one exists, otherwise the
153/// raw hex form (e.g. `0x1A`) for a type with no YAML alias.
155
156/// @brief Build the ready-to-paste YAML block describing a device, for both a fully-decoded
157/// device and one whose type/subtype wasn't reported.
158///
159/// With `metadata_complete == false`, the type/subtype are unknown, so the returned snippet
160/// uses a `<cover|light|switch|lock>` platform placeholder and a commented-out
161/// `io_device_type` explanation; `type`, `subtype`, and `inverted` are ignored in this case.
162///
163/// With `metadata_complete == true`, the snippet names the concrete ESPHome platform for
164/// `type` and fills in `io_subtype` and (for an inverted cover) `invert_position: true`. If
165/// `type` has no known ESPHome platform, an empty string is returned instead — the caller
166/// decides what to say when there's no snippet to show.
167/// @param type Decoded device type.
168/// @param subtype Decoded device subtype; only used when `metadata_complete` is true.
169/// @param device_id Hex device ID string (e.g. "38B4A1").
170/// @param metadata_complete Whether the discovery response included type/subtype metadata.
171/// @param inverted Whether the device's open/close positions are swapped; only used when
172/// `metadata_complete` is true and `type` is a cover.
173/// @return Multi-line YAML snippet, or an empty string when `metadata_complete` is true but
174/// `type` maps to no ESPHome platform.
175std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
176 bool metadata_complete, bool inverted);
177
178// ============================================================================
179// Cover Commands
180// ============================================================================
181
182/// @brief Named device commands for cover-type actuators.
183///
184/// These represent the discrete non-positional actions a controller can send to
185/// a cover device. Each maps to a specific wire encoding in the CMD_EXECUTE payload.
186/// Using this enum avoids conflating numeric positions (0–100) with command codes.
187enum class CoverCommand : uint8_t {
188 STOP = 0, ///< Stop movement immediately.
189 FAVORITE = 1, ///< Move to stored favorite/"My" position.
190 VENT = 2, ///< Move to ventilation position (window-type devices).
191 FORCE_OPEN = 3, ///< Move to fully open at elevated priority; intended to bypass soft locks
192 ///< and environmental limits (confirmed on real hardware to move correctly;
193 ///< bypassing an active lock is still unconfirmed — see create_force_open()
194 ///< in proto_commands.cpp).
195};
196
197/// @brief Get a human-readable name for a CoverCommand.
198/// @param cmd The cover command to name.
199/// @return Null-terminated string such as "STOP", "FAVORITE", or "VENT".
200const char *cover_command_name(CoverCommand cmd);
201
202// ============================================================================
203// Position Report Decoding
204// ============================================================================
205
206/// In status responses, position is encoded as a 16-bit value where
207/// 0x0000 = fully open (0%) and 0xC800 = fully closed (100%).
208static constexpr uint16_t STATUS_POS_MAX = 0xC800;
209/// Target-reached tolerance expressed in raw IO-homecontrol position units.
210/// 100 raw units out of 51200 full-scale is about 0.195%, so this only absorbs
211/// tiny target/current mismatches from device rounding or early stopped flags.
212static constexpr uint16_t STATUS_POS_TOLERANCE_RAW = 100;
213
214/// Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the
215/// low byte carries both the remaining type bits and the 6-bit manufacturer subtype.
216static constexpr uint8_t DEVICE_METADATA_SIZE = 2;
217static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT = 2;
218static constexpr uint8_t DEVICE_TYPE_HIGH_BITS_SHIFT = 6;
219static constexpr uint8_t DEVICE_SUBTYPE_MASK = 0x3F;
220
221// ============================================================================
222// Device State
223// ============================================================================
224
225/// @brief Sentinel value meaning "position is not known yet".
226/// Matches POS_UNKNOWN (0xD4 = 212 decimal) for easy debugging.
227static constexpr float UNKNOWN_POSITION = 212.0F;
228static constexpr uint8_t DEVICE_NAME_BUFFER_SIZE = 32; ///< Device name storage including null terminator
229
230/// @brief Sentinel value meaning "no RSSI sample recorded yet" for `last_rssi_dbm`/`rssi_ema_scaled`.
231/// A real RSSI reading from these radios is always well above `INT16_MIN` — and so is any real
232/// `rssi_ema_scaled` fixed-point value.
233static constexpr int16_t RSSI_UNKNOWN_DBM = INT16_MIN;
234
235/// @brief EMA weight denominator and fixed-point scale for `IoDevice::rssi_ema_scaled`.
236/// One constant serves both roles by construction: the update `S += x − round(S/N)` blends each
237/// new sample `x` in at weight 1/N while keeping `S = N × EMA`.
238static constexpr int16_t RSSI_EMA_SCALE = 8;
239
240/// @brief Runtime state of a paired IO‑Homecontrol device.
241struct IoDevice {
242 uint8_t node_id[NODE_ID_SIZE]{}; ///< Device's 3‑byte radio address.
243 DeviceType type{DeviceType::UNKNOWN}; ///< Device type (shutter, awning, etc.).
244 uint8_t subtype{0}; ///< Device subtype (manufacturer‑specific).
245 char name[DEVICE_NAME_BUFFER_SIZE]{}; ///< Cached UTF-8 device name decoded from Latin-1 wire payloads.
246 float position{UNKNOWN_POSITION}; ///< Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
247 float tilt{UNKNOWN_POSITION}; ///< Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
248 float target{UNKNOWN_POSITION}; ///< Target position the device is moving toward.
249 bool is_stopped{true}; ///< True if device is not moving.
250 bool inverted{false}; ///< True if open/close positions are swapped (e.g., horizontal awning).
251 bool optimistic_state{true}; ///< True if `target` may be set ahead of a confirming poll/response.
252 bool dimmable{false}; ///< True for a LIGHT-class device configured `dimmable: true` in YAML.
253 ///< Not a protocol-level fact (the wire gives no dimmable-capability
254 ///< signal) — set from platform_light.cpp's YAML config via
255 ///< DeviceRegistry::set_dimmable(), purely for accurate profile-name
256 ///< logging.
257 uint8_t last_result_code{0}; ///< Last CMD_ERROR_RESP result byte (0 = none recorded). See
258 ///< command_result_name()/is_limitation_result() in proto_constants.h. Cleared by
259 ///< the next successful status/command reply for this device. Note: 0 is also the
260 ///< real RESULT_UNKNOWN_STATUS_REPLY wire value, so that specific explicit reply is
261 ///< indistinguishable here from "nothing recorded yet" — a known, accepted tradeoff.
262 uint32_t last_result_at_ms{0}; ///< millis() timestamp of last_result_code, 0 when none recorded.
263 uint32_t last_status{0}; ///< millis() timestamp of last received status.
264 int16_t last_rssi_dbm{RSSI_UNKNOWN_DBM}; ///< Most recent raw RSSI sample (dBm), or RSSI_UNKNOWN_DBM.
265 int16_t rssi_ema_scaled{RSSI_UNKNOWN_DBM}; ///< Smoothed RSSI as fixed point in 1/RSSI_EMA_SCALE dBm — read
266 ///< through device_rssi_ema_dbm(), updated by
267 ///< detail::update_link_health() in hub_internal.h. Fixed point
268 ///< (rather than whole dBm) keeps sub-dBm EMA contributions from
269 ///< vanishing to integer truncation, so the average converges on a
270 ///< stable signal instead of stalling up to RSSI_EMA_SCALE−1 dBm
271 ///< away. RSSI_UNKNOWN_DBM before the first sample.
272 uint32_t last_seen_ms{0}; ///< millis() of the last frame received from this device (any command), 0 = never.
273 uint16_t exchange_timeout_count{0}; ///< Cumulative count of outbound exchanges to this device with no valid
274 ///< response (see detail::record_exchange_timeout() in hub_internal.h).
275 uint16_t exchange_attempt_count{0}; ///< Cumulative attempts (`ExchangeEngine::DebugInfo::tries`, 1-based per
276 ///< exchange) across those timed-out exchanges only — attempts within an
277 ///< ultimately successful exchange are not counted (deliberate scope limit).
278};
279
280/// @brief Convert an `rssi_ema_scaled` fixed-point value to whole dBm (round half away from zero).
281/// @param scaled Fixed-point EMA value in 1/RSSI_EMA_SCALE dBm units (not the sentinel).
282/// @return Rounded dBm value.
283inline int16_t rssi_scaled_to_dbm(int16_t scaled) {
284 constexpr int16_t half = RSSI_EMA_SCALE / 2;
285 return static_cast<int16_t>((scaled + (scaled >= 0 ? half : -half)) / RSSI_EMA_SCALE);
286}
287
288/// @brief A device's smoothed RSSI in whole dBm.
289/// @param dev Device record to read.
290/// @return Rounded EMA in dBm, or RSSI_UNKNOWN_DBM when no sample has been recorded yet.
294
295/// @brief Determine whether a device type has inverted position mapping by default.
296/// @param type Device type.
297/// @return true for horizontal awnings; false otherwise.
299
300/// @brief Decode target/current position values from a status frame.
301/// @param target_raw 16‑bit raw target value.
302/// @param current_raw 16‑bit raw current value.
303/// @param is_stopped True if device reports stopped.
304/// @param target Output target position (0–100 or UNKNOWN_POSITION).
305/// @param position Output current position (0–100 or UNKNOWN_POSITION).
306void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position);
307/// @brief Has the device reached its target within tolerance?
308/// @param target Target position (0–100 or UNKNOWN_POSITION).
309/// @param position Current position (0–100 or UNKNOWN_POSITION).
310/// @return true if positions match within STATUS_POS_TOLERANCE_RAW.
311bool has_reached_target_position(float target, float position);
312/// @brief Decode tilt angle from raw 16‑bit value.
313/// @param tilt_raw Raw tilt value from status frame.
314/// @return Tilt percentage (0 = closed, 100 = open) or UNKNOWN_POSITION.
315float decode_tilt_report(uint16_t tilt_raw);
316
317} // namespace home_io_control
318} // namespace esphome
static constexpr uint8_t DEVICE_NAME_BUFFER_SIZE
Device name storage including null terminator.
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 uint8_t DEVICE_METADATA_SIZE
Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the low byt...
static constexpr float UNKNOWN_POSITION
Sentinel value meaning "position is not known yet".
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
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?
int16_t rssi_scaled_to_dbm(int16_t scaled)
Convert an rssi_ema_scaled fixed-point value to whole dBm (round half away from zero).
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.
static constexpr int16_t RSSI_EMA_SCALE
EMA weight denominator and fixed-point scale for IoDevice::rssi_ema_scaled.
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).
int16_t device_rssi_ema_dbm(const IoDevice &dev)
A device's smoothed RSSI in whole dBm.
static constexpr int16_t RSSI_UNKNOWN_DBM
Sentinel value meaning "no RSSI sample recorded yet" for last_rssi_dbm/rssi_ema_scaled.
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?
Fundamental IO-Homecontrol frame and crypto size constants.
Runtime state of a paired IO‑Homecontrol device.
uint32_t last_result_at_ms
millis() timestamp of last_result_code, 0 when none recorded.
float target
Target position the device is moving toward.
uint16_t exchange_attempt_count
Cumulative attempts (ExchangeEngine::DebugInfo::tries, 1-based per exchange) across those timed-out e...
float tilt
Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
uint8_t last_result_code
Last CMD_ERROR_RESP result byte (0 = none recorded).
uint32_t last_status
millis() timestamp of last received status.
char name[DEVICE_NAME_BUFFER_SIZE]
Cached UTF-8 device name decoded from Latin-1 wire payloads.
bool inverted
True if open/close positions are swapped (e.g., horizontal awning).
int16_t last_rssi_dbm
Most recent raw RSSI sample (dBm), or RSSI_UNKNOWN_DBM.
float position
Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
uint8_t subtype
Device subtype (manufacturer‑specific).
uint32_t last_seen_ms
millis() of the last frame received from this device (any command), 0 = never.
int16_t rssi_ema_scaled
Smoothed RSSI as fixed point in 1/RSSI_EMA_SCALE dBm — read through device_rssi_ema_dbm(),...
uint8_t node_id[NODE_ID_SIZE]
Device's 3‑byte radio address.
bool dimmable
True for a LIGHT-class device configured dimmable: true in YAML.
bool optimistic_state
True if target may be set ahead of a confirming poll/response.
DeviceType type
Device type (shutter, awning, etc.).
uint16_t exchange_timeout_count
Cumulative count of outbound exchanges to this device with no valid response (see detail::record_exch...
bool is_stopped
True if device is not moving.