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_sizes.h"
26
27#include <array>
28#include <cstdint>
29#include <string>
30#include <vector>
31
32namespace esphome {
33namespace home_io_control {
34
35/// @brief One configured 1W controller identity.
36///
37/// Fixed-size key and address material; the only heap is the `id` handle, which mirrors how
38/// device IDs are held elsewhere in this component.
39/// @ingroup hioc_protocol
41 std::string id; ///< YAML handle entities reference.
42 uint8_t node_id[NODE_ID_SIZE]{}; ///< Source address we transmit as (configured or derived).
43 uint8_t system_key[AES_KEY_SIZE]{}; ///< Network key for this identity; may differ per identity.
44 uint8_t manufacturer{0}; ///< Manufacturer ID; unused until the enrollment phase.
45 DeviceType io_device_type{DeviceType::UNKNOWN}; ///< Device class this identity commands.
46 uint16_t initial_sequence{0}; ///< Seed for this identity's rolling counter on first use.
47 bool node_id_derived{false}; ///< True when `node_id` was derived rather than configured.
49 false}; ///< Whether the enroll button's 0x30 carries a MAC trailer (`enrollment_with_mac:`).
50 /// EXECUTE ACEI override (`execute_acei:`). 0 = "not overridden, use resolve_oneway_wire_profile()";
51 /// any non-zero value wins. 0 is a safe sentinel: ACEI_VALID_BIT is bit 0, so a valid ACEI is
52 /// always odd — the schema rejects `execute_acei: 0` so this cannot be reached by mistake.
53 uint8_t execute_acei{0};
54 /// When true (`execute_broadcast: all`), 1W EXECUTE frames go to the all-devices address
55 /// `00 00 3F` regardless of `io_device_type` — what a handheld cover remote of either vendor
56 /// does. false = the typed per-class destination (current default). Not a vendor axis; see
57 /// ADR 0031.
59 /// Override for the device classes a VELUX enrollment `0x30` sweep targets (`enrollment_classes:`).
60 /// All-`UNKNOWN` (the default) means "not set — use the manufacturer profile's list"
61 /// (`resolve_oneway_wire_profile()`). `UNKNOWN` entries are skipped when the sweep runs, so a
62 /// one- or two-class override is expressed by leaving the rest `UNKNOWN`. Ignored by the Somfy
63 /// enrollment gesture, which always uses `io_device_type`. See ADR 0032.
65
66 /// @brief Enrollment / typed-class destination address for this identity.
67 ///
68 /// 1W addresses a device *class*, never a node. Delegates to encode_broadcast_address()
69 /// (proto_codecs.h), the single place the bit layout is documented, so this and
70 /// broadcast_target_type() (its decode counterpart) cannot drift apart. Note: an EXECUTE frame
71 /// uses the all-devices address instead when `execute_broadcast_all` is set — this helper is the
72 /// typed-class destination only.
73 /// @param out Output: 3-byte destination address.
74 void broadcast_address(uint8_t out[NODE_ID_SIZE]) const { encode_broadcast_address(this->io_device_type, out); }
75};
76
77/// @brief Which 1W enrollment gesture a manufacturer's actuators expect.
78///
79/// SOMFY: one `0x30` add-controller burst to the identity's own `io_device_type` (the shape this
80/// project has hardware-validated). VELUX_KLI: a `0x39` clear to the all-devices address, then a
81/// `0x30` burst to **each** class in `OneWayWireProfile::enrollment_classes`, then a STOP+DOWN
82/// EXECUTE follow-up — the gesture a real KLI 310/313 PROG press produces (issue #74 capture +
83/// `samr037/iohc-flipper` `tx_runner.c` + the KLI manual). See ADR 0032.
84enum class EnrollGesture : uint8_t { SOMFY, VELUX_KLI };
85
86/// @brief Vendor-divergent 1W wire settings for a controller identity.
87///
88/// The EXECUTE ACEI is decided per ADR 0031; the enrollment gesture and the class sweep per
89/// ADR 0032.
91 uint8_t execute_acei; ///< payload[1] of a 1W CMD_EXECUTE (0x00) frame.
92 bool profile_is_a_guess; ///< true when `manufacturer` matched no known 1W wire profile.
93 EnrollGesture enroll_gesture; ///< Which enrollment gesture this manufacturer's actuators expect.
94 /// Device classes a VELUX_KLI `0x30` sweep targets, `UNKNOWN` entries skipped. All-`UNKNOWN` for
95 /// SOMFY, whose `0x30` goes to the identity's own `io_device_type` instead.
96 std::array<DeviceType, 3> enrollment_classes;
97};
98
99/// The three device classes a real VELUX KLI PROG gesture sweeps its `0x30` across — roller
100/// shutter, awning, dual shutter — and never any other (issue #74 capture, decoded with
101/// `broadcast_target_type()`; matches `samr037/iohc-flipper`'s `PAIR_DST_{WINDOW,SHUTTER,OTHER}`).
104
105/// @brief Resolve an identity's 1W wire profile from its manufacturer byte.
106///
107/// Pure. Somfy (0x02) and unset (0x00) both map to the historical Somfy-shaped default
108/// (`ONEWAY_EXECUTE_ACEI`, `EnrollGesture::SOMFY`); only VELUX (0x01) is special so far
109/// (`ONEWAY_EXECUTE_ACEI_VELUX`, `EnrollGesture::VELUX_KLI`, the class sweep). Any other
110/// explicitly-set manufacturer returns the Somfy default with `profile_is_a_guess=true` so the
111/// Python schema can warn (`__init__.py` `_validate_oneway_controllers()` — keep the {somfy,
112/// velux} set here in sync with the warning there; there is no automated check).
113/// @param manufacturer The identity's manufacturer byte (`MANUFACTURER_*`, or a raw value).
114inline OneWayWireProfile resolve_oneway_wire_profile(uint8_t manufacturer) {
115 constexpr std::array<DeviceType, 3> none{DeviceType::UNKNOWN, DeviceType::UNKNOWN, DeviceType::UNKNOWN};
116 switch (manufacturer) {
118 return {ONEWAY_EXECUTE_ACEI_VELUX, /*profile_is_a_guess=*/false, EnrollGesture::VELUX_KLI,
121 case 0x00:
122 return {ONEWAY_EXECUTE_ACEI, /*profile_is_a_guess=*/false, EnrollGesture::SOMFY, none};
123 default:
124 return {ONEWAY_EXECUTE_ACEI, /*profile_is_a_guess=*/true, EnrollGesture::SOMFY, none};
125 }
126}
127
128/// @brief The device classes this identity's `0x30` enrollment sweep will actually target.
129/// @param identity The controller identity.
130/// @return `enrollment_classes` when the identity overrode it (any entry non-`UNKNOWN`), else the
131/// manufacturer profile's list. `UNKNOWN` entries are skipped by the caller.
132inline std::array<DeviceType, 3> effective_enrollment_classes(const OneWayControllerIdentity &identity) {
133 const bool overridden = identity.enrollment_classes[0] != DeviceType::UNKNOWN ||
136 return overridden ? identity.enrollment_classes
138}
139
140/// @brief The ACEI byte a given identity will put on air for a 1W EXECUTE frame.
141/// @param identity The controller identity.
142/// @return `execute_acei` when overridden (non-zero), else the manufacturer's profile default.
143inline uint8_t effective_execute_acei(const OneWayControllerIdentity &identity) {
144 return identity.execute_acei != 0 ? identity.execute_acei
146}
147
148/// @brief Whether this identity's ACEI comes from an explicit `execute_acei:` rather than the profile.
149/// @param identity The controller identity.
150/// @return true when `execute_acei:` was set (non-zero) and overrides the manufacturer profile default.
151inline bool has_execute_acei_override(const OneWayControllerIdentity &identity) { return identity.execute_acei != 0; }
152
153// === Control surface ===
154
155/// Wire-scale position meaning "fully closed" (0 means fully open). Named here because the two
156/// values are what OPEN and CLOSE actually are — see encode_oneway_action().
157static constexpr uint8_t ONEWAY_POSITION_FULLY_OPEN = 0;
158static constexpr uint8_t ONEWAY_POSITION_FULLY_CLOSED = 100;
159
160/// @brief The command a generated 1W button sends.
161///
162/// The vocabulary of the `commands:` list on a `oneway_controllers:` entry. Kept separate from
163/// CoverCommand because two of these are not commands at all on the wire: OPEN and CLOSE are
164/// positions 0 and 100, and only look like named commands to a user.
165/// @ingroup hioc_protocol
166enum class OneWayButtonAction : uint8_t {
167 OPEN, ///< Position 0 (fully open).
168 CLOSE, ///< Position 100 (fully closed).
169 STOP, ///< CoverCommand::STOP.
170 VENT, ///< CoverCommand::VENT.
171 FAVORITE, ///< CoverCommand::FAVORITE.
172};
173
174/// @brief How a OneWayButtonAction reaches the wire.
175/// @ingroup hioc_protocol
177 bool is_position{false}; ///< True when the action is sent as a numeric position.
178 uint8_t position{0}; ///< Position to send when `is_position`.
179 CoverCommand command{CoverCommand::STOP}; ///< Named command to send otherwise.
180};
181
182/// @brief Resolve a button action to the call that sends it.
183///
184/// Pure, so the mapping can be tested without a radio, an entity or a hub. OPEN and CLOSE resolve
185/// to positions because that is what they are on the wire — there is no distinct open/close
186/// opcode, and treating them as named commands would need a second encoding path for no gain.
187/// @param action Button action to encode.
188/// @return The position-or-command the transmitter should send.
189/// @ingroup hioc_protocol
191 OneWayActionEncoding encoding{};
192 switch (action) {
194 encoding.is_position = true;
196 break;
198 encoding.is_position = true;
200 break;
202 encoding.command = CoverCommand::VENT;
203 break;
206 break;
208 default:
209 encoding.command = CoverCommand::STOP;
210 break;
211 }
212 return encoding;
213}
214
215/// @brief Human-readable name for a button action, as it appears in the diagnostic sensor.
216/// @param action Button action to name.
217/// @return Null-terminated name such as "OPEN".
218/// @ingroup hioc_protocol
220
221/// @brief The configured 1W controller identities, in YAML declaration order.
222///
223/// Lookup is by `id` and linear: a hub has a handful of identities, not hundreds, and keeping
224/// insertion order makes boot logging read the same as the YAML that produced it.
225/// @ingroup hioc_protocol
227 public:
228 /// @brief Add a configured identity. Called from generated code at setup.
229 /// @param identity Fully-resolved identity (key inherited or explicit, address configured or derived).
230 void add(const OneWayControllerIdentity &identity) { this->identities_.push_back(identity); }
231
232 /// @brief Look up an identity by its YAML handle.
233 /// @param id Handle to find.
234 /// @return Pointer to the identity, or nullptr if no such handle is configured.
235 [[nodiscard]] const OneWayControllerIdentity *get(const std::string &id) const {
236 for (const auto &identity : this->identities_) {
237 if (identity.id == id)
238 return &identity;
239 }
240 return nullptr;
241 }
242
243 /// @brief All configured identities, in declaration order.
244 [[nodiscard]] const std::vector<OneWayControllerIdentity> &all() const { return this->identities_; }
245
246 /// @brief Whether any identity is configured.
247 [[nodiscard]] bool empty() const { return this->identities_.empty(); }
248
249 private:
250 std::vector<OneWayControllerIdentity> identities_;
251};
252
253} // namespace home_io_control
254} // 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.
static constexpr uint8_t NODE_ID_SIZE
Device/node addresses are 3 bytes (e.g., "123ABC").
Definition proto_sizes.h:20
DeviceType
Device type identifiers reported by IO‑Homecontrol products.
@ UNKNOWN
Unknown/unspecified device.
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 MANUFACTURER_VELUX
IO-Homecontrol manufacturer ID constants.
static constexpr uint8_t ONEWAY_POSITION_FULLY_OPEN
Wire-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).
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 three device classes a real VELUX KLI PROG gesture sweeps its 0x30 across — roller shutter,...
static constexpr uint8_t ONEWAY_EXECUTE_ACEI_VELUX
ACEI byte for a 1W CMD_EXECUTE frame from a VELUX KLI-class remote.
static constexpr uint8_t AES_KEY_SIZE
AES-128 key size.
Definition proto_sizes.h:23
static constexpr uint8_t ONEWAY_EXECUTE_ACEI
ACEI byte for a 1W CMD_EXECUTE frame — the Somfy-shaped default.
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.
static constexpr uint8_t MANUFACTURER_SOMFY
Somfy (shutters, awnings, blinds).
bool has_execute_acei_override(const OneWayControllerIdentity &identity)
Whether this identity's ACEI comes from an explicit execute_acei: rather than the profile.
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.
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:).
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.
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:).
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.