1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
use super::{cluster, ffi};
use huesmith_core::hue::device::{HueDeviceType, PROFILE_HA};
use huesmith_core::hue::identity::DeviceIdentity;
/// Builds and registers a Zigbee endpoint with all clusters for a Hue-compatible light.
pub struct EndpointBuilder {
endpoint_id: u8,
profile_id: u16,
device_type: HueDeviceType,
identity: &'static DeviceIdentity,
initial_on: bool,
initial_level: u8,
}
impl EndpointBuilder {
pub fn new(
endpoint_id: u8,
device_type: HueDeviceType,
identity: &'static DeviceIdentity,
) -> Self {
Self {
endpoint_id,
profile_id: PROFILE_HA,
device_type,
identity,
initial_on: false,
initial_level: 254,
}
}
pub fn profile_id(mut self, id: u16) -> Self {
self.profile_id = id;
self
}
pub fn initial_on(mut self, on: bool) -> Self {
self.initial_on = on;
self
}
pub fn initial_level(mut self, level: u8) -> Self {
self.initial_level = level;
self
}
pub fn register(self, ep_list: *mut ffi::esp_zb_ep_list_t) {
let cluster_list = unsafe { ffi::esp_zb_zcl_cluster_list_create() };
let basic = cluster::create_basic_cluster(self.identity);
unsafe {
ffi::esp_zb_cluster_list_add_basic_cluster(
cluster_list,
basic,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
let identify = cluster::create_identify_cluster();
unsafe {
ffi::esp_zb_cluster_list_add_identify_cluster(
cluster_list,
identify,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
let groups = cluster::create_groups_cluster();
unsafe {
ffi::esp_zb_cluster_list_add_groups_cluster(
cluster_list,
groups,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
let scenes = cluster::create_scenes_cluster();
unsafe {
ffi::esp_zb_cluster_list_add_scenes_cluster(
cluster_list,
scenes,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
let on_off = cluster::create_on_off_cluster(self.initial_on);
unsafe {
ffi::esp_zb_cluster_list_add_on_off_cluster(
cluster_list,
on_off,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
if self.device_type.has_level_control() {
let level = cluster::create_level_cluster(self.initial_level);
unsafe {
ffi::esp_zb_cluster_list_add_level_cluster(
cluster_list,
level,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
}
if self.device_type.has_color_control() {
let (caps, min_m, max_m) = self.device_type.color_params();
let color = cluster::create_color_control_cluster(caps, min_m, max_m);
unsafe {
ffi::esp_zb_cluster_list_add_color_control_cluster(
cluster_list,
color,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
}
// ZLL Commissioning (0x1000) — server with no attributes.
// Real Hue bulbs include this in their Simple Descriptor for Touchlink
// factory-reset compatibility. No commands are handled; the cluster
// exists only for Bridge discovery.
let zll = unsafe { ffi::esp_zb_touchlink_commissioning_cluster_create(core::ptr::null()) };
unsafe {
ffi::esp_zb_cluster_list_add_touchlink_commissioning_cluster(
cluster_list,
zll,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
// OTA Upgrade (0x0019) — client (out-cluster).
// Advertising this cluster causes the Bridge to show firmware update
// status and send OTA Image Notify commands. Actual OTA download is
// not implemented; the cluster is present for Bridge compatibility only.
//
// NOTE: do NOT present as a genuine Hue model (Signify OUI + real model
// id) with this cluster. The Bridge will then immediately push a real
// firmware update, and ZBOSS's OTA client handler asserts
// (zcl_ota_upgrade_commands.c) because there is no OTA backend, crashing
// the device right after it joins. With a generic identity the Bridge
// does not push firmware, so this stays a harmless visibility-only cluster.
let ota_cfg = ffi::esp_zb_ota_cluster_cfg_t {
ota_upgrade_file_version: 0x0000_0001,
ota_upgrade_manufacturer: ffi::MANUF_CODE_SIGNIFY,
ota_upgrade_image_type: 0xFFFF, // match-any
ota_min_block_reque: 0,
ota_upgrade_file_offset: 0,
ota_upgrade_downloaded_file_ver: 0,
ota_upgrade_server_id: [0xFF; 8], // broadcast = no server known yet
ota_image_upgrade_status: 0, // Normal
};
let ota = unsafe { ffi::esp_zb_ota_cluster_create(&ota_cfg) };
unsafe {
ffi::esp_zb_cluster_list_add_ota_cluster(
cluster_list,
ota,
ffi::ZB_ZCL_CLUSTER_CLIENT_ROLE,
);
}
// Signify 0xFC01 — empty manufacturer-specific server cluster.
// The Hue Bridge sends an init command (cmd 0x03) to this cluster during
// device discovery. Advertising it prevents "unexpected cluster" errors.
// Commands to it receive ZCL Default Response "unsupported command",
// which the Bridge handles gracefully.
let fc01 = unsafe { ffi::esp_zb_zcl_attr_list_create(0xFC01) };
unsafe {
ffi::esp_zb_cluster_list_add_custom_cluster(
cluster_list,
fc01,
ffi::ZB_ZCL_CLUSTER_SERVER_ROLE,
);
}
let ep_cfg = ffi::esp_zb_endpoint_config_t {
endpoint: self.endpoint_id,
app_profile_id: self.profile_id,
app_device_id: self.device_type.device_id(),
app_device_version: 1,
};
let ret = unsafe { ffi::esp_zb_ep_list_add_ep(ep_list, cluster_list, ep_cfg) };
log::info!(
"Registered endpoint {} as {:?} (profile 0x{:04X}, device 0x{:04X}) ret={}",
self.endpoint_id,
self.device_type,
self.profile_id,
self.device_type.device_id(),
ret,
);
}
}