Home IO Control
ESPHome add-on for IO-Homecontrol devices
Loading...
Searching...
No Matches
oneway_controller.h
Go to the documentation of this file.
1#pragma once
2
3/// @file oneway_controller.h
4/// @brief Controller identities for the one-way (1W) protocol.
5/// @ingroup hioc_protocol
6///
7/// 1W frames are **class-addressed**: a command goes to a typed broadcast address
8/// `(io_device_type << 6) | 0x3F`, not to an individual device. Nothing on the wire names a
9/// device, so a 1W entity has no node address to bind to. What distinguishes one 1W control
10/// surface from another is the *controller* doing the transmitting — its source address, its
11/// network key, and the device class it speaks to. That triple is a controller identity, and it
12/// takes the place node addressing has for 2W. See ADR 0027.
13///
14/// A hub holds several, deliberately: adopting a foreign 1W network's key (see
15/// oneway_key_adoption.cpp) produces an identity whose key is *not* the hub's own, and it
16/// must coexist with identities on the hub's own network rather than replace them.
17///
18/// @note Ownership. These identities belong to the `OneWayTransmitter` collaborator
19/// (oneway_transmitter.h), held by value in the hub, which keeps only the wiring (ADR 0004).
20/// This header owns the types; it does not own an instance of them.
21
22#include "proto_codecs.h"
23#include "proto_constants.h"
24#include "proto_device_model.h"
25#include "proto_device_types.h"
26#include "proto_sizes.h"
27
28#include <array>
29#include <cstdint>
30#include <optional>
31#include <string>
32#include <vector>
33
34namespace esphome {
35namespace home_io_control {
36
37/// @brief Which preamble/CTRL1 shape a 1W identity's bursts actually go out with.
38///
39/// Every enumerator is a real on-air shape, so a value of this type is always transmittable —
40/// "not configured" is *not* one of them. That state lives in
41/// `OneWayControllerIdentity::power_class_override`, an empty optional, and is resolved to one of
42/// these by effective_power_class() before any of it reaches the transmitter. Keeping the two
43/// apart is what lets oneway_burst_copy_shape() switch exhaustively with no unreachable case.
44///
45/// See ADR 0038 for the shapes and ADR 0041 for how an unset key resolves.
46enum class OneWayPowerClass : uint8_t {
47 LEGACY_LONG, ///< LONG_PREAMBLE on every copy, CTRL1 0x00 everywhere.
48 ALWAYS_ALIVE, ///< `low_power: false`: normal start preamble on every copy, CTRL1 0x00.
49 LOW_POWER, ///< `low_power: true`: copy 1 LONG_PREAMBLE + CTRL1_LOW_POWER, repeats normal.
50};
51
52/// @brief Human-readable name for a OneWayPowerClass, as it appears in the boot log.
53/// @param power_class Power class to name.
54/// @return Null-terminated name such as "always-alive".
55const char *oneway_power_class_name(OneWayPowerClass power_class);
56
57/// @brief Which wake-up shape one copy of a 1W burst gets.
58enum class OneWayPreamble : uint8_t {
59 WAKE, ///< The long wake-up preamble (`LONG_PREAMBLE`), paired with `CTRL1_LOW_POWER` set.
60 NORMAL, ///< The runtime-tunable `normal_start_preamble`, CTRL1's low-power bit clear.
61};
62
63/// @brief The preamble and CTRL1 shape one copy of a 1W burst gets.
64///
65/// Chip-neutral by design: this header names bytes, not chips, and does not know about
66/// `TuningConfig` — resolving `OneWayPreamble::NORMAL` to an actual byte count is
67/// `OneWayTransmitter`'s job (oneway_transmitter.h), the one place in the controller layer that
68/// holds a `TuningConfig *`.
70 OneWayPreamble preamble; ///< Which preamble this copy transmits with.
71 bool low_power_flag; ///< Whether this copy sets `CTRL1_LOW_POWER`.
72};
73
74/// @brief Resolve which preamble/CTRL1 shape one copy of a burst gets, from the identity's power
75/// class and the copy's position in the burst.
76///
77/// Pure: no radio, no tuning, testable on its own. Implements the table in ADR 0038 — `LEGACY_LONG`
78/// and `ALWAYS_ALIVE` are uniform across every copy; only `LOW_POWER` varies by position, putting
79/// the wake-up shape on copy 0 alone — what a real remote's GEAR/EXECUTE burst does, and the
80/// mirror of how `ExchangeEngine` (ADR 0029) already picks a 2W start frame's preamble from the
81/// target's own low-power declaration.
82/// @param power_class The identity's resolved power class, from effective_power_class().
83/// @param copy_index 0-based position of this copy within the burst.
84/// @return The preamble and CTRL1 shape for that copy.
85inline OneWayCopyShape oneway_burst_copy_shape(OneWayPowerClass power_class, uint8_t copy_index) {
86 switch (power_class) {
88 return {OneWayPreamble::NORMAL, /*low_power_flag=*/false};
90 return copy_index == 0 ? OneWayCopyShape{OneWayPreamble::WAKE, /*low_power_flag=*/true}
91 : OneWayCopyShape{OneWayPreamble::NORMAL, /*low_power_flag=*/false};
93 default:
94 return {OneWayPreamble::WAKE, /*low_power_flag=*/false};
95 }
96}
97
98/// @brief One configured 1W controller identity.
99///
100/// Fixed-size key and address material; the only heap is the `id` handle, which mirrors how
101/// device IDs are held elsewhere in this component.
102/// @ingroup hioc_protocol
104 std::string id; ///< YAML handle entities reference.
105 uint8_t node_id[NODE_ID_SIZE]{}; ///< Source address we transmit as (configured or derived).
106 uint8_t system_key[AES_KEY_SIZE]{}; ///< Network key for this identity; may differ per identity.
107 uint8_t manufacturer{0}; ///< Manufacturer ID; unused until the enrollment phase.
108 DeviceType io_device_type{DeviceType::UNKNOWN}; ///< Device class this identity commands.
109 uint16_t initial_sequence{0}; ///< Seed for this identity's rolling counter on first use.
110 bool node_id_derived{false}; ///< True when `node_id` was derived rather than configured.
112 false}; ///< Whether the enroll button's 0x30 carries a MAC trailer (`enrollment_with_mac:`).
113 /// EXECUTE ACEI override (`execute_acei:`). 0 = "not overridden, use resolve_oneway_wire_profile()";
114 /// any non-zero value wins. 0 is a safe sentinel: ACEI_VALID_BIT is bit 0, so a valid ACEI is
115 /// always odd — the schema rejects `execute_acei: 0` so this cannot be reached by mistake.
116 uint8_t execute_acei{0};
117 /// When true (`execute_broadcast: all`), 1W EXECUTE frames go to the all-devices address
118 /// `00 00 3F` regardless of `io_device_type` — what a handheld cover remote of either vendor
119 /// does. false = the typed per-class destination (current default). Not a vendor axis; see
120 /// ADR 0031.
122 /// Override for the device classes a VELUX enrollment `0x30` sweep targets (`enrollment_classes:`).
123 /// All-`UNKNOWN` (the default) means "not set — use the manufacturer profile's list"
124 /// (`resolve_oneway_wire_profile()`), which only fits exterior shading; a KLI 312 interior blind
125 /// needs `{VERTICAL_INTERIOR_BLIND, INTERIOR_VENETIAN_BLIND}` here. `UNKNOWN` entries are skipped when the sweep
126 /// runs, so a one- or two-class override is expressed by leaving the rest `UNKNOWN`. Ignored by the Somfy enrollment
127 /// gesture, which always uses `io_device_type`. See ADR 0032.
129 /// `low_power:` exactly as configured, or **empty when the key is absent** — in which case the
130 /// shape comes from the manufacturer profile (ADR 0041). Never read this directly to transmit;
131 /// call effective_power_class(), which is the only place the two cases are collapsed.
132 ///
133 /// Whatever it resolves to applies to every 1W TX of the identity -- commands, positions,
134 /// enrollment (both gestures), un-enrollment. Last field: codegen emits a designated initialiser
135 /// in declaration order. See ADR 0038 for the shapes themselves.
136 std::optional<OneWayPowerClass> power_class_override;
137 /// `invert_position:` exactly as configured, or **empty when the key is absent** — in which case
138 /// the device kind decides (`INVERTED` capability bit). Never read this directly to transmit;
139 /// call effective_inverted(). Declared after `power_class_override` because codegen emits a
140 /// designated initialiser in declaration order.
141 std::optional<bool> invert_override;
142
143 /// @brief Enrollment / typed-class destination address for this identity.
144 ///
145 /// 1W addresses a device *class*, never a node. Delegates to encode_broadcast_address()
146 /// (proto_codecs.h), the single place the bit layout is documented, so this and
147 /// broadcast_target_type() (its decode counterpart) cannot drift apart. Note: an EXECUTE frame
148 /// uses the all-devices address instead when `execute_broadcast_all` is set — this helper is the
149 /// typed-class destination only.
150 /// @param out Output: 3-byte destination address.
151 void broadcast_address(uint8_t out[NODE_ID_SIZE]) const { encode_broadcast_address(this->io_device_type, out); }
152};
153
154/// @brief Which 1W enrollment gesture a manufacturer's actuators expect.
155///
156/// SOMFY: one `0x30` add-controller burst to the identity's own `io_device_type` (the shape this
157/// project has hardware-validated). VELUX_KLI: a `0x39` clear to the all-devices address, then a
158/// `0x30` burst to **each** class in `OneWayWireProfile::enrollment_classes`, then a STOP+DOWN
159/// EXECUTE follow-up — the gesture a real KLI remote produces (issue #74 capture +
160/// the KLI manual), confirmed on a VELUX SML roller shutter and on KLI 312
161/// interior blinds. See ADR 0032.
162enum class EnrollGesture : uint8_t { SOMFY, VELUX_KLI };
163
164/// @brief Vendor-divergent 1W wire settings for a controller identity.
165///
166/// The EXECUTE ACEI is decided per ADR 0031; the enrollment gesture and the class sweep per
167/// ADR 0032.
169 uint8_t execute_acei; ///< payload[1] of a 1W CMD_EXECUTE (0x00) frame.
170 bool profile_is_a_guess; ///< true when `manufacturer` matched no known 1W wire profile.
171 EnrollGesture enroll_gesture; ///< Which enrollment gesture this manufacturer's actuators expect.
172 /// Device classes a VELUX_KLI `0x30` sweep targets, `UNKNOWN` entries skipped. All-`UNKNOWN` for
173 /// SOMFY, whose `0x30` goes to the identity's own `io_device_type` instead.
174 std::array<DeviceType, 3> enrollment_classes;
175 /// Burst shape for an identity that does not set `low_power:` at all. Per ADR 0041: VELUX gets
176 /// `ALWAYS_ALIVE`, because an awake VELUX receiver does not accept a frame behind the 1024-byte
177 /// preamble and 1W has no acknowledgement to reveal that. Every other manufacturer — and every
178 /// unrecognised one — keeps `LEGACY_LONG`, so a profile nobody has measured never silently
179 /// acquires a shape nobody tested for it.
181};
182
183/// The default `0x30` sweep for `manufacturer: velux`: the exterior-shading classes a KLI 310/313
184/// names — roller shutter, vertical exterior awning, dual roller shutter (issue #74 capture, decoded with
185/// `broadcast_target_type()`).
186/// Not universal: a KLI 312 interior blind uses vertical interior blind + interior venetian blind, so those identities
187/// set `enrollment_classes:` from the classes their remote's own `0x2E` names.
190
191/// @brief Resolve an identity's 1W wire profile from its manufacturer byte.
192///
193/// Pure. Somfy (0x02) and unset (0x00) both map to the historical Somfy-shaped default
194/// (`ONEWAY_EXECUTE_ACEI`, `EnrollGesture::SOMFY`); only VELUX (0x01) is special so far
195/// (`ONEWAY_EXECUTE_ACEI_VELUX`, `EnrollGesture::VELUX_KLI`, the class sweep). Any other
196/// explicitly-set manufacturer returns the Somfy default with `profile_is_a_guess=true` so the
197/// Python schema can warn (`oneway_controllers.py` `validate_oneway_controllers()` — keep the {somfy,
198/// velux} set here in sync with the warning there; there is no automated check).
199/// @param manufacturer The identity's manufacturer byte (`MANUFACTURER_*`, or a raw value).
200inline OneWayWireProfile resolve_oneway_wire_profile(uint8_t manufacturer) {
201 constexpr std::array<DeviceType, 3> none{DeviceType::UNKNOWN, DeviceType::UNKNOWN, DeviceType::UNKNOWN};
202 switch (manufacturer) {
203 case MANUFACTURER_VELUX:
204 return {ONEWAY_EXECUTE_ACEI_VELUX, /*profile_is_a_guess=*/false, EnrollGesture::VELUX_KLI,
206 case MANUFACTURER_SOMFY:
207 case 0x00:
208 return {ONEWAY_EXECUTE_ACEI, /*profile_is_a_guess=*/false, EnrollGesture::SOMFY, none,
210 default:
211 return {ONEWAY_EXECUTE_ACEI, /*profile_is_a_guess=*/true, EnrollGesture::SOMFY, none,
213 }
214}
215
216/// @brief The device classes this identity's `0x30` enrollment sweep will actually target.
217/// @param identity The controller identity.
218/// @return `enrollment_classes` when the identity overrode it (any entry non-`UNKNOWN`), else the
219/// manufacturer profile's list. `UNKNOWN` entries are skipped by the caller.
220inline std::array<DeviceType, 3> effective_enrollment_classes(const OneWayControllerIdentity &identity) {
221 const bool overridden = identity.enrollment_classes[0] != DeviceType::UNKNOWN ||
224 return overridden ? identity.enrollment_classes
226}
227
228/// @brief The ACEI byte a given identity will put on air for a 1W EXECUTE frame.
229/// @param identity The controller identity.
230/// @return `execute_acei` when overridden (non-zero), else the manufacturer's profile default.
231inline uint8_t effective_execute_acei(const OneWayControllerIdentity &identity) {
232 return identity.execute_acei != 0 ? identity.execute_acei
234}
235
236/// @brief Whether this identity's ACEI comes from an explicit `execute_acei:` rather than the profile.
237/// @param identity The controller identity.
238/// @return true when `execute_acei:` was set (non-zero) and overrides the manufacturer profile default.
239inline bool has_execute_acei_override(const OneWayControllerIdentity &identity) { return identity.execute_acei != 0; }
240
241/// @brief The burst shape this identity actually transmits with.
242///
243/// The one place an absent `low_power:` is turned into a real shape, and therefore the only
244/// function the transmitter and the boot log may ask. An explicit `low_power:` always wins; with
245/// the key absent the manufacturer profile decides (ADR 0041).
246/// @param identity The controller identity.
247/// @return `power_class_override` when set, else the manufacturer profile's `default_power_class`.
251
252/// @brief Whether this identity's burst shape comes from an explicit `low_power:` rather than the profile.
253/// @param identity The controller identity.
254/// @return true when `low_power:` was set in YAML and overrides the manufacturer profile default.
256 return identity.power_class_override.has_value();
257}
258
259/// @brief Whether this identity's OPEN/CLOSE/position values are mirrored on the wire.
260///
261/// An inverted device kind (a horizontal awning, for example) treats wire position 0 as closed,
262/// the same way the 2W cover path handles `IoDevice::inverted`. An explicit `invert_position:`
263/// always wins; with the key absent the identity's `io_device_type` decides.
264/// \todo Confirm on a second inverted-kind 1W receiver.
265/// @param identity The controller identity.
266/// @return `invert_override` when set, else the kind's `INVERTED` capability bit.
267inline bool effective_inverted(const OneWayControllerIdentity &identity) {
268 return identity.invert_override.value_or(
269 has_feature(device_features(identity.io_device_type, 0, DeviceModel::NONE), device_feature::INVERTED));
270}
271
272/// @brief Whether this identity's inversion comes from an explicit `invert_position:` rather than the kind.
273/// @param identity The controller identity.
274/// @return true when `invert_position:` was set in YAML.
275inline bool has_invert_override(const OneWayControllerIdentity &identity) {
276 return identity.invert_override.has_value();
277}
278
279// === Control surface ===
280
281/// User-scale position meaning "fully closed" (0 means fully open). Named here because the two
282/// values are what OPEN and CLOSE are before OneWayTransmitter::send_position() mirrors them for an
283/// inverted identity — see encode_oneway_action() and effective_inverted().
284static constexpr uint8_t ONEWAY_POSITION_FULLY_OPEN = 0;
285static constexpr uint8_t ONEWAY_POSITION_FULLY_CLOSED = 100;
286
287/// @brief The command a generated 1W button sends.
288///
289/// The vocabulary of the `commands:` list on a `oneway_controllers:` entry. Kept separate from
290/// CoverCommand because two of these are not commands at all on the wire: OPEN and CLOSE are
291/// positions 0 and 100, and only look like named commands to a user.
292/// @ingroup hioc_protocol
293enum class OneWayButtonAction : uint8_t {
294 OPEN, ///< Position 0 (fully open).
295 CLOSE, ///< Position 100 (fully closed).
296 STOP, ///< CoverCommand::STOP.
297 VENT, ///< CoverCommand::VENT.
298 FAVORITE, ///< CoverCommand::FAVORITE.
299};
300
301/// @brief How a OneWayButtonAction reaches the wire.
302/// @ingroup hioc_protocol
304 bool is_position{false}; ///< True when the action is sent as a numeric position.
305 uint8_t position{0}; ///< Position to send when `is_position`.
306 CoverCommand command{CoverCommand::STOP}; ///< Named command to send otherwise.
307};
308
309/// @brief Resolve a button action to the call that sends it.
310///
311/// Pure, so the mapping can be tested without a radio, an entity or a hub. OPEN and CLOSE resolve
312/// to positions because that is what they are on the wire — there is no distinct open/close
313/// opcode, and treating them as named commands would need a second encoding path for no gain. The
314/// positions are user positions (0 = open); the transmitter mirrors them for an inverted identity.
315/// @param action Button action to encode.
316/// @return The position-or-command the transmitter should send.
317/// @ingroup hioc_protocol
319 OneWayActionEncoding encoding{};
320 switch (action) {
322 encoding.is_position = true;
324 break;
326 encoding.is_position = true;
328 break;
330 encoding.command = CoverCommand::VENT;
331 break;
334 break;
336 default:
337 encoding.command = CoverCommand::STOP;
338 break;
339 }
340 return encoding;
341}
342
343/// @brief Human-readable name for a button action, as it appears in the diagnostic sensor.
344/// @param action Button action to name.
345/// @return Null-terminated name such as "OPEN".
346/// @ingroup hioc_protocol
348
349/// @brief The configured 1W controller identities, in YAML declaration order.
350///
351/// Lookup is by `id` and linear: a hub has a handful of identities, not hundreds, and keeping
352/// insertion order makes boot logging read the same as the YAML that produced it.
353/// @ingroup hioc_protocol
355 public:
356 /// @brief Add a configured identity. Called from generated code at setup.
357 /// @param identity Fully-resolved identity (key inherited or explicit, address configured or derived).
358 void add(const OneWayControllerIdentity &identity) { this->identities_.push_back(identity); }
359
360 /// @brief Look up an identity by its YAML handle.
361 /// @param id Handle to find.
362 /// @return Pointer to the identity, or nullptr if no such handle is configured.
363 [[nodiscard]] const OneWayControllerIdentity *get(const std::string &id) const {
364 for (const auto &identity : this->identities_) {
365 if (identity.id == id)
366 return &identity;
367 }
368 return nullptr;
369 }
370
371 /// @brief All configured identities, in declaration order.
372 [[nodiscard]] const std::vector<OneWayControllerIdentity> &all() const { return this->identities_; }
373
374 /// @brief Whether any identity is configured.
375 [[nodiscard]] bool empty() const { return this->identities_.empty(); }
376
377 private:
378 std::vector<OneWayControllerIdentity> identities_;
379};
380
381} // namespace home_io_control
382} // namespace esphome
The configured 1W controller identities, in YAML declaration order.
void add(const OneWayControllerIdentity &identity)
Add a configured identity.
const OneWayControllerIdentity * get(const std::string &id) const
Look up an identity by its YAML handle.
bool empty() const
Whether any identity is configured.
const std::vector< OneWayControllerIdentity > & all() const
All configured identities, in declaration order.
const char * oneway_button_action_name(OneWayButtonAction action)
Human-readable name for a button action, as it appears in the diagnostic sensor.
OneWayActionEncoding encode_oneway_action(OneWayButtonAction action)
Resolve a button action to the call that sends it.
OneWayButtonAction
The command a generated 1W button sends.
EnrollGesture
Which 1W enrollment gesture a manufacturer's actuators expect.
DeviceFeatures device_features(DeviceType type, uint8_t subtype, DeviceModel model)
Capability bits of a (type, subtype, model).
OneWayPreamble
Which wake-up shape one copy of a 1W burst gets.
@ NORMAL
The runtime-tunable normal_start_preamble, CTRL1's low-power bit clear.
@ WAKE
The long wake-up preamble (LONG_PREAMBLE), paired with CTRL1_LOW_POWER set.
void encode_broadcast_address(DeviceType type, uint8_t out[NODE_ID_SIZE])
Encode a device type into its typed-broadcast destination address — the exact inverse of broadcast_ta...
static constexpr uint8_t ONEWAY_POSITION_FULLY_OPEN
User-scale position meaning "fully closed" (0 means fully open).
CoverCommand
Named device commands for cover-type actuators.
@ FAVORITE
Move to stored favorite/"My" position.
@ VENT
Move to ventilation position (window-type devices).
OneWayPowerClass effective_power_class(const OneWayControllerIdentity &identity)
The burst shape this identity actually transmits with.
bool effective_inverted(const OneWayControllerIdentity &identity)
Whether this identity's OPEN/CLOSE/position values are mirrored on the wire.
@ NONE
No model: the (type, subtype) row.
DeviceType
IO-Homecontrol node types.
@ VERTICAL_EXTERIOR_AWNING
Vertical exterior awning.
@ UNKNOWN
Unknown/unspecified device.
@ DUAL_ROLLER_SHUTTER
Dual-section roller shutter.
constexpr bool has_feature(DeviceFeatures set, DeviceFeatures bit)
True when every bit of bit is set in set.
std::array< DeviceType, 3 > effective_enrollment_classes(const OneWayControllerIdentity &identity)
The device classes this identity's 0x30 enrollment sweep will actually target.
uint8_t effective_execute_acei(const OneWayControllerIdentity &identity)
The ACEI byte a given identity will put on air for a 1W EXECUTE frame.
static constexpr std::array< DeviceType, 3 > VELUX_KLI_ENROLLMENT_CLASSES
The default 0x30 sweep for manufacturer: velux: the exterior-shading classes a KLI 310/313 names — ro...
OneWayPowerClass
Which preamble/CTRL1 shape a 1W identity's bursts actually go out with.
@ LOW_POWER
low_power: true: copy 1 LONG_PREAMBLE + CTRL1_LOW_POWER, repeats normal.
@ ALWAYS_ALIVE
low_power: false: normal start preamble on every copy, CTRL1 0x00.
@ LEGACY_LONG
LONG_PREAMBLE on every copy, CTRL1 0x00 everywhere.
const char * oneway_power_class_name(OneWayPowerClass power_class)
Human-readable name for a OneWayPowerClass, as it appears in the boot log.
bool has_invert_override(const OneWayControllerIdentity &identity)
Whether this identity's inversion comes from an explicit invert_position: rather than the kind.
bool has_power_class_override(const OneWayControllerIdentity &identity)
Whether this identity's burst shape comes from an explicit low_power: rather than the profile.
static constexpr uint8_t ONEWAY_POSITION_FULLY_CLOSED
OneWayWireProfile resolve_oneway_wire_profile(uint8_t manufacturer)
Resolve an identity's 1W wire profile from its manufacturer byte.
bool has_execute_acei_override(const OneWayControllerIdentity &identity)
Whether this identity's ACEI comes from an explicit execute_acei: rather than the profile.
OneWayCopyShape oneway_burst_copy_shape(OneWayPowerClass power_class, uint8_t copy_index)
Resolve which preamble/CTRL1 shape one copy of a burst gets, from the identity's power class and the ...
Device-name, address-classification and 1W-frame codecs.
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.
Fundamental IO-Homecontrol frame and crypto size constants.
How a OneWayButtonAction reaches the wire.
bool is_position
True when the action is sent as a numeric position.
uint8_t position
Position to send when is_position.
CoverCommand command
Named command to send otherwise.
One configured 1W controller identity.
uint8_t node_id[NODE_ID_SIZE]
Source address we transmit as (configured or derived).
std::string id
YAML handle entities reference.
bool execute_broadcast_all
When true (execute_broadcast: all), 1W EXECUTE frames go to the all-devices address 00 00 3F regardle...
bool enrollment_with_mac
Whether the enroll button's 0x30 carries a MAC trailer (enrollment_with_mac:).
std::optional< bool > invert_override
invert_position: exactly as configured, or empty when the key is absent — in which case the device ki...
uint8_t system_key[AES_KEY_SIZE]
Network key for this identity; may differ per identity.
void broadcast_address(uint8_t out[NODE_ID_SIZE]) const
Enrollment / typed-class destination address for this identity.
uint8_t execute_acei
EXECUTE ACEI override (execute_acei:).
uint16_t initial_sequence
Seed for this identity's rolling counter on first use.
bool node_id_derived
True when node_id was derived rather than configured.
std::optional< OneWayPowerClass > power_class_override
low_power: exactly as configured, or empty when the key is absent — in which case the shape comes fro...
DeviceType io_device_type
Device class this identity commands.
uint8_t manufacturer
Manufacturer ID; unused until the enrollment phase.
std::array< DeviceType, 3 > enrollment_classes
Override for the device classes a VELUX enrollment 0x30 sweep targets (enrollment_classes:).
The preamble and CTRL1 shape one copy of a 1W burst gets.
OneWayPreamble preamble
Which preamble this copy transmits with.
bool low_power_flag
Whether this copy sets CTRL1_LOW_POWER.
Vendor-divergent 1W wire settings for a controller identity.
bool profile_is_a_guess
true when manufacturer matched no known 1W wire profile.
std::array< DeviceType, 3 > enrollment_classes
Device classes a VELUX_KLI 0x30 sweep targets, UNKNOWN entries skipped.
EnrollGesture enroll_gesture
Which enrollment gesture this manufacturer's actuators expect.
uint8_t execute_acei
payload[1] of a 1W CMD_EXECUTE (0x00) frame.
OneWayPowerClass default_power_class
Burst shape for an identity that does not set low_power: at all.