mirror of
https://github.com/arendst/Tasmota.git
synced 2026-10-26 22:11:22 +00:00
DALI-2 push button bindings to control gear targets (DaliBind) (#25073)
* DALI: bind DALI-2 push button events to control gear targets Add command DaliBind<1..8> <device>,<instance>,<target>,<mode> to link push button events (IEC 62386-301) from an input device instance to a control gear target. Modes are Toggle, On, Off, Up and Down; Up and Down start a fade on button press and stop it on release. The first matching binding is executed. DaliBind<x> 0 removes a binding and DaliBind alone lists them all. Bindings are stored in the DALI settings file, packed as one uint32 per slot, so they survive a restart. * Describe how start-up brightness stored differently per short addres vs dali groups
This commit is contained in:
1 parent
cadd9a5612
commit
b0c8e4f53f
3 files changed
+256
-17
No files matched your search
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
## [15.6.0.2]
|
||||
### Added
|
||||
- DALI-2 push button bindings to control gear targets with command `DaliBind` (toggle, on, off, up, down)
|
||||
|
||||
|
||||
### Breaking Changed
|
||||
|
||||
@@ -114,6 +114,7 @@ The latter links can be used for OTA upgrades too like ``OtaUrl https://ota.tasm
|
||||
|
||||
## Changelog v15.6.0.2
|
||||
### Added
|
||||
- DALI-2 push button bindings to control gear targets with command `DaliBind` (toggle, on, off, up, down)
|
||||
- Support for GUI tooltip on touch media like phones and tablets
|
||||
- Support for TFA Dostmann Marbella 868MHz pool thermometer using a CC1101 [#24959](https://github.com/arendst/Tasmota/issues/24959)
|
||||
- Support for MI32 Xiaomi Mi Body Composition Scale (MIBCS/MIBFS) [#25049](https://github.com/arendst/Tasmota/issues/25049)
|
||||
|
||||
@@ -51,6 +51,24 @@
|
||||
* DaliDevice <device> - Report DALI-2 control device version, status and instances (type, enabled, event scheme)
|
||||
* DaliDevice <broadcast>|<device>,<scheme> - Set event scheme of all instances of a control device
|
||||
* (0 = Instance, 1 = Device, 2 = DeviceInstance, 3 = DeviceGroup, 4 = InstanceGroup)
|
||||
* DaliBind<1..8> <device>,<instance>,<target>,<mode> - Bind a DALI-2 push button instance to a control gear target
|
||||
* <device> 0 = any control device, 1..64 = control device short address +1
|
||||
* <instance> 0..31 = instance number, 255 = any instance
|
||||
* <target> 0 = broadcast, 1..64 = control gear short address +1, 101..116 = group
|
||||
* <mode> 1 = Toggle, 2 = On, 3 = Off, 4 = Up, 5 = Down (see below)
|
||||
* DaliBind<1..8> 0 - Remove binding
|
||||
* DaliBind - Show all bindings
|
||||
*
|
||||
* Binding behaviour for push button events (IEC 62386-301), the first matching binding is executed:
|
||||
* 1 Toggle - short press toggles the target, long press dims alternating up and down, double press full brightness
|
||||
* 2 On - short press switches on to the last level, long press dims up, double press full brightness
|
||||
* 3 Off - short press switches off, long press dims down
|
||||
* 4 Up - short press or rotation dims up, double press full brightness (rotary clockwise)
|
||||
* 5 Down - short press or rotation dims down (rotary anticlockwise)
|
||||
* The last level is only stored for broadcast and group targets. Switching on a short address target restores the
|
||||
* broadcast level as the driver keeps no per short address state, so use a group target to retain a channel's own level.
|
||||
* Example zencontrol zc-switch rotary at short address 0 controlling broadcast:
|
||||
* Backlog DaliBind1 1,0,0,1; DaliBind2 1,1,0,4; DaliBind3 1,2,0,5
|
||||
*
|
||||
* DALI-2 input devices (push buttons, occupancy and light sensors) send 24-bit event messages which are decoded and
|
||||
* published for rules and MQTT as {"DALI":{"Event":"0x82840B","Scheme":"Instance","Type":1,"Instance":1,"Info":11,"Name":"LongPressRepeat"}}
|
||||
@@ -166,6 +184,10 @@
|
||||
|
||||
#define DALI_MAX_STORED 17 // Store broadcast and group states
|
||||
|
||||
#ifndef DALI_MAX_BIND
|
||||
#define DALI_MAX_BIND 8 // Number of input device to control gear bindings (max 32)
|
||||
#endif
|
||||
|
||||
#define DALI_TOPIC "DALI"
|
||||
#define D_PRFX_DALI "Dali"
|
||||
|
||||
@@ -218,18 +240,27 @@
|
||||
|
||||
/*********************************************************************************************/
|
||||
|
||||
typedef struct DliBind_t {
|
||||
uint8_t device; // 0 = any control device, 1..64 = short address +1
|
||||
uint8_t instance; // 0..31 = instance number, 255 = any instance
|
||||
uint8_t target; // 0 = broadcast, 1..64 = gear short address +1, 101..116 = group
|
||||
uint8_t mode; // 0 = unused, 1 = Toggle, 2 = On, 3 = Off, 4 = Up, 5 = Down
|
||||
} DliBind_t;
|
||||
|
||||
typedef struct DliSettings_t {
|
||||
uint32_t crc32; // To detect file changes
|
||||
uint8_t target;
|
||||
uint8_t light_type;
|
||||
uint8_t max_gear;
|
||||
uint8_t probe_retry;
|
||||
DliBind_t bind[DALI_MAX_BIND];
|
||||
} DliSettings_t;
|
||||
|
||||
struct DALI {
|
||||
DliSettings_t Settings; // Persistent settings
|
||||
TasmotaDali *dali;
|
||||
uint32_t light_sync;
|
||||
uint32_t bind_down; // Current dim direction per binding (bit set = down)
|
||||
uint8_t address;
|
||||
uint8_t command;
|
||||
uint8_t last_dimmer;
|
||||
@@ -270,6 +301,18 @@ bool DaliLoadData(void) {
|
||||
Dali->Settings.light_type = root.getUInt(PSTR("LightType"), Dali->Settings.light_type);
|
||||
Dali->Settings.max_gear = root.getUInt(PSTR("MaxGear"), Dali->Settings.max_gear);
|
||||
Dali->Settings.probe_retry = root.getUInt(PSTR("Retry"), Dali->Settings.probe_retry);
|
||||
JsonParserArray arr = root[PSTR("Bind")];
|
||||
if (arr) {
|
||||
for (uint32_t i = 0; i < DALI_MAX_BIND; i++) {
|
||||
if (arr[i]) {
|
||||
uint32_t bind = arr[i].getUInt(); // Packed as mode << 24 | target << 16 | instance << 8 | device
|
||||
Dali->Settings.bind[i].device = bind;
|
||||
Dali->Settings.bind[i].instance = bind >> 8;
|
||||
Dali->Settings.bind[i].target = bind >> 16;
|
||||
Dali->Settings.bind[i].mode = bind >> 24;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -281,6 +324,17 @@ bool DaliSaveData(void) {
|
||||
Dali->Settings.light_type,
|
||||
Dali->Settings.max_gear,
|
||||
Dali->Settings.probe_retry);
|
||||
Response_P(PSTR("{\"" XDRV_75_KEY "\":{\"Crc\":%u,\"Target\":%u,\"LightType\":%u,\"MaxGear\":%u,\"Bind\":["),
|
||||
Dali->Settings.crc32,
|
||||
Dali->Settings.target,
|
||||
Dali->Settings.light_type,
|
||||
Dali->Settings.max_gear);
|
||||
for (uint32_t i = 0; i < DALI_MAX_BIND; i++) {
|
||||
DliBind_t *bind = &Dali->Settings.bind[i];
|
||||
ResponseAppend_P(PSTR("%s%u"), (i) ? "," : "",
|
||||
(bind->mode << 24) | (bind->target << 16) | (bind->instance << 8) | bind->device);
|
||||
}
|
||||
ResponseAppend_P(PSTR("]}}"));
|
||||
|
||||
return UfsJsonSettingsWrite(ResponseData());
|
||||
}
|
||||
@@ -883,6 +937,138 @@ bool DaliLoopSync(uint32_t channels) {
|
||||
|
||||
/*- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -*/
|
||||
|
||||
/*********************************************************************************************\
|
||||
* DALI control gear power control shared by command DaliPower and input device bindings
|
||||
\*********************************************************************************************/
|
||||
|
||||
uint32_t DaliSetPower(uint32_t target, uint32_t payload) {
|
||||
// target 0 = broadcast, 1..64 = short address, 101..116 = group
|
||||
// payload 0 = off, 1 = on to last dimmer state, 2 = toggle, 3..254 = dim level
|
||||
// returns state index (0 = broadcast or short address, 1..16 = group)
|
||||
uint32_t index = 0; // Broadcast
|
||||
if ((target >= 101) && (target <= 116)) {
|
||||
index = target - 100; // Group1 to 16
|
||||
}
|
||||
if (payload <= 2) {
|
||||
if (2 == payload) {
|
||||
payload = (Dali->power[index]) ? 0 : 1;
|
||||
}
|
||||
if (1 == payload) {
|
||||
payload = Dali->dimmer[index];
|
||||
}
|
||||
}
|
||||
uint32_t adr = DaliTarget2Address(target);
|
||||
#ifdef DALI_POWER_OFF_NO_FADE
|
||||
if (!payload) {
|
||||
DaliSendData(adr | DALI_SELECTOR_BIT, DALI_102_OFF); // Power off without fade
|
||||
} else
|
||||
#endif // DALI_POWER_OFF_NO_FADE
|
||||
DaliSendData(adr, payload); // DAPC command - dim level
|
||||
return index;
|
||||
}
|
||||
|
||||
/*********************************************************************************************\
|
||||
* DALI-2 input device to control gear bindings
|
||||
*
|
||||
* A binding links push button events (IEC 62386-301) of one input device instance to a control
|
||||
* gear target so common switch and rotary use cases need no rules. See mode table in header.
|
||||
\*********************************************************************************************/
|
||||
|
||||
const char kDaliBindMode[] PROGMEM = "|Toggle|On|Off|Up|Down";
|
||||
|
||||
enum DaliBindMode { DALI_BIND_NONE, DALI_BIND_TOGGLE, DALI_BIND_ON, DALI_BIND_OFF, DALI_BIND_UP, DALI_BIND_DOWN, DALI_BIND_MAX };
|
||||
|
||||
// IEC 62386-301 push button event information used by bindings
|
||||
enum DaliButtonEvent { DALI_BUTTON_SHORT_PRESS = 2, DALI_BUTTON_DOUBLE_PRESS = 5, DALI_BUTTON_LONG_PRESS_START = 9,
|
||||
DALI_BUTTON_LONG_PRESS_REPEAT = 11, DALI_BUTTON_LONG_PRESS_STOP = 12 };
|
||||
|
||||
void DaliBindDim(uint32_t adr, bool down, bool start) {
|
||||
// Dim target for 200 ms at fade rate. Switch on if needed at start of an upward dim.
|
||||
uint32_t cmd = (down) ? DALI_102_DOWN : (start) ? DALI_102_ON_AND_STEP_UP : DALI_102_UP;
|
||||
DaliSendData(adr | DALI_SELECTOR_BIT, cmd);
|
||||
}
|
||||
|
||||
void DaliBindExecute(uint32_t slot, uint32_t info) {
|
||||
DliBind_t *bind = &Dali->Settings.bind[slot];
|
||||
uint32_t adr = DaliTarget2Address(bind->target);
|
||||
uint32_t index = ((bind->target >= 101) && (bind->target <= 116)) ? bind->target - 100 : 0;
|
||||
uint32_t mask = 1 << slot;
|
||||
bool down = Dali->bind_down & mask;
|
||||
|
||||
switch (info) {
|
||||
case DALI_BUTTON_SHORT_PRESS:
|
||||
switch (bind->mode) {
|
||||
case DALI_BIND_TOGGLE: {
|
||||
uint32_t payload = 2; // Toggle using stored broadcast or group state
|
||||
if ((bind->target >= 1) && (bind->target <= 64)) { // Short address state is not stored so ask the gear
|
||||
int level = DaliSendWaitResponse(adr | DALI_SELECTOR_BIT, DALI_102_QUERY_ACTUAL_LEVEL);
|
||||
if (level >= 0) { payload = (0 == level) ? 1 : 0; }
|
||||
}
|
||||
DaliSetPower(bind->target, payload);
|
||||
break;
|
||||
}
|
||||
case DALI_BIND_ON:
|
||||
DaliSetPower(bind->target, 1);
|
||||
break;
|
||||
case DALI_BIND_OFF:
|
||||
DaliSetPower(bind->target, 0);
|
||||
break;
|
||||
case DALI_BIND_UP:
|
||||
DaliBindDim(adr, false, true);
|
||||
break;
|
||||
case DALI_BIND_DOWN:
|
||||
DaliBindDim(adr, true, false);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case DALI_BUTTON_DOUBLE_PRESS:
|
||||
if ((DALI_BIND_TOGGLE == bind->mode) || (DALI_BIND_ON == bind->mode) || (DALI_BIND_UP == bind->mode)) {
|
||||
DaliSetPower(bind->target, 254); // Full brightness
|
||||
}
|
||||
break;
|
||||
case DALI_BUTTON_LONG_PRESS_START:
|
||||
switch (bind->mode) {
|
||||
case DALI_BIND_TOGGLE: // Alternate direction on every long press, always up when off
|
||||
down = (Dali->power[index]) ? !down : false;
|
||||
break;
|
||||
case DALI_BIND_ON:
|
||||
case DALI_BIND_UP:
|
||||
down = false;
|
||||
break;
|
||||
case DALI_BIND_OFF:
|
||||
case DALI_BIND_DOWN:
|
||||
down = true;
|
||||
break;
|
||||
}
|
||||
if (down) { Dali->bind_down |= mask; } else { Dali->bind_down &= ~mask; }
|
||||
DaliBindDim(adr, down, true);
|
||||
break;
|
||||
case DALI_BUTTON_LONG_PRESS_REPEAT:
|
||||
DaliBindDim(adr, down, false);
|
||||
break;
|
||||
case DALI_BUTTON_LONG_PRESS_STOP: { // Sync stored state with the level reached by dimming
|
||||
int level = DaliSendWaitResponse(adr | DALI_SELECTOR_BIT, DALI_102_QUERY_ACTUAL_LEVEL);
|
||||
if ((level >= 0) && (level <= 254)) { DaliSaveState(adr, level); }
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t DaliBindEvent(int address, int instance, int type, uint32_t info) {
|
||||
// Execute the first binding matching a push button event. Returns binding 1..DALI_MAX_BIND or 0 if none.
|
||||
// Type is unknown (-1) for Device and DeviceInstance event schemes, so only reject known non push button types
|
||||
if ((type != -1) && (type != 1)) { return 0; }
|
||||
for (uint32_t slot = 0; slot < DALI_MAX_BIND; slot++) {
|
||||
DliBind_t *bind = &Dali->Settings.bind[slot];
|
||||
if (DALI_BIND_NONE == bind->mode) { continue; }
|
||||
if (bind->device && (address != bind->device -1)) { continue; }
|
||||
if ((bind->instance != 255) && (instance != bind->instance)) { continue; }
|
||||
DaliBindExecute(slot, info);
|
||||
return slot +1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*********************************************************************************************\
|
||||
* DALI-2 input device event messages (IEC 62386-103 clause 9.7)
|
||||
*
|
||||
@@ -950,6 +1136,8 @@ void DaliEventMessage(uint32_t data) {
|
||||
}
|
||||
uint32_t info = data & 0x3FF;
|
||||
|
||||
uint32_t bind = DaliBindEvent(address, instance, type, info); // Execute binding before building the response
|
||||
|
||||
char scheme_name[16];
|
||||
GetTextIndexed(scheme_name, sizeof(scheme_name), scheme_index, kDaliEventScheme);
|
||||
Response_P(PSTR("{\"DALI\":{\"Event\":\"0x%06X\",\"Scheme\":\"%s\""), data, scheme_name);
|
||||
@@ -959,6 +1147,7 @@ void DaliEventMessage(uint32_t data) {
|
||||
if (type >= 0) { ResponseAppend_P(PSTR(",\"Type\":%d"), type); }
|
||||
if (instance >= 0) { ResponseAppend_P(PSTR(",\"Instance\":%d"), instance); }
|
||||
ResponseAppend_P(PSTR(",\"Info\":%d"), info);
|
||||
if (bind) { ResponseAppend_P(PSTR(",\"Bind\":%d"), bind); }
|
||||
|
||||
switch (type) {
|
||||
case 1: { // IEC 62386-301 Push button
|
||||
@@ -1370,7 +1559,7 @@ const char kDALICommands[] PROGMEM = D_PRFX_DALI "|" // Prefix
|
||||
#ifdef USE_LIGHT
|
||||
"|Light|Target|Channels|Retry"
|
||||
#endif // USE_LIGHT
|
||||
"|Send|Query|Scan|Group|GroupSliders|BS|Gear|Device|DeviceScan";
|
||||
"|Send|Query|Scan|Group|GroupSliders|BS|Gear|Device|DeviceScan|Bind";
|
||||
|
||||
void (* const DALICommand[])(void) PROGMEM = {
|
||||
&CmndDali, &CmndDaliPower, &CmndDaliDimmer,
|
||||
@@ -1378,7 +1567,7 @@ void (* const DALICommand[])(void) PROGMEM = {
|
||||
&CmndDaliLight, &CmndDaliTarget, &CmndDaliChannels, &CmndDaliRetry,
|
||||
#endif // USE_LIGHT
|
||||
&CmndDaliSend, &CmndDaliQuery, &CmndDaliScan, &CmndDaliGroup, &CmndDaliGroupSliders, &CmndDaliBroadcastSlider, &CmndDaliGear,
|
||||
&CmndDaliDevice, &CmndDaliDeviceScan };
|
||||
&CmndDaliDevice, &CmndDaliDeviceScan, &CmndDaliBind };
|
||||
|
||||
bool DaliJsonParse(void) {
|
||||
// {"addr":254,"cmd":100}
|
||||
@@ -1470,21 +1659,7 @@ void CmndDaliPower(void) {
|
||||
if (((XdrvMailbox.index >= 0) && (XdrvMailbox.index <= 64)) ||
|
||||
((XdrvMailbox.index >= 101) && (XdrvMailbox.index <= 116))) {
|
||||
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 254)) {
|
||||
if (XdrvMailbox.payload <= 2) {
|
||||
if (2 == XdrvMailbox.payload) {
|
||||
XdrvMailbox.payload = (Dali->power[index]) ? 0 : 1;
|
||||
}
|
||||
if (1 == XdrvMailbox.payload) {
|
||||
XdrvMailbox.payload = Dali->dimmer[index];
|
||||
}
|
||||
}
|
||||
uint32_t adr = DaliTarget2Address(XdrvMailbox.index);
|
||||
#ifdef DALI_POWER_OFF_NO_FADE
|
||||
if (!XdrvMailbox.payload) {
|
||||
DaliSendData(adr | DALI_SELECTOR_BIT, DALI_102_OFF); // Power off without fade
|
||||
} else
|
||||
#endif // DALI_POWER_OFF_NO_FADE
|
||||
DaliSendData(adr, XdrvMailbox.payload); // DAPC command - dim level
|
||||
index = DaliSetPower(XdrvMailbox.index, XdrvMailbox.payload);
|
||||
}
|
||||
}
|
||||
ResponseDali(index);
|
||||
@@ -1908,6 +2083,68 @@ void CmndDaliDevice(void) {
|
||||
|
||||
/*-------------------------------------------------------------------------------------------*/
|
||||
|
||||
void ResponseAppendDaliBind(uint32_t slot, bool with_slot) {
|
||||
// {"Bind":1,"Device":1,"Instance":0,"Target":0,"Mode":1,"Name":"Toggle"}
|
||||
DliBind_t *bind = &Dali->Settings.bind[slot];
|
||||
char mode_name[8];
|
||||
GetTextIndexed(mode_name, sizeof(mode_name), (bind->mode < DALI_BIND_MAX) ? bind->mode : 0, kDaliBindMode);
|
||||
ResponseAppend_P(PSTR("{"));
|
||||
if (with_slot) { ResponseAppend_P(PSTR("\"Bind\":%d,"), slot +1); }
|
||||
ResponseAppend_P(PSTR("\"Device\":%d,\"Instance\":%d,\"Target\":%d,\"Mode\":%d,\"Name\":\"%s\"}"),
|
||||
bind->device, bind->instance, bind->target, bind->mode, mode_name);
|
||||
}
|
||||
|
||||
void CmndDaliBind(void) {
|
||||
// DaliBind - Show all bindings
|
||||
// DaliBind2 - Show binding 2
|
||||
// DaliBind2 0 - Remove binding 2
|
||||
// DaliBind2 <device>,<instance>,<target>,<mode> - Bind instance of control device to control gear target
|
||||
// DaliBind1 1,0,0,1 - Control device 1 instance 0 toggles and dims broadcast
|
||||
// DaliBind2 0,255,101,2 - Any instance of any control device switches on group 1
|
||||
if (!XdrvMailbox.usridx) { // Show all bindings
|
||||
Response_P(PSTR("{\"%s\":["), XdrvMailbox.command);
|
||||
uint32_t count = 0;
|
||||
for (uint32_t slot = 0; slot < DALI_MAX_BIND; slot++) {
|
||||
if (Dali->Settings.bind[slot].mode) {
|
||||
if (count) { ResponseAppend_P(PSTR(",")); }
|
||||
ResponseAppendDaliBind(slot, true);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
ResponseAppend_P(PSTR("]}"));
|
||||
return;
|
||||
}
|
||||
if ((XdrvMailbox.index < 1) || (XdrvMailbox.index > DALI_MAX_BIND)) { return; }
|
||||
uint32_t slot = XdrvMailbox.index -1;
|
||||
DliBind_t *bind = &Dali->Settings.bind[slot];
|
||||
if (XdrvMailbox.data_len > 0) {
|
||||
uint32_t values[4] = { 0 };
|
||||
uint32_t params = ParseParameters(4, values);
|
||||
if ((1 == params) && (0 == values[0])) { // Remove binding
|
||||
memset(bind, 0, sizeof(DliBind_t));
|
||||
}
|
||||
else if ((4 == params) &&
|
||||
(values[0] <= 64) &&
|
||||
((values[1] <= 31) || (255 == values[1])) &&
|
||||
((values[2] <= 64) || ((values[2] >= 101) && (values[2] <= 116))) &&
|
||||
(values[3] >= DALI_BIND_TOGGLE) && (values[3] < DALI_BIND_MAX)) {
|
||||
bind->device = values[0];
|
||||
bind->instance = values[1];
|
||||
bind->target = values[2];
|
||||
bind->mode = values[3];
|
||||
Dali->bind_down &= ~(1 << slot);
|
||||
} else {
|
||||
ResponseCmndFailed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
Response_P(PSTR("{\"%s%d\":"), XdrvMailbox.command, XdrvMailbox.index);
|
||||
ResponseAppendDaliBind(slot, false);
|
||||
ResponseJsonEnd();
|
||||
}
|
||||
|
||||
/*-------------------------------------------------------------------------------------------*/
|
||||
|
||||
void CmndDaliBroadcastSlider(void) {
|
||||
// DaliBS 1 - Show broadcast slider
|
||||
// DaliBS 0 - Hide broadcast slider
|
||||
|
||||
Reference in new issue
Block a user