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). Not YAML-selectable: a
39 ///< discovery pseudo-type, not a configurable actuator.
40 ///< Allowlisted in device_type_sync_test.cpp.
41 DUAL_SHUTTER = 0x0D, ///< Dual-section shutter.
42 HEATING_TEMPERATURE_INTERFACE = 0x0E, ///< Heating temperature interface.
43 ON_OFF_SWITCH = 0x0F, ///< Generic on/off switch.
44 HORIZONTAL_AWNING = 0x10, ///< Horizontal awning (open/close inverted).
45 EXTERNAL_VENETIAN_BLIND = 0x11, ///< External venetian blind.
46 LOUVRE_BLIND = 0x12, ///< Louvre blind.
47 CURTAIN_TRACK = 0x13, ///< Curtain track.
48 // VENTILATION_POINT (capability class SWITCH), EXTERIOR_HEATING and HEAT_PUMP (both CLIMATE)
49 // decode and carry capability classes but are deliberately withheld from YAML: no platform
50 // consumes CLIMATE yet, and the climate/ventilation platform is unbuilt. They are allowlisted
51 // in device_type_sync_test.cpp's reverse check; move them into __init__.py's
52 // DEVICE_TYPE_OPTIONS (and yaml_device_type_name()) when that platform lands.
53 VENTILATION_POINT = 0x14, ///< Ventilation point.
54 EXTERIOR_HEATING = 0x15, ///< Exterior heating.
55 HEAT_PUMP = 0x16, ///< Heat pump.
56 INTRUSION_ALARM = 0x17, ///< Intrusion alarm.
57 SWINGING_SHUTTER = 0x18, ///< Swinging shutter.
58};
59
60/// @brief High‑level capability class derived from DeviceType.
61enum class DeviceCapabilityClass : uint8_t {
62 UNKNOWN = 0x00, ///< Unknown capability.
63 COVER = 0x01, ///< Position‑controlled cover (shutter/blind/awning).
64 LIGHT = 0x02, ///< Binary on/off light.
65 SWITCH = 0x03, ///< Binary on/off switch.
66 SENSOR = 0x04, ///< Sensor device.
67 BEACON = 0x05, ///< Beacon.
68 CLIMATE = 0x06, ///< Climate device (heating/cooling).
69 LOCK = 0x07, ///< Lock.
70};
71
72/// @brief Convert a DeviceType to a lowercase string identifier.
73/// @param type Device type enum.
74/// @return Null‑terminated string name (e.g., "roller_shutter").
75const char *device_type_name(DeviceType type);
76
77/// @brief Return the YAML-friendly device-type name for types exposed in the Python schema.
78/// @param type Device type enum.
79/// @return Null‑terminated string (e.g., "external_venetian_blind"), or nullptr if the type
80/// has no symbolic YAML alias (user must use a raw numeric value).
81const char *yaml_device_type_name(DeviceType type);
82
83/// @brief Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
84/// @param type Raw device type.
85/// @return Capability class (COVER, LIGHT, SWITCH, etc.).
87
88/// @brief Get a human‑readable name for a capability class.
89/// @param type Device type (unused, kept for signature compatibility).
90/// @return String like "cover", "light", "switch", "unknown".
92
93/// @brief Does this device type support precise position control (0–100)?
94/// @param type Device type.
95/// @return true for cover‑family devices.
97
98/// @brief Does this device type support binary on/off control?
99/// @param type Device type.
100/// @return true for lights and switches.
102
103/// @brief Does this device type support status request commands (0x03)?
104/// @param type Device type.
105/// @return true for covers, binary devices, and lock devices.
107
108/// @brief Does this device type support binary lock/unlock control via execute commands?
109/// @param type Device type.
110/// @return true for lock devices.
112
113/// @brief Does this device type support 2W climate/heating control (CMD_WRITE_PRIVATE 0x20)?
114///
115/// The single capability gate for the heating send path and the climate entity: true exactly
116/// when device_capability_class(type) is DeviceCapabilityClass::CLIMATE
117/// (HEATING_TEMPERATURE_INTERFACE, EXTERIOR_HEATING, HEAT_PUMP). There is deliberately no
118/// per-device-type or per-vendor branch anywhere downstream.
119/// @param type Device type.
120/// @return true for climate-class devices.
122
123/// @brief Does this device type support tilt (slat angle) control?
124/// @param type Device type.
125/// @return true for venetian blinds, blinds, external venetian blinds, louvre blinds.
127
128/// @brief Does this device type support the ventilation position command?
129///
130/// The ventilation command moves window-type actuators to a predefined
131/// partially-open position suitable for air exchange without fully opening.
132/// @param type Device type.
133/// @return true for WINDOW_OPENER and VENTILATION_POINT.
135
136/// @brief Decode a protocol-packed device type from two metadata bytes.
137/// @param type_msb First metadata byte.
138/// @param type_subtype Second metadata byte containing the remaining type bits and subtype.
139/// @return Decoded device type.
140DeviceType decode_packed_device_type(uint8_t type_msb, uint8_t type_subtype);
141
142/// @brief Decode a protocol-packed device subtype from the second metadata byte.
143/// @param type_subtype Second metadata byte containing subtype in bits [5:0].
144/// @return Manufacturer-specific subtype.
145uint8_t decode_packed_device_subtype(uint8_t type_subtype);
146
147/// @brief Encode a DeviceType/subtype pair into the two-byte packed metadata format used by
148/// discovery responses — the inverse of decode_packed_device_type()/decode_packed_device_subtype().
149/// @param type Device type to encode.
150/// @param subtype Manufacturer-specific subtype; only bits [5:0] are used.
151/// @param type_msb Output: first metadata byte.
152/// @param type_subtype Output: second metadata byte (top 2 type bits + 6-bit subtype).
153void encode_packed_device_type(DeviceType type, uint8_t subtype, uint8_t &type_msb, uint8_t &type_subtype);
154
155/// @brief Human‑readable operation profile name for a device type.
156/// Used for logging and diagnostics.
157/// @param type Device type.
158/// @return String such as "cover_position", "cover_position_tilt", "binary_on_off", "lock", etc.
160
161/// @brief Human-readable device type string for diagnostics, including the raw numeric value.
162/// @param type Device type.
163/// @return String such as "horizontal_awning (0x10)", or the raw hex form ("0x1A") when the
164/// type has no symbolic name.
166
167/// @brief Build the YAML value for a device's `io_device_type` key.
168/// @param type Device type.
169/// @return A quoted symbolic name (e.g. `"horizontal_awning"`) when one exists, otherwise the
170/// raw hex form (e.g. `0x1A`) for a type with no YAML alias.
172
173/// @brief Build the ready-to-paste YAML block describing a device, for both a fully-decoded
174/// device and one whose type/subtype wasn't reported.
175///
176/// With `metadata_complete == false`, the type/subtype are unknown, so the returned snippet
177/// uses a `<cover|light|switch|lock>` platform placeholder and a commented-out
178/// `io_device_type` explanation; `type`, `subtype`, and `inverted` are ignored in this case.
179///
180/// With `metadata_complete == true`, the snippet names the concrete ESPHome platform for
181/// `type` and fills in `io_subtype` and (for an inverted cover) `invert_position: true`. If
182/// `type` has no known ESPHome platform, an empty string is returned instead — the caller
183/// decides what to say when there's no snippet to show.
184///
185/// `low_power: true` is emitted in **both** shapes when `low_power` is set (the device
186/// self-reported POWER_SAVE_LOW_POWER in its discovery Multi Information Byte); it is omitted
187/// entirely otherwise, since its absence is a valid, correct config.
188///
189/// The emitted keys must track the device-bound platform schemas (`platform_schema_extension()`
190/// in platform_common.py, plus each of cover.py/light.py/switch.py/lock.py's own extra keys) by
191/// hand. `make yaml-emitter-sync` (scripts/check-yaml-emitters.py) catches drift between the two
192/// statically.
193/// @param type Decoded device type.
194/// @param subtype Decoded device subtype; only used when `metadata_complete` is true.
195/// @param device_id Hex device ID string (e.g. "38B4A1").
196/// @param metadata_complete Whether the discovery response included type/subtype metadata.
197/// @param inverted Whether the device's open/close positions are swapped; only used when
198/// `metadata_complete` is true and `type` is a cover.
199/// @param low_power Whether the device self-reported a low-power / duty-cycled class; emits
200/// `low_power: true` when set, in both snippet shapes.
201/// @return Multi-line YAML snippet, or an empty string when `metadata_complete` is true but
202/// `type` maps to no ESPHome platform.
203std::string build_device_yaml_snippet(DeviceType type, uint8_t subtype, const std::string &device_id,
204 bool metadata_complete, bool inverted, bool low_power);
205
206// ============================================================================
207// Cover Commands
208// ============================================================================
209
210/// @brief Named device commands for cover-type actuators.
211///
212/// These represent the discrete non-positional actions a controller can send to
213/// a cover device. Each maps to a specific wire encoding in the CMD_EXECUTE payload.
214/// Using this enum avoids conflating numeric positions (0–100) with command codes.
215enum class CoverCommand : uint8_t {
216 STOP = 0, ///< Stop movement immediately.
217 FAVORITE = 1, ///< Move to stored favorite/"My" position.
218 VENT = 2, ///< Move to ventilation position (window-type devices).
219 FORCE_OPEN = 3, ///< Move to fully open at elevated priority; intended to bypass soft locks
220 ///< and environmental limits (confirmed on real hardware to move correctly;
221 ///< bypassing an active lock is still unconfirmed — see create_force_open()
222 ///< in proto_commands.cpp).
223};
224
225/// @brief Get a human-readable name for a CoverCommand.
226/// @param cmd The cover command to name.
227/// @return Null-terminated string such as "STOP", "FAVORITE", or "VENT".
228const char *cover_command_name(CoverCommand cmd);
229
230// ============================================================================
231// Position Report Decoding
232// ============================================================================
233
234/// In status responses, position is encoded as a 16-bit value where
235/// 0x0000 = fully open (0%) and 0xC800 = fully closed (100%).
236static constexpr uint16_t STATUS_POS_MAX = 0xC800;
237/// Target-reached tolerance expressed in raw IO-homecontrol position units.
238/// 100 raw units out of 51200 full-scale is about 0.195%, so this only absorbs
239/// tiny target/current mismatches from device rounding or early stopped flags.
240static constexpr uint16_t STATUS_POS_TOLERANCE_RAW = 100;
241
242/// Packed device metadata uses two bytes where the high 8 bits carry the upper type bits and the
243/// low byte carries both the remaining type bits and the 6-bit manufacturer subtype.
244static constexpr uint8_t DEVICE_METADATA_SIZE = 2;
245static constexpr uint8_t DEVICE_TYPE_LOW_BITS_SHIFT = 2;
246static constexpr uint8_t DEVICE_TYPE_HIGH_BITS_SHIFT = 6;
247static constexpr uint8_t DEVICE_SUBTYPE_MASK = 0x3F;
248
249// ============================================================================
250// Device State
251// ============================================================================
252
253/// @brief Sentinel value meaning "position is not known yet".
254/// Matches POS_UNKNOWN (0xD4 = 212 decimal) for easy debugging.
255static constexpr float UNKNOWN_POSITION = 212.0F;
256static constexpr uint8_t DEVICE_NAME_BUFFER_SIZE = 32; ///< Device name storage including null terminator
257
258/// @brief Sentinel value meaning "no RSSI sample recorded yet" for `last_rssi_dbm`/`rssi_ema_scaled`.
259/// A real RSSI reading from these radios is always well above `INT16_MIN` — and so is any real
260/// `rssi_ema_scaled` fixed-point value.
261static constexpr int16_t RSSI_UNKNOWN_DBM = INT16_MIN;
262
263/// @brief EMA weight denominator and fixed-point scale for `IoDevice::rssi_ema_scaled`.
264/// One constant serves both roles by construction: the update `S += x − round(S/N)` blends each
265/// new sample `x` in at weight 1/N while keeping `S = N × EMA`.
266static constexpr int16_t RSSI_EMA_SCALE = 8;
267
268/// @brief What the hub predicted ahead of confirmation, kept apart from what the device reported.
269///
270/// The observed fields on IoDevice (`position`, `target`, `tilt`, `is_stopped`) mean "this is what
271/// the device last told us" and are never written by a guess. Anything the hub predicts — to give
272/// the Home Assistant UI immediate feedback across the queue-dispatch and exchange gap — lands
273/// here instead, so it can be withdrawn when the command that produced it fails
274/// (DeviceRegistry::rollback_optimistic()) without risking a real observation.
275///
276/// Consumers read through effective_target() / effective_tilt() / effective_is_stopped() rather
277/// than either set of fields directly.
279 /// Predicted movement. NONE defers to the observed `is_stopped`; a bool could not express
280 /// "predict stopped" (apply_optimistic_stop) distinctly from "no prediction".
281 enum class Motion : uint8_t {
282 NONE = 0, ///< No movement prediction; the observed `is_stopped` decides.
283 MOVING, ///< Predicted to be travelling (a position command was issued).
284 STOPPED, ///< Predicted to be at rest (a STOP was issued).
285 };
286
287 float target{UNKNOWN_POSITION}; ///< Predicted main-position target, or UNKNOWN_POSITION.
288 float tilt{UNKNOWN_POSITION}; ///< Predicted slat angle, or UNKNOWN_POSITION.
289 Motion motion{Motion::NONE}; ///< Predicted movement state.
290
291 /// @return true when nothing is predicted, so a rollback would be a no-op.
292 [[nodiscard]] bool empty() const {
294 }
295 /// Withdraw every prediction.
296 void clear() { *this = {}; }
297 /// Withdraw the position prediction; call when a decoded position observation supersedes it.
298 /// Clears `motion` with `target` because apply_optimistic_target() sets the two together.
303 /// Withdraw the tilt prediction; call when a decoded tilt observation supersedes it.
305};
306
307/// @brief Runtime state of a paired IO‑Homecontrol device.
308struct IoDevice {
309 uint8_t node_id[NODE_ID_SIZE]{}; ///< Device's 3‑byte radio address.
310 DeviceType type{DeviceType::UNKNOWN}; ///< Device type (shutter, awning, etc.).
311 uint8_t subtype{0}; ///< Device subtype (manufacturer‑specific).
312 char name[DEVICE_NAME_BUFFER_SIZE]{}; ///< Cached UTF-8 device name decoded from Latin-1 wire payloads.
313 float position{UNKNOWN_POSITION}; ///< Current position: 0=open, 100=closed, or UNKNOWN_POSITION.
314 float tilt{UNKNOWN_POSITION}; ///< Current tilt: 0=closed, 100=open, or UNKNOWN_POSITION.
315 float target{UNKNOWN_POSITION}; ///< Target position the device is moving toward.
316 bool is_stopped{true}; ///< True if device is not moving.
317 bool inverted{false}; ///< True if open/close positions are swapped (e.g., horizontal awning).
318 bool silent{false}; ///< True to send position moves with the reference hub's "silent
319 ///< operation" extended block, which makes the motor travel more
320 ///< slowly. Like `inverted`/`dimmable` this is a YAML-declared
321 ///< preference, not a protocol-reported fact — nothing on the wire
322 ///< tells us whether a device is in that mode.
323 bool optimistic_state{true}; ///< True if `target` may be set ahead of a confirming poll/response.
324 bool low_power{false}; ///< YAML-declared: this target is a low-power / duty-cycled receiver, so
325 ///< directed frames set CTRL1_LOW_POWER and use the long wake-up preamble.
326 ///< A protocol-reported class exists (discovery Multi Information Byte) but
327 ///< is surfaced through the pairing/scan snippet, never applied at runtime.
328 bool dimmable{false}; ///< True for a LIGHT-class device configured `dimmable: true` in YAML.
329 ///< Not a protocol-level fact (the wire gives no dimmable-capability
330 ///< signal) — set from platform_light.cpp's YAML config via
331 ///< DeviceRegistry::set_dimmable(), purely for accurate profile-name
332 ///< logging.
333 uint8_t last_result_code{0}; ///< Last CMD_ERROR_RESP result byte (0 = none recorded). See
334 ///< command_result_name()/is_limitation_result() in proto_constants.h. Cleared by
335 ///< the next successful status/command reply for this device. Note: 0 is also the
336 ///< real RESULT_UNKNOWN_STATUS_REPLY wire value, so that specific explicit reply is
337 ///< indistinguishable here from "nothing recorded yet" — a known, accepted tradeoff.
338 uint32_t last_result_at_ms{0}; ///< millis() timestamp of last_result_code, 0 when none recorded.
339 uint8_t last_commander[NODE_ID_SIZE]{}; ///< Node ID of the controller that last commanded this device, as
340 ///< reported verbatim by the device in its own status payload. All
341 ///< zeroes until a status reply carrying the record has been decoded
342 ///< (see detail::decode_last_command_record() in hub_internal.h).
343 uint8_t last_command_originator{0}; ///< That command's Command Originator byte (ORIGINATOR_* in
344 ///< proto_constants.h). Only meaningful when `has_last_command`.
345 bool has_last_command{false}; ///< True once a status reply carried a well-formed last-command record.
346 uint32_t last_status{0}; ///< millis() timestamp of last received status.
347 int16_t last_rssi_dbm{RSSI_UNKNOWN_DBM}; ///< Most recent raw RSSI sample (dBm), or RSSI_UNKNOWN_DBM.
348 int16_t rssi_ema_scaled{RSSI_UNKNOWN_DBM}; ///< Smoothed RSSI as fixed point in 1/RSSI_EMA_SCALE dBm — read
349 ///< through device_rssi_ema_dbm(), updated by
350 ///< detail::update_link_health() in hub_internal.h. Fixed point
351 ///< (rather than whole dBm) keeps sub-dBm EMA contributions from
352 ///< vanishing to integer truncation, so the average converges on a
353 ///< stable signal instead of stalling up to RSSI_EMA_SCALE−1 dBm
354 ///< away. RSSI_UNKNOWN_DBM before the first sample.
355 uint32_t last_seen_ms{0}; ///< millis() of the last frame received from this device (any command), 0 = never.
356 uint16_t exchange_timeout_count{0}; ///< Cumulative count of outbound exchanges to this device with no valid
357 ///< response (see detail::record_exchange_timeout() in hub_internal.h).
358 uint16_t exchange_attempt_count{0}; ///< Cumulative attempts (`ExchangeEngine::DebugInfo::tries`, 1-based per
359 ///< exchange) across those timed-out exchanges only — attempts within an
360 ///< ultimately successful exchange are not counted (deliberate scope limit).
361 OptimisticState optimistic{}; ///< Hub-side predictions; see OptimisticState. Never observation.
362};
363
364/// @brief Convert an `rssi_ema_scaled` fixed-point value to whole dBm (round half away from zero).
365/// @param scaled Fixed-point EMA value in 1/RSSI_EMA_SCALE dBm units (not the sentinel).
366/// @return Rounded dBm value.
367inline int16_t rssi_scaled_to_dbm(int16_t scaled) {
368 constexpr int16_t half = RSSI_EMA_SCALE / 2;
369 return static_cast<int16_t>((scaled + (scaled >= 0 ? half : -half)) / RSSI_EMA_SCALE);
370}
371
372/// @brief A device's smoothed RSSI in whole dBm.
373/// @param dev Device record to read.
374/// @return Rounded EMA in dBm, or RSSI_UNKNOWN_DBM when no sample has been recorded yet.
378
379/// @brief The main-position target a consumer should act on: the prediction when one stands,
380/// otherwise the device's own last reported target.
381/// @param dev Device record to read.
382/// @return The predicted target when set, otherwise `dev.target`.
383inline float effective_target(const IoDevice &dev) {
384 return dev.optimistic.target != UNKNOWN_POSITION ? dev.optimistic.target : dev.target;
385}
386
387/// @brief The slat angle a consumer should act on, prediction first.
388/// @param dev Device record to read.
389/// @return The predicted tilt when set, otherwise `dev.tilt`.
390inline float effective_tilt(const IoDevice &dev) {
391 return dev.optimistic.tilt != UNKNOWN_POSITION ? dev.optimistic.tilt : dev.tilt;
392}
393
394/// @brief Whether a consumer should treat the device as at rest, prediction first.
395/// @param dev Device record to read.
396/// @return The predicted movement state when one stands, otherwise `dev.is_stopped`.
397inline bool effective_is_stopped(const IoDevice &dev) {
398 switch (dev.optimistic.motion) {
400 return false;
402 return true;
404 default:
405 return dev.is_stopped;
406 }
407}
408
409/// @brief Determine whether a device type has inverted position mapping by default.
410/// @param type Device type.
411/// @return true for horizontal awnings; false otherwise.
413
414/// @brief Decode target/current position values from a status frame.
415/// @param target_raw 16‑bit raw target value.
416/// @param current_raw 16‑bit raw current value.
417/// @param is_stopped True if device reports stopped.
418/// @param target Output target position (0–100 or UNKNOWN_POSITION).
419/// @param position Output current position (0–100 or UNKNOWN_POSITION).
420void decode_position_report(uint16_t target_raw, uint16_t current_raw, bool is_stopped, float &target, float &position);
421/// @brief Has the device reached its target within tolerance?
422/// @param target Target position (0–100 or UNKNOWN_POSITION).
423/// @param position Current position (0–100 or UNKNOWN_POSITION).
424/// @return true if positions match within STATUS_POS_TOLERANCE_RAW.
425bool has_reached_target_position(float target, float position);
426/// @brief Decode tilt angle from raw 16‑bit value.
427/// @param tilt_raw Raw tilt value from status frame.
428/// @return Tilt percentage (0 = closed, 100 = open) or UNKNOWN_POSITION.
429float decode_tilt_report(uint16_t tilt_raw);
430
431} // namespace home_io_control
432} // 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.
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.
float effective_target(const IoDevice &dev)
The main-position target a consumer should act on: the prediction when one stands,...
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)?
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)?
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.
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...
bool effective_is_stopped(const IoDevice &dev)
Whether a consumer should treat the device as at rest, prediction first.
float effective_tilt(const IoDevice &dev)
The slat angle a consumer should act on, prediction first.
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.
uint8_t last_commander[NODE_ID_SIZE]
Node ID of the controller that last commanded this device, as reported verbatim by the device in its ...
uint8_t last_command_originator
That command's Command Originator byte (ORIGINATOR_* in proto_constants.h).
char name[DEVICE_NAME_BUFFER_SIZE]
Cached UTF-8 device name decoded from Latin-1 wire payloads.
OptimisticState optimistic
Hub-side predictions; see OptimisticState. Never observation.
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.
bool silent
True to send position moves with the reference hub's "silent operation" extended block,...
bool low_power
YAML-declared: this target is a low-power / duty-cycled receiver, so directed frames set CTRL1_LOW_PO...
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 has_last_command
True once a status reply carried a well-formed last-command record.
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.
What the hub predicted ahead of confirmation, kept apart from what the device reported.
@ STOPPED
Predicted to be at rest (a STOP was issued).
@ MOVING
Predicted to be travelling (a position command was issued).
@ NONE
No movement prediction; the observed is_stopped decides.
void clear()
Withdraw every prediction.
Motion motion
Predicted movement state.
void clear_tilt()
Withdraw the tilt prediction; call when a decoded tilt observation supersedes it.
void clear_position()
Withdraw the position prediction; call when a decoded position observation supersedes it.
float tilt
Predicted slat angle, or UNKNOWN_POSITION.
float target
Predicted main-position target, or UNKNOWN_POSITION.