11#include "esphome/core/application.h"
12#include "esphome/core/hal.h"
13#include "esphome/core/log.h"
20constexpr const char *
const TAG =
"home_io_control.oneway_tx";
30 for (uint8_t copy = 0; copy < ONEWAY_BURST_REPEATS; copy++) {
40 for (
const auto &identity : this->identities_.all())
41 this->sequences_.add_identity(identity.node_id, identity.initial_sequence);
44bool OneWayTransmitter::send_(
const std::string &controller_id,
46 const char *explicit_intent) {
48 if (identity ==
nullptr) {
49 ESP_LOGW(
TAG,
"1W tx: no controller identity '%s'", controller_id.c_str());
50 this->report_failure_(controller_id, 0,
false);
55 uint16_t sequence = 0;
56 if (!this->sequences_.
next(identity->
node_id, sequence)) {
58 this->report_failure_(controller_id, 0,
false);
63 if (!build(frame, *identity, sequence)) {
66 ESP_LOGW(TAG,
"1W tx: could not build a frame for '%s'", controller_id.c_str());
67 this->report_failure_(controller_id, sequence,
true);
71 const bool transmitted =
78 std::string intent = explicit_intent;
88 this->report_attempt_(controller_id, intent, identity->
io_device_type, sequence,
true,
93void OneWayTransmitter::report_attempt_(
const std::string &controller_id,
const std::string &intent,
94 DeviceType target_type, uint16_t sequence,
bool sequence_reserved,
98 OneWayCommandReport report{};
99 report.controller_id = controller_id;
100 report.intent = intent;
101 report.target_type = target_type;
102 report.sequence = sequence;
103 report.sequence_reserved = sequence_reserved;
104 report.transmitted = transmitted;
105 this->report_(report);
108void OneWayTransmitter::report_failure_(
const std::string &controller_id, uint16_t sequence,
bool sequence_reserved) {
109 this->report_attempt_(controller_id,
"",
DeviceType::UNKNOWN, sequence, sequence_reserved,
false);
125 char intent[ONEWAY_INTENT_BUFFER_SIZE]{};
126 if (position == ONEWAY_POSITION_FULLY_OPEN) {
127 snprintf(intent,
sizeof(intent),
"OPEN");
128 }
else if (position == ONEWAY_POSITION_FULLY_CLOSED) {
129 snprintf(intent,
sizeof(intent),
"CLOSE");
131 snprintf(intent,
sizeof(intent),
"position %u%%",
static_cast<unsigned>(position));
137 const bool mirror =
effective_inverted(identity) && position <= ONEWAY_POSITION_FULLY_CLOSED;
138 const uint8_t wire_position = mirror ?
static_cast<uint8_t
>(ONEWAY_POSITION_FULLY_CLOSED - position) : position;
148 if (identity ==
nullptr) {
149 ESP_LOGW(TAG,
"1W tx: no controller identity '%s'", controller_id.c_str());
150 this->report_failure_(controller_id, 0,
false);
154 return this->send_velux_kli_enrollment_(*identity);
155 return this->send_somfy_enrollment_(*identity);
172 const bool removed = this->send_(
175 return create_1w_remove_controller(frame, id.node_id, DeviceType::UNKNOWN, sequence, id.system_key);
179 ESP_LOGW(
TAG,
"1W tx: enrollment's 0x39 prelude did not reach the radio for '%s' -- trying 0x30 anyway",
180 identity.
id.c_str());
185 [](IoFrame &frame,
const OneWayControllerIdentity &
id, uint16_t sequence) {
186 return create_1w_add_controller(frame, id.node_id, DeviceType::UNKNOWN, id.manufacturer, sequence,
187 id.system_key, id.enrollment_with_mac);
192bool OneWayTransmitter::send_velux_kli_enrollment_(
const OneWayControllerIdentity &identity) {
199 const bool removed = this->send_(
201 [](IoFrame &frame,
const OneWayControllerIdentity &
id, uint16_t sequence) {
202 return create_1w_remove_controller(frame, id.node_id, DeviceType::UNKNOWN, sequence, id.system_key);
206 ESP_LOGW(TAG,
"1W tx: VELUX enrollment's 0x39 prelude did not reach the radio for '%s' -- continuing",
207 identity.id.c_str());
214 ESP_LOGW(TAG,
"1W tx: VELUX 0x30 sweep transmitted nothing for '%s' -- skipping the STOP+DOWN follow-up",
215 identity.id.c_str());
224 const bool stopped = this->send_(
226 [acei](IoFrame &frame,
const OneWayControllerIdentity &
id, uint16_t sequence) {
227 return create_1w_execute_command(frame, id.node_id, id.io_device_type, CoverCommand::STOP, sequence,
228 id.system_key, acei, true);
231 const bool lowered = this->send_(
233 [acei](IoFrame &frame,
const OneWayControllerIdentity &
id, uint16_t sequence) {
236 return create_1w_execute_position(frame, id.node_id, id.io_device_type, ONEWAY_POSITION_FULLY_CLOSED, sequence,
237 id.system_key, acei, true);
240 if (!stopped || !lowered) {
241 ESP_LOGW(TAG,
"1W tx: VELUX enrollment's STOP+DOWN follow-up did not fully reach the radio for '%s'",
242 identity.id.c_str());
248 const std::array<DeviceType, 3> &classes) {
251 uint16_t sequence = 0;
252 if (!this->sequences_.next(identity.node_id, sequence)) {
253 this->report_failure_(identity.id, 0,
false);
257 bool any_transmitted =
false;
258 for (
const DeviceType target_type : classes) {
263 identity.system_key, identity.enrollment_with_mac)) {
264 ESP_LOGW(TAG,
"1W tx: could not build a 0x30 for class 0x%02X on '%s'",
static_cast<unsigned>(target_type),
265 identity.id.c_str());
269 any_transmitted =
true;
272 this->report_attempt_(identity.id,
"ENROLL", identity.io_device_type, sequence,
true,
274 return any_transmitted;
303 if (info.
has_intent || intent_label !=
nullptr) {
304 ESP_LOGI(TAG,
"1W tx: from %s to %s intent %s (%ux, preamble %s)",
node_id_to_string(frame.
src).c_str(), target,
305 intent_label !=
nullptr ? intent_label : info.
intent, ONEWAY_BURST_REPEATS, preambles.c_str());
307 ESP_LOGI(TAG,
"1W tx: from %s to %s cmd 0x%02X (%ux, preamble %s)",
node_id_to_string(frame.
src).c_str(), target,
308 frame.
cmd, ONEWAY_BURST_REPEATS, preambles.c_str());
312 for (uint8_t copy = 0; copy < ONEWAY_BURST_REPEATS; copy++) {
317 delay(ONEWAY_BURST_INTERVAL_MS);
326 copy_frame.
ctrl1 |= CTRL1_LOW_POWER;
328 copy_frame.
ctrl1 &=
static_cast<uint8_t
>(~CTRL1_LOW_POWER);
331 if (this->transmit_(copy_frame, FREQ_CH2, preamble)) {
336 if (sent != ONEWAY_BURST_REPEATS) {
339 ESP_LOGW(TAG,
"1W tx: only %u of %u copies transmitted", sent, ONEWAY_BURST_REPEATS);
const OneWayControllerIdentity * get(const std::string &id) const
Look up an identity by its YAML handle.
bool next(const uint8_t node_id[NODE_ID_SIZE], uint16_t &out)
Reserve and return the next sequence for an identity.
bool send_position(const std::string &controller_id, uint8_t position)
Send a numeric position as the identity's controller.
void setup()
Open each registered identity's persistent sequence counter.
bool send_burst(const IoFrame &frame, OneWayPowerClass power_class, const char *intent_label=nullptr)
Transmit one already-built, already-signed 1W frame as a burst.
bool send_unenrollment(const std::string &controller_id)
Un-register this identity from every device of its class currently in association mode (CMD 0x39) alo...
bool send_enrollment(const std::string &controller_id)
Register this identity as a controller on every device currently in association mode (the receiver's ...
bool send_command(const std::string &controller_id, CoverCommand cmd)
Send a named command as the identity's controller.
uint16_t oneway_copy_preamble_bytes(OneWayCopyShape shape, uint16_t normal_start_preamble)
Resolve one copy's shape (oneway_burst_copy_shape()) to the actual preamble byte count.
std::string format_oneway_preamble_list(OneWayPowerClass power_class, uint16_t normal_start_preamble)
Render the preamble bytes each copy of a burst will use, e.g.
@ WAKE
The long wake-up preamble (LONG_PREAMBLE), paired with CTRL1_LOW_POWER set.
static constexpr const char * TAG
CoverCommand
Named device commands for cover-type actuators.
bool create_1w_remove_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE])
Build a 1W remove-controller frame (CMD 0x39).
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.
const char * oneway_target_label(const OneWayFrameInfo &info)
The destination label a decoded 1W frame renders as in a log line.
DeviceType
IO-Homecontrol node types.
@ UNKNOWN
Unknown/unspecified device.
bool create_1w_execute_command(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, CoverCommand cmd, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t acei, bool broadcast_all)
Build a 1W named-command execute frame (CMD 0x00) targeting a device class.
OneWayFrameInfo decode_1w_frame(const IoFrame &frame)
Decode a parsed 1W frame into a structured OneWayFrameInfo.
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.
OneWayPowerClass
Which preamble/CTRL1 shape a 1W identity's bursts actually go out with.
std::string node_id_to_string(const uint8_t id[NODE_ID_SIZE])
Format a 3‑byte node ID as a 6‑character uppercase hex string.
OneWayWireProfile resolve_oneway_wire_profile(uint8_t manufacturer)
Resolve an identity's 1W wire profile from its manufacturer byte.
bool create_1w_execute_position(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t position, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], uint8_t acei, bool broadcast_all)
Build a 1W position execute frame (CMD 0x00) targeting a device class.
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 ...
bool create_1w_add_controller(IoFrame &f, const uint8_t src[NODE_ID_SIZE], DeviceType target_type, uint8_t manufacturer, uint16_t sequence, const uint8_t controller_key[AES_KEY_SIZE], bool with_mac)
Build a 1W add-controller frame (CMD 0x30).
static const char *const TAG
One-way (1W) transmit collaborator.
Device-name, address-classification and 1W-frame codecs.
Command builders for the IO‑Homecontrol protocol.
Physical-layer radio and timing parameters for the IO-Homecontrol protocol.
Parsed IO‑Homecontrol frame (CTRL0/1 + addresses + command + data).
uint8_t src[NODE_ID_SIZE]
Source node ID (3 bytes).
uint8_t ctrl1
Control byte 1: low power, beacon, etc.
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...
uint8_t system_key[AES_KEY_SIZE]
Network key for this identity; may differ per identity.
DeviceType io_device_type
Device class this identity commands.
uint8_t manufacturer
Manufacturer ID; unused until the enrollment phase.
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.
Decoded representation of a 1W remote frame.
bool has_intent
True if originator/ACEI/intent fields were decoded.
char intent[ONEWAY_INTENT_BUFFER_SIZE]
Human-readable command intent (e.g., "CLOSE").
EnrollGesture enroll_gesture
Which enrollment gesture this manufacturer's actuators expect.