Skip to content

Commit 867a84e

Browse files
committed
Merge pull request romasku#405 from RealByron/few_fixes
Some fixes and generalisations
2 parents 5cbfcaa + 234842e commit 867a84e

10 files changed

Lines changed: 148 additions & 40 deletions

File tree

docs/contribute/porting.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ The pinout is stored in the **device config string**.
9090
| **`B`** | Reset button | • Puts device in pairing |
9191
| **`L`** | Network led | • Blinks while pairing <br> • Is the backlight sometimes |
9292
| **`S`** | Switch | • User input <br> • Tactile/touch button or external switch <br> • Spam to put in pairing mode |
93-
| **`R`** | Relay / Triac | • Output <br> • Non-latching: `RC1` - 1 pin: on when high <br> • Latching: `RC2C3` - 2 pins: pulse on, pulse off |
93+
| **`R`** | Relay / Triac | • Output <br> • Non-latching: `RC1` - 1 pin: on when high <br> • Latching: `RC2C3` - 2 pins: pulse on, pulse off <br> • Add `i` to invert (active-low): `RC1i` |
9494
| **`X`** | Cover Switch | • User input for cover control <br> • Format: `XA2B3u` - 2 pins + pull resistor: open button, close button |
95-
| **`C`** | Cover | • Motor control for curtains/blinds/shades <br> • Format: `CA2B3` - 2 pins: open relay, close relay |
95+
| **`C`** | Cover | • Motor control for curtains/blinds/shades <br> • Format: `CA2B3` - 2 pins: open relay, close relay <br> • Add `i` to invert both relays (active-low): `CA2B3i` |
9696
| **`I`** | Indicator LED | • 1 per relay, follows state <br> • Briefly flashes on button press (binding confirmation) <br> • Blinks while pairing if there is no network led |
9797

9898
For buttons (`B`), switches (`S`), and cover switches (`X`), the next character chooses the internal pull-up/down resistor:
@@ -101,7 +101,7 @@ For buttons (`B`), switches (`S`), and cover switches (`X`), the next character
101101
Usually, pressing the button bridges the GPIO pin to Ground (active low).
102102
⤷ So we need a pull-up resistor `u`: to hold it at VCC (high) while not-pressed.
103103

104-
For LEDs, add `i` to invert the state.
104+
For LEDs (`L`, `I`), relays (`R`), and cover relays (`C`), add `i` to invert the output (active-low).
105105

106106
Additional options:
107107
| Format | Option | Function |

docs/updating.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ This page describes **converting** and **updating** supported devices **wireless
5757
8. **Start** the update (red download button)
5858
9. **Re-download** the custom [# Quirks / Converters / Extensions](#quirks--converters--extensions) and restart ZHA / Z2M
5959
10. **Interview** the device **`i`**
60-
⤷ option missing from ZHA, remove and re-pair if needed
61-
(updates endpoints, clusters and identifiers)
62-
11. **Reconfigure** the device **`🗘`**
63-
(resets reporting and stuff?, keeps user binds and settings)
64-
12. Re-do user settings if needed
60+
⤷ option missing from ZHA, remove and re-pair if needed
61+
(updates endpoints, clusters and identifiers)
62+
11. **Reconfigure** the device **`🗘`**
63+
(resets reporting intervals, keeps user binds and settings)
64+
12. Re-do user settings if needed
6565

6666
> *If your device is several versions behind, it will update directly to the latest version.*
6767
@@ -120,7 +120,7 @@ zha:
120120
> - e.g. 10W dumb bulb is safe
121121
> - estimated values: >4W for EndDevice, >8W for Router
122122
> - **not recommended: no-Neutral switch + smart bulb ( <1W when brightness=0 )**
123-
> ⤷ dummy load (capactior) may be required
123+
> ⤷ dummy load (capacitor) may be required
124124

125125
<details>
126126
<summary> <b> Index link format </b> </summary>

helper_scripts/templates/switch_custom.js.jinja

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ const romasku = {
3434
endpointName,
3535
lookup: { on_off: 0, off_on: 1, toggle_simple: 2, toggle_smart_sync: 3, toggle_smart_opposite: 4 },
3636
cluster: "genOnOffSwitchCfg",
37-
attribute: {ID: 0x0010, type: 0x30, required: true, write: true, min: 0, max: 4}, // Enum8
37+
attribute: {ID: 0x0010, type: Zcl.DataType.ENUM8, required: true, write: true, min: 0, max: 4},
3838
description: `Select how switch should work:
3939
- on_off: When switch physically moved to position 1 it always generates ON command, and when moved to position 2 it generates OFF command
4040
- off_on: Same as on_off, but positions are swapped
@@ -49,7 +49,7 @@ const romasku = {
4949
endpointName,
5050
lookup: { toggle: 0, momentary: 1, momentary_nc: 2 },
5151
cluster: "genOnOffSwitchCfg",
52-
attribute: { ID: 0xff00, type: 0x30 }, // Enum8
52+
attribute: { ID: 0xff00, type: Zcl.DataType.ENUM8 },
5353
description: "Select the type of switch connected to the device",
5454
entityCategory: "config",
5555
}),
@@ -59,7 +59,7 @@ const romasku = {
5959
endpointName,
6060
lookup: { detached: 0, press_start: 1, short_press: 3, long_press: 2},
6161
cluster: "genOnOffSwitchCfg",
62-
attribute: { ID: 0xff01, type: 0x30 }, // Enum8
62+
attribute: { ID: 0xff01, type: Zcl.DataType.ENUM8 },
6363
description: "When to turn on/off internal relay",
6464
entityCategory: "config",
6565
}),
@@ -71,7 +71,7 @@ const romasku = {
7171
Array.from({ length: relay_cnt || 2 }, (_, i) => [`relay_${i + 1}`, i + 1])
7272
),
7373
cluster: "genOnOffSwitchCfg",
74-
attribute: { ID: 0xff02, type: 0x20 }, // uint8
74+
attribute: { ID: 0xff02, type: Zcl.DataType.UINT8 },
7575
description: "Which internal relay it should trigger",
7676
entityCategory: "config",
7777
}),
@@ -81,7 +81,7 @@ const romasku = {
8181
endpointName,
8282
lookup: { press_start: 1, short_press: 3, long_press: 2},
8383
cluster: "genOnOffSwitchCfg",
84-
attribute: { ID: 0xff05, type: 0x30 }, // Enum8
84+
attribute: { ID: 0xff05, type: Zcl.DataType.ENUM8 },
8585
description: "When turn on/off binded device",
8686
entityCategory: "config",
8787
}),
@@ -90,7 +90,7 @@ const romasku = {
9090
name,
9191
endpointNames: [endpointName],
9292
cluster: "genOnOffSwitchCfg",
93-
attribute: { ID: 0xff03, type: 0x21 }, // uint16
93+
attribute: { ID: 0xff03, type: Zcl.DataType.UINT16 },
9494
description: "What duration is considerd to be long press",
9595
valueMin: 0,
9696
valueMax: 5000,
@@ -101,7 +101,7 @@ const romasku = {
101101
name,
102102
endpointNames: [endpointName],
103103
cluster: "genOnOffSwitchCfg",
104-
attribute: { ID: 0xff04, type: 0x20 }, // uint8
104+
attribute: { ID: 0xff04, type: Zcl.DataType.UINT8 },
105105
description: "Level (dim) move rate in steps per ms",
106106
valueMin: 1,
107107
valueMax: 255,
@@ -124,7 +124,7 @@ const romasku = {
124124
endpointName,
125125
lookup: { same: 0, opposite: 1, manual: 2 },
126126
cluster: "genOnOff",
127-
attribute: { ID: 0xff01, type: 0x30 }, // Enum8
127+
attribute: { ID: 0xff01, type: Zcl.DataType.ENUM8 },
128128
description: "Mode for the relay indicator LED",
129129
entityCategory: "config",
130130
}),
@@ -135,7 +135,7 @@ const romasku = {
135135
valueOn: ["ON", 1],
136136
valueOff: ["OFF", 0],
137137
cluster: "genOnOff",
138-
attribute: {ID: 0xff02, type: 0x10}, // Boolean
138+
attribute: {ID: 0xff02, type: Zcl.DataType.BOOLEAN},
139139
description: "State of the relay indicator LED",
140140
access: "ALL",
141141
entityCategory: "config",
@@ -170,17 +170,18 @@ const romasku = {
170170
valueOn: ["ON", 1],
171171
valueOff: ["OFF", 0],
172172
cluster: "genBasic",
173-
attribute: {ID: 0xff01, type: 0x10}, // Boolean
173+
attribute: {ID: 0xff01, type: Zcl.DataType.BOOLEAN},
174174
description: "State of the network indicator LED",
175175
access: "ALL",
176176
entityCategory: "config",
177177
}),
178+
178179
multiPressResetCount: (name, endpointName) =>
179180
numeric({
180181
name,
181182
endpointNames: [endpointName],
182183
cluster: "genBasic",
183-
attribute: { ID: 0xff02, type: 0x20 }, // uint8
184+
attribute: { ID: 0xff02, type: Zcl.DataType.UINT8 },
184185
description: "Number of consecutive presses to trigger factory reset (0 = disabled)",
185186
valueMin: 0,
186187
valueMax: 255,
@@ -192,7 +193,7 @@ const romasku = {
192193
endpointName,
193194
access: "ALL",
194195
cluster: "genBasic",
195-
attribute: { ID: 0xff00, type: 0x44 }, // long str
196+
attribute: { ID: 0xff00, type: Zcl.DataType.LONG_CHAR_STR },
196197
description: "Current configuration of the device",
197198
zigbeeCommandOptions: {timeout: 30_000},
198199
validate: (value) => {
@@ -441,7 +442,7 @@ const definitions = [
441442
romasku.relayIndicator("{{relayName}}_indicator", "{{relayName}}"),
442443
{% endfor %}
443444
{% for coverName in device.coverNames %}
444-
windowCovering({
445+
windowCovering({
445446
controls: ["lift"],
446447
coverInverted: true,
447448
configureReporting: false,
@@ -470,7 +471,7 @@ const definitions = [
470471
// switch action:
471472
await endpoint{{loop.index}}.configureReporting("genMultistateInput", [
472473
{
473-
attribute: {ID: 0x0055 /* presentValue */, type: 0x21}, // uint16
474+
attribute: {ID: 0x0055 /* presentValue */, type: Zcl.DataType.UINT16},
474475
minimumReportInterval: 0,
475476
maximumReportInterval: constants.repInterval.MAX,
476477
reportableChange: 1,
@@ -483,7 +484,7 @@ const definitions = [
483484
await reporting.bind(batteryEndpoint, coordinatorEndpoint, ["genPowerCfg"]);
484485
await batteryEndpoint.configureReporting("genPowerCfg", [
485486
{
486-
attribute: {ID: 0x0021, type: 0x20}, // BatteryPercentageRemaining
487+
attribute: {ID: 0x0021, type: Zcl.DataType.UINT8}, // BatteryPercentageRemaining
487488
minimumReportInterval: 0,
488489
maximumReportInterval: constants.repInterval.HOUR,
489490
reportableChange: 2, // 1% (2 in ZCL 0-200 format)
@@ -503,7 +504,7 @@ const definitions = [
503504
{% for relayName in device.relayIndicatorNames %}
504505
await endpoint{{loop.index + (device.switchNames | length)}}.configureReporting("genOnOff", [
505506
{
506-
attribute: {ID: 0xff02, type: 0x10}, // Boolean
507+
attribute: {ID: 0xff02, type: Zcl.DataType.BOOLEAN},
507508
minimumReportInterval: 0,
508509
maximumReportInterval: constants.repInterval.MAX,
509510
reportableChange: 1,

src/device_config/config_parser.c

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,14 @@ void parse_config() {
133133
hal_gpio_pin_t pin = hal_gpio_parse_pin(entry + 1);
134134
hal_gpio_pull_t pull = hal_gpio_parse_pull(entry + 3);
135135
hal_gpio_init(pin, 1, pull);
136+
bool pressed_when_high = (pull == HAL_GPIO_PULL_DOWN) ? 1 : 0;
136137

137138
buttons[buttons_cnt].pin = pin;
138139
buttons[buttons_cnt].long_press_duration_ms = 2000;
139140
buttons[buttons_cnt].multi_press_duration_ms = 800;
140141
buttons[buttons_cnt].debounce_delay_ms = debounce_ms;
141142
buttons[buttons_cnt].on_long_press = on_reset_clicked;
143+
buttons[buttons_cnt].pressed_when_high = pressed_when_high;
142144
buttons_cnt++;
143145
} else if (entry[0] == 'L') {
144146
hal_gpio_pin_t pin = hal_gpio_parse_pin(entry + 1);
@@ -188,15 +190,14 @@ void parse_config() {
188190
hal_gpio_pin_t pin = hal_gpio_parse_pin(entry + 1);
189191
hal_gpio_pull_t pull = hal_gpio_parse_pull(entry + 3);
190192
hal_gpio_init(pin, 1, pull);
193+
bool pressed_when_high = (pull == HAL_GPIO_PULL_DOWN) ? 1 : 0;
191194

192195
buttons[buttons_cnt].pin = pin;
193-
buttons[buttons_cnt].long_press_duration_ms = 800;
194-
buttons[buttons_cnt].multi_press_duration_ms = 800;
195-
buttons[buttons_cnt].debounce_delay_ms = debounce_ms;
196-
buttons[buttons_cnt].on_multi_press = on_multi_press_reset;
197-
198-
if (entry[3] == 'd')
199-
buttons[buttons_cnt].pressed_when_high = 1;
196+
buttons[buttons_cnt].long_press_duration_ms = 800;
197+
buttons[buttons_cnt].multi_press_duration_ms = 800;
198+
buttons[buttons_cnt].debounce_delay_ms = debounce_ms;
199+
buttons[buttons_cnt].on_multi_press = on_multi_press_reset;
200+
buttons[buttons_cnt].pressed_when_high = pressed_when_high;
200201
switch_clusters[switch_clusters_cnt].switch_idx = switch_clusters_cnt;
201202
switch_clusters[switch_clusters_cnt].mode =
202203
ZCL_ONOFF_CONFIGURATION_SWITCH_TYPE_TOGGLE;
@@ -212,13 +213,14 @@ void parse_config() {
212213
buttons_cnt++;
213214
switch_clusters_cnt++;
214215
} else if (entry[0] == 'R') {
215-
hal_gpio_pin_t pin = hal_gpio_parse_pin(entry + 1);
216+
hal_gpio_pin_t pin = hal_gpio_parse_pin(entry + 1);
217+
bool on_high = entry[3] != 'i';
216218
hal_gpio_init(pin, 0, HAL_GPIO_PULL_NONE);
217219

218220
relays[relays_cnt].pin = pin;
219-
relays[relays_cnt].on_high = 1;
221+
relays[relays_cnt].on_high = on_high;
220222

221-
if (entry[3] != '\0') {
223+
if (entry[3] != '\0' && entry[3] != 'i') {
222224
pin = hal_gpio_parse_pin(entry + 3);
223225
hal_gpio_init(pin, 0, HAL_GPIO_PULL_NONE);
224226
relays[relays_cnt].off_pin = pin;
@@ -231,9 +233,10 @@ void parse_config() {
231233
relays_cnt++;
232234
relay_clusters_cnt++;
233235
} else if (entry[0] == 'X') {
234-
hal_gpio_pin_t open_pin = hal_gpio_parse_pin(entry + 1);
235-
hal_gpio_pin_t close_pin = hal_gpio_parse_pin(entry + 3);
236-
hal_gpio_pull_t pull = hal_gpio_parse_pull(entry + 5);
236+
hal_gpio_pin_t open_pin = hal_gpio_parse_pin(entry + 1);
237+
hal_gpio_pin_t close_pin = hal_gpio_parse_pin(entry + 3);
238+
hal_gpio_pull_t pull = hal_gpio_parse_pull(entry + 5);
239+
bool pressed_when_high = (pull == HAL_GPIO_PULL_DOWN) ? 1 : 0;
237240

238241
hal_gpio_init(open_pin, 1, pull);
239242
hal_gpio_init(close_pin, 1, pull);
@@ -243,13 +246,15 @@ void parse_config() {
243246
buttons[buttons_cnt].multi_press_duration_ms = 800;
244247
buttons[buttons_cnt].debounce_delay_ms = debounce_ms;
245248
buttons[buttons_cnt].on_multi_press = on_multi_press_reset;
249+
buttons[buttons_cnt].pressed_when_high = pressed_when_high;
246250
button_t *open_button = &buttons[buttons_cnt++];
247251

248252
buttons[buttons_cnt].pin = close_pin;
249253
buttons[buttons_cnt].long_press_duration_ms = 800;
250254
buttons[buttons_cnt].multi_press_duration_ms = 800;
251255
buttons[buttons_cnt].debounce_delay_ms = debounce_ms;
252256
buttons[buttons_cnt].on_multi_press = on_multi_press_reset;
257+
buttons[buttons_cnt].pressed_when_high = pressed_when_high;
253258
button_t *close_button = &buttons[buttons_cnt++];
254259

255260
cover_switch_clusters[cover_switch_clusters_cnt].open_button =
@@ -262,17 +267,18 @@ void parse_config() {
262267
} else if (entry[0] == 'C') {
263268
hal_gpio_pin_t open_pin = hal_gpio_parse_pin(entry + 1);
264269
hal_gpio_pin_t close_pin = hal_gpio_parse_pin(entry + 3);
270+
bool on_high = entry[5] != 'i';
265271

266272
hal_gpio_init(open_pin, 0, HAL_GPIO_PULL_NONE);
267273
hal_gpio_init(close_pin, 0, HAL_GPIO_PULL_NONE);
268274

269275
relays[relays_cnt].pin = open_pin;
270-
relays[relays_cnt].on_high = 1;
276+
relays[relays_cnt].on_high = on_high;
271277
relays[relays_cnt].is_latching = 0;
272278
relay_t *open_relay = &relays[relays_cnt++];
273279

274280
relays[relays_cnt].pin = close_pin;
275-
relays[relays_cnt].on_high = 1;
281+
relays[relays_cnt].on_high = on_high;
276282
relays[relays_cnt].is_latching = 0;
277283
relay_t *close_relay = &relays[relays_cnt++];
278284

src/zigbee/cover_cluster.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ void cover_cluster_on_write_attr(zigbee_cover_cluster *cluster, uint16_t attribu
164164
}
165165

166166
void cover_cluster_callback_attr_write_trampoline(uint8_t endpoint, uint16_t attribute_id) {
167+
if (cover_cluster_by_endpoint[endpoint] == NULL) {
168+
return;
169+
}
167170
cover_cluster_on_write_attr(cover_cluster_by_endpoint[endpoint], attribute_id);
168171
}
169172

src/zigbee/cover_switch_cluster.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,9 @@ void cover_switch_cluster_on_write_attr(zigbee_cover_switch_cluster *cluster,
324324

325325
void cover_switch_cluster_callback_attr_write_trampoline(uint8_t endpoint,
326326
uint16_t attribute_id) {
327+
if (cover_switch_cluster_by_endpoint[endpoint] == NULL) {
328+
return;
329+
}
327330
cover_switch_cluster_on_write_attr(cover_switch_cluster_by_endpoint[endpoint],
328331
attribute_id);
329332
}

src/zigbee/switch_cluster.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ void switch_cluster_report_action(zigbee_switch_cluster *cluster);
8282

8383
void switch_cluster_callback_attr_write_trampoline(uint8_t endpoint,
8484
uint16_t attribute_id) {
85+
if (switch_cluster_by_endpoint[endpoint] == NULL) {
86+
return;
87+
}
8588
switch_cluster_on_write_attr(switch_cluster_by_endpoint[endpoint],
8689
attribute_id);
8790
}

tests/test_active_low_cover.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Tests for active-low cover relay support via the 'i' inversion flag (CA0A1i config)."""
2+
import pytest
3+
4+
from client import StubProc
5+
from conftest import Device, MINIMUM_SWITCH_TIME_MS
6+
7+
8+
@pytest.fixture
9+
def cover_device_inverted():
10+
p = StubProc(device_config="X;Y;CA0A1i;").start()
11+
try:
12+
d = Device(p)
13+
d.step_time(MINIMUM_SWITCH_TIME_MS)
14+
yield d
15+
finally:
16+
p.stop()
17+
18+
19+
def test_active_low_cover_open_drives_open_relay_low(cover_device_inverted: Device):
20+
cover_device_inverted.zcl_cover_open(1)
21+
assert not cover_device_inverted.get_gpio("A0", refresh=True)
22+
assert cover_device_inverted.get_gpio("A1", refresh=True)
23+
24+
25+
def test_active_low_cover_close_drives_close_relay_low(cover_device_inverted: Device):
26+
cover_device_inverted.zcl_cover_close(1)
27+
assert cover_device_inverted.get_gpio("A0", refresh=True)
28+
assert not cover_device_inverted.get_gpio("A1", refresh=True)

tests/test_active_low_relay.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Tests for active-low relay support via the 'i' inversion flag (RB0i config)."""
2+
import pytest
3+
4+
from client import StubProc
5+
from conftest import Device
6+
7+
8+
@pytest.fixture
9+
def relay_inverted_device():
10+
p = StubProc(device_config="Mfg;Model;SA0u;RB0i;").start()
11+
try:
12+
yield Device(p)
13+
finally:
14+
p.stop()
15+
16+
17+
def test_active_low_relay_on_gpio_is_low(relay_inverted_device: Device):
18+
relay_inverted_device.zcl_relay_on(2)
19+
assert not relay_inverted_device.get_gpio("B0", refresh=True)
20+
21+
22+
def test_active_low_relay_off_gpio_is_high(relay_inverted_device: Device):
23+
relay_inverted_device.zcl_relay_on(2)
24+
relay_inverted_device.zcl_relay_off(2)
25+
assert relay_inverted_device.get_gpio("B0", refresh=True)

0 commit comments

Comments
 (0)