Skip to content

Commit c9d677f

Browse files
authored
Fix shutter command mapping to use correct Tasmota format (#468)
1 parent ef8dffd commit c9d677f

File tree

4 files changed

+165
-2
lines changed

4 files changed

+165
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ States:
125125

126126
### **WORK IN PROGRESS**
127127
* (copilot) **BREAKING**: Commands now correctly use cmnd/ prefix instead of tele/ prefix - Fix regex bug causing MQTT commands to use "tele" instead of "cmnd" topics
128+
* (copilot) Fix shutter command mapping to use correct Tasmota format - Transforms Shutter1_Position to ShutterPosition1 for proper device control
128129
* (copilot) Fix IRHVAC Power, Light and Mode fields showing NULL instead of actual string values
129130
* (copilot) Add Zigbee device control support for Tasmota coordinators - users can now control Zigbee devices (Power/Dimmer) through ioBroker states via automatic ZbSend command generation
130131
* (copilot) Added configuration for advanced MQTT settings

lib/datapoints.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,69 @@ module.exports = {
302302
IrReceived_IRHVAC_Beep: { type: 'string', role: 'state', read: true, write: false },
303303
IrReceived_IRHVAC_Sleep: { type: 'number', role: 'value', read: true, write: false },
304304
IrReceived_IRHVAC_Celsius: { type: 'string', role: 'state', read: true, write: false },
305+
// Shutter control datapoints (Position, Direction, Target, Tilt for shutters 1-16)
306+
Shutter1_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
307+
Shutter1_Direction: { type: 'number', role: 'value', read: true, write: false },
308+
Shutter1_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
309+
Shutter1_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
310+
Shutter2_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
311+
Shutter2_Direction: { type: 'number', role: 'value', read: true, write: false },
312+
Shutter2_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
313+
Shutter2_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
314+
Shutter3_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
315+
Shutter3_Direction: { type: 'number', role: 'value', read: true, write: false },
316+
Shutter3_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
317+
Shutter3_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
318+
Shutter4_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
319+
Shutter4_Direction: { type: 'number', role: 'value', read: true, write: false },
320+
Shutter4_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
321+
Shutter4_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
322+
Shutter5_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
323+
Shutter5_Direction: { type: 'number', role: 'value', read: true, write: false },
324+
Shutter5_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
325+
Shutter5_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
326+
Shutter6_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
327+
Shutter6_Direction: { type: 'number', role: 'value', read: true, write: false },
328+
Shutter6_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
329+
Shutter6_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
330+
Shutter7_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
331+
Shutter7_Direction: { type: 'number', role: 'value', read: true, write: false },
332+
Shutter7_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
333+
Shutter7_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
334+
Shutter8_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
335+
Shutter8_Direction: { type: 'number', role: 'value', read: true, write: false },
336+
Shutter8_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
337+
Shutter8_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
338+
Shutter9_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
339+
Shutter9_Direction: { type: 'number', role: 'value', read: true, write: false },
340+
Shutter9_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
341+
Shutter9_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
342+
Shutter10_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
343+
Shutter10_Direction: { type: 'number', role: 'value', read: true, write: false },
344+
Shutter10_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
345+
Shutter10_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
346+
Shutter11_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
347+
Shutter11_Direction: { type: 'number', role: 'value', read: true, write: false },
348+
Shutter11_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
349+
Shutter11_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
350+
Shutter12_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
351+
Shutter12_Direction: { type: 'number', role: 'value', read: true, write: false },
352+
Shutter12_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
353+
Shutter12_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
354+
Shutter13_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
355+
Shutter13_Direction: { type: 'number', role: 'value', read: true, write: false },
356+
Shutter13_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
357+
Shutter13_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
358+
Shutter14_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
359+
Shutter14_Direction: { type: 'number', role: 'value', read: true, write: false },
360+
Shutter14_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
361+
Shutter14_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
362+
Shutter15_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
363+
Shutter15_Direction: { type: 'number', role: 'value', read: true, write: false },
364+
Shutter15_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
365+
Shutter15_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
366+
Shutter16_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
367+
Shutter16_Direction: { type: 'number', role: 'value', read: true, write: false },
368+
Shutter16_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
369+
Shutter16_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
305370
};

lib/server.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,20 +252,41 @@ function MQTTServer(adapter) {
252252
}
253253
}
254254

255+
/**
256+
* Transform shutter state names to correct Tasmota command format
257+
* e.g., "Shutter1_Position" -> "ShutterPosition1"
258+
* e.g., "Shutter1_Tilt" -> "ShutterTilt1"
259+
*
260+
* @param {string} stateId - The original state ID to transform
261+
* @returns {string} The transformed state ID for Tasmota commands
262+
*/
263+
function transformShutterStateId(stateId) {
264+
const shutterMatch = stateId.match(/^Shutter(\d+)_(Position|Direction|Target|Tilt)$/);
265+
if (shutterMatch) {
266+
const shutterNumber = shutterMatch[1];
267+
const command = shutterMatch[2];
268+
return `Shutter${command}${shutterNumber}`;
269+
}
270+
return stateId;
271+
}
272+
255273
function setStateImmediate(channelId, stateId, val) {
274+
// Transform shutter state names to correct Tasmota command format
275+
const transformedStateId = transformShutterStateId(stateId);
276+
256277
if (clients[channelId] && clients[channelId]._map && clients[channelId]._map[stateId]) {
257278
setImmediate(
258279
sendState2Client,
259280
clients[channelId],
260-
clients[channelId]._map[stateId] || `cmnd/sonoff/${stateId}`,
281+
clients[channelId]._map[stateId] || `cmnd/sonoff/${transformedStateId}`,
261282
val,
262283
adapter.config.defaultQoS,
263284
);
264285
} else if (clients[channelId] && clients[channelId]._fallBackName) {
265286
setImmediate(
266287
sendState2Client,
267288
clients[channelId],
268-
`cmnd/${clients[channelId]._fallBackName}/${stateId}`,
289+
`cmnd/${clients[channelId]._fallBackName}/${transformedStateId}`,
269290
val,
270291
adapter.config.defaultQoS,
271292
);

test/testServer.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ const rules = {
6666
'tele/HomeMonitor/STATE': {send: '{"Time":"2018-01-19T20:15:16","Uptime":0,"Vcc":3.170,"Wifi":{"AP":1,"SSId":"SmartHOME","RSSI":100,"APMac":"60:31:97:3E:74:B4"}}', expect: {Vcc: 3.170, Wifi_RSSI: 100}},
6767
'tele/HomeMonitor/SENSOR': {send: '{"Time":"2018-01-19T20:15:16","Temperature":20.0,"Humidity":16.0,"Light":10,"Noise":60,"AirQuality":90,"TempUnit":"C"}', expect: {Temperature: 20.0, Humidity: 16.0, Light: 10, Noise: 60, AirQuality: 90}},
6868
'tele/esp32_shutter/STATE': {send: '{"Time":"2025-01-07T10:00:00","Uptime":"0T01:00:00","SHUTTER1":0,"SHUTTER2":25,"SHUTTER3":50,"SHUTTER4":75,"SHUTTER5":100,"SHUTTER6":0,"SHUTTER7":25,"SHUTTER8":50,"SHUTTER9":75,"SHUTTER10":100,"SHUTTER11":0,"SHUTTER12":25,"SHUTTER13":50,"SHUTTER14":75,"SHUTTER15":100,"SHUTTER16":33}', expect: {SHUTTER1: 0, SHUTTER2: 25, SHUTTER3: 50, SHUTTER4: 75, SHUTTER5: 100, SHUTTER6: 0, SHUTTER7: 25, SHUTTER8: 50, SHUTTER9: 75, SHUTTER10: 100, SHUTTER11: 0, SHUTTER12: 25, SHUTTER13: 50, SHUTTER14: 75, SHUTTER15: 100, SHUTTER16: 33}},
69+
// Shutter control test cases - issue #278
70+
'stat/TM_Shutter/RESULT': {send: '{"Shutter1":{"Position":56,"Direction":0,"Target":56,"Tilt":0}}', expect: {'Shutter1_Position': 56, 'Shutter1_Direction': 0, 'Shutter1_Target': 56, 'Shutter1_Tilt': 0}},
71+
'stat/Shutter_Device/RESULT': {send: '{"Shutter2":{"Position":75,"Direction":1,"Target":100,"Tilt":25}}', expect: {'Shutter2_Position': 75, 'Shutter2_Direction': 1, 'Shutter2_Target': 100, 'Shutter2_Tilt': 25}},
72+
'stat/MultiShutter/RESULT': {send: '{"Shutter3":{"Position":0,"Direction":-1,"Target":0,"Tilt":50},"Shutter4":{"Position":100,"Direction":0,"Target":100,"Tilt":0}}', expect: {'Shutter3_Position': 0, 'Shutter3_Direction': -1, 'Shutter3_Target': 0, 'Shutter3_Tilt': 50, 'Shutter4_Position': 100, 'Shutter4_Direction': 0, 'Shutter4_Target': 100, 'Shutter4_Tilt': 0}},
6973
'tele/tasmota/RESULT': {send: '{"IrReceived":{"Protocol":"FUJITSU_AC","Bits":128,"Data":"0x1463001010FE0930210000000000208F","Repeat":0,"IRHVAC":{"Vendor":"FUJITSU_AC","Model":1,"Mode":"Auto","Power":"On","Celsius":"On","Temp":18,"FanSpeed":"Auto","SwingV":"Off","SwingH":"Off","Quiet":"Off","Turbo":"Off","Econo":"Off","Light":"Off","Filter":"Off","Clean":"Off","Beep":"Off","Sleep":-1}}}', expect: {'IrReceived_IRHVAC_Power': 'On', 'IrReceived_IRHVAC_Light': 'Off', 'IrReceived_IRHVAC_Mode': 'Auto', 'IrReceived_IRHVAC_Vendor': 'FUJITSU_AC', 'IrReceived_IRHVAC_Temp': 18}},
7074
// Zigbee bulb test case from issue #265
7175
'tele/Zigbee_Coordinator_CC2530/SENSOR': {send: '{"Time":"2022-05-05T20:49:08","ZbReceived":{"0x0856":{"Device":"0x0856","Name":"E14 Bulb","Power":1,"Dimmer":20,"Endpoint":1,"LinkQuality":65}}}', expect: {'ZbReceived_0x0856_Power': 1, 'ZbReceived_0x0856_Dimmer': 20, 'ZbReceived_0x0856_Device': '0x0856', 'ZbReceived_0x0856_Name': 'E14 Bulb'}},
@@ -422,6 +426,78 @@ describe('Sonoff server: Test mqtt server', () => {
422426
}, 500);
423427
}).timeout(10000);
424428

429+
it('Sonoff Server: Test shutter command transformation', function (done) { // let FUNCTION and not => here
430+
this.timeout(5000);
431+
432+
// First send a shutter RESULT message to create the device states using a shutter client
433+
mqttClientEmitter.publish('stat/TM_Shutter/RESULT',
434+
'{"Shutter1":{"Position":56,"Direction":0,"Target":56,"Tilt":0}}');
435+
436+
setTimeout(() => {
437+
// Simulate user changing the Shutter1_Position state
438+
const shutterPositionStateId = 'sonoff.0.Emitter_1.Shutter1_Position';
439+
440+
// Clear any previous received messages
441+
lastReceivedTopic1 = null;
442+
lastReceivedMessage1 = null;
443+
lastReceivedTopic2 = null;
444+
lastReceivedMessage2 = null;
445+
446+
// Trigger state change
447+
states.setState(shutterPositionStateId, {val: 49, ack: false}, (err) => {
448+
expect(err).to.be.not.ok;
449+
450+
setTimeout(() => {
451+
// Check if ShutterPosition1 command was published (check both emitter and detector received messages)
452+
const receivedTopic = lastReceivedTopic1 || lastReceivedTopic2;
453+
const receivedMessage = lastReceivedMessage1 || lastReceivedMessage2;
454+
455+
expect(receivedTopic).to.be.ok;
456+
expect(receivedTopic).to.equal('cmnd/Emitter_1/ShutterPosition1');
457+
expect(receivedMessage).to.equal('49');
458+
459+
done();
460+
}, 100);
461+
});
462+
}, 500);
463+
}).timeout(10000);
464+
465+
it('Sonoff Server: Test shutter tilt command transformation', function (done) { // let FUNCTION and not => here
466+
this.timeout(5000);
467+
468+
// First send a shutter RESULT message to create the device states using a shutter client
469+
mqttClientEmitter.publish('stat/TM_Shutter/RESULT',
470+
'{"Shutter2":{"Position":75,"Direction":1,"Target":100,"Tilt":25}}');
471+
472+
setTimeout(() => {
473+
// Simulate user changing the Shutter2_Tilt state
474+
const shutterTiltStateId = 'sonoff.0.Emitter_1.Shutter2_Tilt';
475+
476+
// Clear any previous received messages
477+
lastReceivedTopic1 = null;
478+
lastReceivedMessage1 = null;
479+
lastReceivedTopic2 = null;
480+
lastReceivedMessage2 = null;
481+
482+
// Trigger state change
483+
states.setState(shutterTiltStateId, {val: 80, ack: false}, (err) => {
484+
expect(err).to.be.not.ok;
485+
486+
setTimeout(() => {
487+
// Check if ShutterTilt2 command was published (check both emitter and detector received messages)
488+
const receivedTopic = lastReceivedTopic1 || lastReceivedTopic2;
489+
const receivedMessage = lastReceivedMessage1 || lastReceivedMessage2;
490+
491+
expect(receivedTopic).to.be.ok;
492+
expect(receivedTopic).to.equal('cmnd/Emitter_1/ShutterTilt2');
493+
expect(receivedMessage).to.equal('80');
494+
495+
done();
496+
}, 100);
497+
});
498+
}, 500);
499+
}).timeout(10000);
500+
425501
after('Sonoff Server: Stop js-controller', function (_done) { // let FUNCTION and not => here
426502
this.timeout(5000);
427503
mqttClientEmitter.stop();

0 commit comments

Comments
 (0)