Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
proto_device_types.h
Go to the documentation of this file.
1#pragma once
2
3/// @file proto_device_types.h
4/// @brief The IO node-type and subtype table: names, entity class, platform and behaviour flags.
5/// @ingroup hioc_protocol
6///
7/// One row per (node type, subtype) the project knows by name. Every per-type fact that is not a
8/// bare number — the lowercase identifier, the YAML selectability, the capability class, whether a
9/// platform can drive the device yet, and the capability bits — is read from that table, so there is
10/// exactly one place to add a type or a subtype. A lookup falls back from (type, subtype) to
11/// (type, 0) to "not in the table".
12///
13/// Subtype names agree across manufacturers, so rows are keyed on (type, subtype). A row names a wire
14/// type, not a product: products that share a (type, subtype) are told apart by data the wire does not
15/// carry, which is why the names stay generic. Where two such products need different handling, the
16/// (type, subtype) row has model rows below it (DeviceModel, YAML `io_model:`): a model row states its
17/// own whole capability word, exactly like a subtype row, and keeps the class and platform of the row
18/// above it. A model is declared, never learned, so a lookup takes it as an explicit argument and
19/// DeviceModel::NONE is the (type, subtype) row itself (ADR 0067).
20
21#include "proto_constants.h"
23#include "proto_device_model.h"
24
25#include <cstdint>
26#include <string>
27
28namespace esphome {
29namespace home_io_control {
30
31/// @brief Subtype value that marks a device taking the main parameter only, on any node type.
32/// The subtype is defined that way in the public VELUX KLF 200 API specification (Appendix 2).
33static constexpr uint8_t SUBTYPE_MAIN_PARAMETER_ONLY = 58;
34
35/// @brief Subtype of the electrical heater (node type 52) with an adjustable setpoint, the one driven
36/// over the private register protocol (proto_heating.h).
37static constexpr uint8_t SUBTYPE_HEATER_WITH_SETPOINT = 1;
38
39/// @brief Bit flags carried by a DeviceTypeInfo row: identity facts. Capabilities are DeviceFeatures.
41/// Type rows only: the type's identifier is accepted by the YAML `io_device_type` key
42/// (hub_validators.py `DEVICE_TYPE_OPTIONS` lists exactly these).
43static constexpr uint8_t YAML_SELECTABLE = 1U << 0U;
44/// An ESPHome platform of the row's capability class can drive this (type, subtype). Rows without
45/// it are reported as "not supported yet" and get no paste-ready YAML.
46static constexpr uint8_t HAS_PLATFORM = 1U << 1U;
47/// Subtype rows only: `name` already is the complete description ("window lock") and is printed
48/// alone, instead of being appended to the type's name ("roller shutter, with projection").
49static constexpr uint8_t STANDALONE_NAME = 1U << 2U;
50/// Subtype rows only: no capture or report has confirmed this kind on a real device, so what the
51/// row says is inferred. Discovery and pairing log it as experimental, the codegen warns when a
52/// config declares it (hub_validators.py `KIND_EXPERIMENTAL` is Python's copy of this flag), and
53/// the node-type table in the published docs marks it. A type row (subtype 0) is never experimental.
54static constexpr uint8_t EXPERIMENTAL = 1U << 3U;
55} // namespace device_type_flags
56
57/// @brief One row of the node-type / subtype table.
59 DeviceType type; ///< Node type.
60 uint8_t subtype; ///< Subtype; 0 is the type's own row.
61 DeviceModel model; ///< NONE for a type or subtype row; the model of a model row.
62 const char *name; ///< Type row: lowercase identifier ("roller_shutter"). Subtype row:
63 ///< a description that follows the type name ("with projection").
64 ///< Model row: its YAML `io_model:` spelling ("towel_dryer").
65 DeviceCapabilityClass capability; ///< Entity family the device maps to.
66 uint8_t flags; ///< device_type_flags bits.
67 DeviceFeatures features; ///< device_feature bits: the whole word, never merged with the type row's.
68};
69
70/// @brief Look up the table row for a (type, subtype, model).
71/// @param type Node type.
72/// @param subtype Subtype; a subtype with no row of its own falls back to the type's row (subtype 0).
73/// @param model Declared model; a model the (type, subtype) has no row for falls back to the
74/// (type, subtype) row. DeviceModel::NONE for a device declared without one.
75/// @return The matching row, or nullptr when the type is not in the table.
76const DeviceTypeInfo *find_device_type_info(DeviceType type, uint8_t subtype, DeviceModel model);
77
78/// @brief Look up the table row with exactly this (type, subtype), without falling back.
79/// @param type Node type.
80/// @param subtype Subtype.
81/// @return The (type, subtype) row itself (never a model row), or nullptr when there is none.
82const DeviceTypeInfo *find_exact_device_type_info(DeviceType type, uint8_t subtype);
83
84/// @brief Look up the model row with exactly this (type, subtype, model), without falling back.
85/// @param type Node type.
86/// @param subtype Subtype.
87/// @param model Model; DeviceModel::NONE has no model row.
88/// @return The model row, or nullptr when the (type, subtype) does not offer this model.
89const DeviceTypeInfo *find_device_model_info(DeviceType type, uint8_t subtype, DeviceModel model);
90
91/// @brief Does this exact (type, subtype) have model rows below it?
92/// @details True where products that share the wire type need telling apart; the hub then checks a
93/// device's declared model against what the device answers (a WARN-only hint).
94/// @param type Node type.
95/// @param subtype Subtype.
96/// @return true when at least one model row names this (type, subtype).
97bool device_offers_models(DeviceType type, uint8_t subtype);
98
99/// @brief Can an ESPHome platform drive this (type, subtype) today?
100/// @param type Node type.
101/// @param subtype Subtype.
102/// @return true when the resolved row carries device_type_flags::HAS_PLATFORM.
103bool device_has_platform(DeviceType type, uint8_t subtype);
104
105/// @brief Name of the ESPHome platform that drives this (type, subtype).
106/// @param type Node type.
107/// @param subtype Subtype.
108/// @return "cover", "light", "switch", "lock", "climate" or "fan", or nullptr when no platform drives it yet.
109const char *device_platform_name(DeviceType type, uint8_t subtype);
110
111/// @brief Is this (type, subtype) a kind no capture or report has confirmed on a real device?
112/// @param type Node type.
113/// @param subtype Subtype.
114/// @return true when the exact row carries device_type_flags::EXPERIMENTAL; a subtype with no row of
115/// its own is not experimental, because it behaves like its (confirmed) type.
116bool device_is_experimental(DeviceType type, uint8_t subtype);
117
118/// @brief Capability class of a (type, subtype), subtype-aware.
119/// @details device_capability_class(DeviceType) answers for the type alone; this overload lets a
120/// subtype row differ from its type (for example a heater with an adjustable setpoint).
121/// @param type Node type.
122/// @param subtype Subtype.
123/// @return The resolved row's class, or DeviceCapabilityClass::UNKNOWN when the type is not in the table.
125
126/// @brief Capability bits of a (type, subtype, model).
127/// @details The model row; else the exact (type, subtype) row; else, for subtype
128/// SUBTYPE_MAIN_PARAMETER_ONLY, main_parameter_only_variant() of the type's row (of no features
129/// when the type has no row); else the type's own row; else no features. Callers resolve a
130/// device's bits once, onto the device record, and read them from there; this lookup serves
131/// the resolver and checks that run before a device exists.
132/// @param type Node type.
133/// @param subtype Subtype.
134/// @param model Declared model (IoDevice::model); DeviceModel::NONE where no model is declared, such
135/// as discovery and the paste-ready YAML, which only know the wire type.
136/// @return The feature word.
137DeviceFeatures device_features(DeviceType type, uint8_t subtype, DeviceModel model);
138
139/// @brief Features of a device kind that has no table row, or that is not declared at all.
140/// @details An undeclared device keeps the permissive behaviour its entity gives it until the type is
141/// learned and a row takes over: a cover accepts a slider, favourite and vent and sends the
142/// 8-byte position frame; a light, switch or lock sends the 8-byte frame; an exterior-heating
143/// fan takes levels in that frame. No class has tilt.
144/// @param entity_class Capability class of the entity that registered the device.
145/// @return The feature word, or no features for a class with no EXECUTE position path.
147
148/// @brief Human-readable description of a (type, subtype) for logs and reports.
149/// @param type Node type.
150/// @param subtype Subtype.
151/// @return "<type name>, <subtype name>" (for example "electrical heater, with adjustable setpoint"),
152/// "<type name>, subtype N" when the subtype has no name, the bare type name for subtype 0,
153/// and "unknown type 0xNN" when the type is not in the table.
154std::string format_device_kind(DeviceType type, uint8_t subtype);
155
156/// @brief Wire values of a (type, subtype), for logs that must stay readable without the table.
157/// @param type Node type.
158/// @param subtype Subtype.
159/// @return "0xTT/S", for example "0x34/1".
160std::string format_device_ids(DeviceType type, uint8_t subtype);
161
162/// @brief The features of a subtype-58 variant of a kind.
163/// @details VELUX KLF 200 API specification Appendix 2 defines subtype 58 as "main parameter only" for
164/// a few node types; this component applies the same rule to every type and, from issue #163,
165/// also reads it as "no intermediate values". The variant clears LEVELS, TILT and
166/// EXTENDED_POSITION_FRAME (each needs a functional parameter or an intermediate value) and sets
167/// MAIN_PARAMETER_ONLY. Named positions are main-parameter values of the family's actuator
168/// profile (KLF 200 section 14.2) and are kept. Used by the 58 rows and by the lookup fallback.
169/// @param type_features The features of the kind the variant derives from.
170/// @return The variant's features.
172 using namespace device_feature;
173 return static_cast<DeviceFeatures>((type_features & ~(LEVELS | TILT | EXTENDED_POSITION_FRAME)) |
174 MAIN_PARAMETER_ONLY);
175}
176
177// === Main-parameter values ===
178
179/// @brief Combine the two bytes of a main parameter as the EXECUTE payload carries them.
180/// @param high First (high) byte.
181/// @param low Second (low) byte, the modifier of a named position.
182constexpr uint16_t main_parameter_of(uint8_t high, uint8_t low = 0) {
183 return static_cast<uint16_t>((static_cast<uint16_t>(high) << BITS_PER_BYTE) | low);
184}
185
186/// Main parameter that opens (or switches off) a device: percent 0.
187static constexpr uint16_t MAIN_PARAMETER_OPEN = 0x0000;
188/// Main parameter that closes (or switches on) a device: 100 percent, the top of the position scale.
189static constexpr uint16_t MAIN_PARAMETER_CLOSED = STATUS_POS_MAX;
190/// Main parameter that stops a movement.
191static constexpr uint16_t MAIN_PARAMETER_STOP = main_parameter_of(POS_STOP);
192/// What position_main_parameter() returns for a value that is no position; no device accepts it.
193static constexpr uint16_t MAIN_PARAMETER_INVALID = 0xFFFF;
194
195/// @brief One named position: its feature bit, its main-parameter value and the command that sends it.
196/// @details Named positions are main-parameter values defined per actuator profile (VELUX KLF 200 API
197/// specification, section 14.2). NAMED_POSITIONS is the only place a value, its bit and its
198/// command are paired; the frame builder, the value check, the command names and the Python
199/// mirror all derive from it.
201 DeviceFeatures feature; ///< The bit a device needs to accept it.
202 uint16_t main_parameter; ///< Value sent as the EXECUTE main parameter.
203 CoverCommand command; ///< The command that sends it.
204 const char *name; ///< cover_command_name() of the command ("FAVORITE", "VENT", "PEDESTRIAN", "PARTIAL").
205};
206
207/// @brief The named positions.
208/// @details PARTIAL (0xD809) runs a garage opener's stored partial position (issue #163). PEDESTRIAN
209/// is experimental: its value rests on no public source. A motor without the position
210/// refuses either (PARAMETER_UNKNOWN) and nothing moves.
211/// \todo Confirm 0xD807 on a real gate opener.
212/// Adding one is a bit (with its evidence), a row here, a CoverCommand value, the rows that
213/// have it in the type table, and a companion button in the Python codegen.
214inline constexpr NamedPosition NAMED_POSITIONS[] = {
215 {device_feature::FAVORITE, main_parameter_of(POS_FAVORITE), CoverCommand::FAVORITE, "FAVORITE"},
216 {device_feature::VENT, main_parameter_of(POS_FAVORITE, POS_VENT_MODIFIER), CoverCommand::VENT, "VENT"},
217 {device_feature::PEDESTRIAN, main_parameter_of(POS_FAVORITE, POS_PEDESTRIAN_MODIFIER), CoverCommand::PEDESTRIAN,
218 "PEDESTRIAN"},
219 {device_feature::PARTIAL, main_parameter_of(POS_FAVORITE, POS_PARTIAL_MODIFIER), CoverCommand::PARTIAL, "PARTIAL"},
220};
221
222/// @brief The named position a command sends.
223/// @param command Cover command.
224/// @return The NAMED_POSITIONS entry, or nullptr for a command that is not a named position.
226 for (const NamedPosition &named : NAMED_POSITIONS) {
227 if (named.command == command)
228 return &named;
229 }
230 return nullptr;
231}
232
233/// @brief The main parameter a named command sends.
234/// @details STOP is the one command that is a main-parameter value without being a named position;
235/// every other value comes from NAMED_POSITIONS. The frame builders for 2W and 1W both read
236/// their bytes here, so a value is written down once.
237/// @param command Cover command.
238/// @param main_parameter Receives the value on success.
239/// @return true for STOP and for every command in NAMED_POSITIONS; false for any other command
240/// (FORCE_OPEN is built from a position, not from a named value).
241inline bool command_main_parameter(CoverCommand command, uint16_t &main_parameter) {
242 if (command == CoverCommand::STOP) {
243 main_parameter = MAIN_PARAMETER_STOP;
244 return true;
245 }
246 const NamedPosition *named = find_named_position(command);
247 if (named == nullptr)
248 return false;
249 main_parameter = named->main_parameter;
250 return true;
251}
252
253/// @brief True when a device with these features accepts this main-parameter value in an EXECUTE.
254/// @details Open (0x0000), closed (0xC800) and stop (0xD200) always; 0x0001-0xC7FF only with LEVELS;
255/// a named position only with its bit; anything else never.
256/// @param features The device's resolved features.
257/// @param main_parameter The main parameter an EXECUTE would carry.
258constexpr bool device_accepts_main_parameter(DeviceFeatures features, uint16_t main_parameter) {
259 if (main_parameter == MAIN_PARAMETER_OPEN || main_parameter == MAIN_PARAMETER_CLOSED ||
260 main_parameter == MAIN_PARAMETER_STOP)
261 return true;
262 if (main_parameter < MAIN_PARAMETER_CLOSED)
263 return has_feature(features, device_feature::LEVELS);
264 for (const NamedPosition &named : NAMED_POSITIONS) {
265 if (named.main_parameter == main_parameter)
266 return has_feature(features, named.feature);
267 }
268 return false;
269}
270
271/// @brief The main parameter set_device_position() sends for a percent position or the POS_STOP sentinel.
272/// @details The position guard converts with this and never compares a percent against wire values.
273/// @param position Percent 0-100 (IO scale), or POS_STOP.
274/// @return POS_STOP gives MAIN_PARAMETER_STOP; 0-100 gives the percent on the doubled wire scale;
275/// anything else gives MAIN_PARAMETER_INVALID, which no device accepts.
276constexpr uint16_t position_main_parameter(uint8_t position) {
277 if (position == POS_STOP)
278 return MAIN_PARAMETER_STOP;
279 if (position > POSITION_WIRE_MAX / POSITION_WIRE_SCALE)
281 return main_parameter_of(static_cast<uint8_t>(POSITION_WIRE_SCALE * position));
282}
283
284} // namespace home_io_control
285} // namespace esphome
Capability bits carried by a DeviceTypeInfo row and resolved onto a device record.
Bit flags carried by a DeviceTypeInfo row: identity facts. Capabilities are DeviceFeatures.
static constexpr uint8_t YAML_SELECTABLE
Type rows only: the type's identifier is accepted by the YAML io_device_type key (hub_validators....
static constexpr uint8_t EXPERIMENTAL
Subtype rows only: no capture or report has confirmed this kind on a real device, so what the row say...
static constexpr uint8_t HAS_PLATFORM
An ESPHome platform of the row's capability class can drive this (type, subtype).
static constexpr uint8_t STANDALONE_NAME
Subtype rows only: name already is the complete description ("window lock") and is printed alone,...
DeviceFeatures device_features(DeviceType type, uint8_t subtype, DeviceModel model)
Capability bits of a (type, subtype, model).
constexpr uint16_t main_parameter_of(uint8_t high, uint8_t low=0)
Combine the two bytes of a main parameter as the EXECUTE payload carries them.
DeviceFeatures undeclared_kind_features(DeviceCapabilityClass entity_class)
Features of a device kind that has no table row, or that is not declared at all.
std::string format_device_ids(DeviceType type, uint8_t subtype)
Wire values of a (type, subtype), for logs that must stay readable without the table.
DeviceCapabilityClass device_capability_class(DeviceType type)
Map a raw IO‑Homecontrol type to the closest ESPHome/Home Assistant entity family.
static constexpr uint16_t MAIN_PARAMETER_OPEN
Main parameter that opens (or switches off) a device: percent 0.
CoverCommand
Named device commands for cover-type actuators.
@ FAVORITE
Move to stored favorite/"My" position.
@ VENT
Move to ventilation position (window-type devices).
@ PEDESTRIAN
Move to the pedestrian position (gate openers); experimental.
@ PARTIAL
Move to the stored partial position (garage openers; issue #163).
const DeviceTypeInfo * find_device_type_info(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the table row for a (type, subtype, model).
DeviceModel
A product model below a (node type, subtype): for products that report the same wire type but need di...
static constexpr uint8_t POS_PEDESTRIAN_MODIFIER
Modifier byte for the pedestrian-position command (POS_FAVORITE main byte, as for ventilation).
const DeviceTypeInfo * find_device_model_info(DeviceType type, uint8_t subtype, DeviceModel model)
Look up the model row with exactly this (type, subtype, model), without falling back.
const NamedPosition * find_named_position(CoverCommand command)
The named position a command sends.
DeviceType
IO-Homecontrol node types.
constexpr NamedPosition NAMED_POSITIONS[]
The named positions.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
static constexpr uint8_t SUBTYPE_HEATER_WITH_SETPOINT
Subtype of the electrical heater (node type 52) with an adjustable setpoint, the one driven over the ...
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.
static constexpr uint16_t MAIN_PARAMETER_CLOSED
Main parameter that closes (or switches on) a device: 100 percent, the top of the position scale.
bool device_offers_models(DeviceType type, uint8_t subtype)
Does this exact (type, subtype) have model rows below it?
constexpr uint16_t position_main_parameter(uint8_t position)
The main parameter set_device_position() sends for a percent position or the POS_STOP sentinel.
constexpr DeviceFeatures main_parameter_only_variant(DeviceFeatures type_features)
The features of a subtype-58 variant of a kind.
static constexpr uint16_t MAIN_PARAMETER_STOP
Main parameter that stops a movement.
const char * device_platform_name(DeviceType type, uint8_t subtype)
Name of the ESPHome platform that drives this (type, subtype).
static constexpr uint16_t MAIN_PARAMETER_INVALID
What position_main_parameter() returns for a value that is no position; no device accepts it.
bool device_has_platform(DeviceType type, uint8_t subtype)
Can an ESPHome platform drive this (type, subtype) today?
bool device_is_experimental(DeviceType type, uint8_t subtype)
Is this (type, subtype) a kind no capture or report has confirmed on a real device?
uint16_t DeviceFeatures
Capability bits of a device kind; see device_feature.
bool command_main_parameter(CoverCommand command, uint16_t &main_parameter)
The main parameter a named command sends.
static constexpr uint8_t POS_PARTIAL_MODIFIER
Modifier byte for the partial-position command (POS_FAVORITE main byte, as for ventilation).
DeviceCapabilityClass
High‑level capability class derived from DeviceType.
constexpr bool device_accepts_main_parameter(DeviceFeatures features, uint16_t main_parameter)
True when a device with these features accepts this main-parameter value in an EXECUTE.
std::string format_device_kind(DeviceType type, uint8_t subtype)
Human-readable description of a (type, subtype) for logs and reports.
IO-Homecontrol command IDs, result codes and protocol enumerations.
Capability bits of a device kind: what a (node type, subtype) can do.
IO-Homecontrol device-type model, capabilities and runtime device state.
One row of the node-type / subtype table.
uint8_t flags
device_type_flags bits.
uint8_t subtype
Subtype; 0 is the type's own row.
DeviceFeatures features
device_feature bits: the whole word, never merged with the type row's.
const char * name
Type row: lowercase identifier ("roller_shutter").
DeviceModel model
NONE for a type or subtype row; the model of a model row.
DeviceCapabilityClass capability
Entity family the device maps to.
One named position: its feature bit, its main-parameter value and the command that sends it.
uint16_t main_parameter
Value sent as the EXECUTE main parameter.
CoverCommand command
The command that sends it.
const char * name
cover_command_name() of the command ("FAVORITE", "VENT", "PEDESTRIAN", "PARTIAL").
DeviceFeatures feature
The bit a device needs to accept it.