Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ States:

### **WORK IN PROGRESS**
* (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
* (copilot) Fix shutter command mapping to use correct Tasmota format - Transforms Shutter1_Position to ShutterPosition1 for proper device control
* (copilot) Fix IRHVAC Power, Light and Mode fields showing NULL instead of actual string values
* (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
* (copilot) Added configuration for advanced MQTT settings
Expand Down
65 changes: 65 additions & 0 deletions lib/datapoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -302,4 +302,69 @@ module.exports = {
IrReceived_IRHVAC_Beep: { type: 'string', role: 'state', read: true, write: false },
IrReceived_IRHVAC_Sleep: { type: 'number', role: 'value', read: true, write: false },
IrReceived_IRHVAC_Celsius: { type: 'string', role: 'state', read: true, write: false },
// Shutter control datapoints (Position, Direction, Target, Tilt for shutters 1-16)
Shutter1_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter1_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter1_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter1_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter2_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter2_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter2_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter2_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter3_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter3_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter3_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter3_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter4_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter4_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter4_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter4_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter5_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter5_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter5_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter5_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter6_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter6_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter6_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter6_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter7_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter7_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter7_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter7_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter8_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter8_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter8_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter8_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter9_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter9_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter9_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter9_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter10_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter10_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter10_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter10_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter11_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter11_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter11_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter11_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter12_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter12_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter12_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter12_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter13_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter13_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter13_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter13_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter14_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter14_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter14_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter14_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter15_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter15_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter15_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter15_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter16_Position: { type: 'number', role: 'level.blind', read: true, write: true, min: 0, max: 100, unit: '%' },
Shutter16_Direction: { type: 'number', role: 'value', read: true, write: false },
Shutter16_Target: { type: 'number', role: 'value', read: true, write: false, min: 0, max: 100, unit: '%' },
Shutter16_Tilt: { type: 'number', role: 'level.tilt', read: true, write: true, min: 0, max: 100, unit: '%' },
};
25 changes: 23 additions & 2 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,20 +252,41 @@ function MQTTServer(adapter) {
}
}

/**
* Transform shutter state names to correct Tasmota command format
* e.g., "Shutter1_Position" -> "ShutterPosition1"
* e.g., "Shutter1_Tilt" -> "ShutterTilt1"
*
* @param {string} stateId - The original state ID to transform
* @returns {string} The transformed state ID for Tasmota commands
*/
function transformShutterStateId(stateId) {
const shutterMatch = stateId.match(/^Shutter(\d+)_(Position|Direction|Target|Tilt)$/);
if (shutterMatch) {
const shutterNumber = shutterMatch[1];
const command = shutterMatch[2];
return `Shutter${command}${shutterNumber}`;
}
return stateId;
}

function setStateImmediate(channelId, stateId, val) {
// Transform shutter state names to correct Tasmota command format
const transformedStateId = transformShutterStateId(stateId);

if (clients[channelId] && clients[channelId]._map && clients[channelId]._map[stateId]) {
setImmediate(
sendState2Client,
clients[channelId],
clients[channelId]._map[stateId] || `cmnd/sonoff/${stateId}`,
clients[channelId]._map[stateId] || `cmnd/sonoff/${transformedStateId}`,
val,
adapter.config.defaultQoS,
);
} else if (clients[channelId] && clients[channelId]._fallBackName) {
setImmediate(
sendState2Client,
clients[channelId],
`cmnd/${clients[channelId]._fallBackName}/${stateId}`,
`cmnd/${clients[channelId]._fallBackName}/${transformedStateId}`,
val,
adapter.config.defaultQoS,
);
Expand Down
76 changes: 76 additions & 0 deletions test/testServer.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const rules = {
'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}},
'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}},
'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}},
// Shutter control test cases - issue #278
'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}},
'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}},
'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}},
'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}},
// Zigbee bulb test case from issue #265
'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'}},
Expand Down Expand Up @@ -422,6 +426,78 @@ describe('Sonoff server: Test mqtt server', () => {
}, 500);
}).timeout(10000);

it('Sonoff Server: Test shutter command transformation', function (done) { // let FUNCTION and not => here
this.timeout(5000);

// First send a shutter RESULT message to create the device states using a shutter client
mqttClientEmitter.publish('stat/TM_Shutter/RESULT',
'{"Shutter1":{"Position":56,"Direction":0,"Target":56,"Tilt":0}}');

setTimeout(() => {
// Simulate user changing the Shutter1_Position state
const shutterPositionStateId = 'sonoff.0.Emitter_1.Shutter1_Position';

// Clear any previous received messages
lastReceivedTopic1 = null;
lastReceivedMessage1 = null;
lastReceivedTopic2 = null;
lastReceivedMessage2 = null;

// Trigger state change
states.setState(shutterPositionStateId, {val: 49, ack: false}, (err) => {
expect(err).to.be.not.ok;

setTimeout(() => {
// Check if ShutterPosition1 command was published (check both emitter and detector received messages)
const receivedTopic = lastReceivedTopic1 || lastReceivedTopic2;
const receivedMessage = lastReceivedMessage1 || lastReceivedMessage2;

expect(receivedTopic).to.be.ok;
expect(receivedTopic).to.equal('cmnd/Emitter_1/ShutterPosition1');
expect(receivedMessage).to.equal('49');

done();
}, 100);
});
}, 500);
}).timeout(10000);

it('Sonoff Server: Test shutter tilt command transformation', function (done) { // let FUNCTION and not => here
this.timeout(5000);

// First send a shutter RESULT message to create the device states using a shutter client
mqttClientEmitter.publish('stat/TM_Shutter/RESULT',
'{"Shutter2":{"Position":75,"Direction":1,"Target":100,"Tilt":25}}');

setTimeout(() => {
// Simulate user changing the Shutter2_Tilt state
const shutterTiltStateId = 'sonoff.0.Emitter_1.Shutter2_Tilt';

// Clear any previous received messages
lastReceivedTopic1 = null;
lastReceivedMessage1 = null;
lastReceivedTopic2 = null;
lastReceivedMessage2 = null;

// Trigger state change
states.setState(shutterTiltStateId, {val: 80, ack: false}, (err) => {
expect(err).to.be.not.ok;

setTimeout(() => {
// Check if ShutterTilt2 command was published (check both emitter and detector received messages)
const receivedTopic = lastReceivedTopic1 || lastReceivedTopic2;
const receivedMessage = lastReceivedMessage1 || lastReceivedMessage2;

expect(receivedTopic).to.be.ok;
expect(receivedTopic).to.equal('cmnd/Emitter_1/ShutterTilt2');
expect(receivedMessage).to.equal('80');

done();
}, 100);
});
}, 500);
}).timeout(10000);

after('Sonoff Server: Stop js-controller', function (_done) { // let FUNCTION and not => here
this.timeout(5000);
mqttClientEmitter.stop();
Expand Down
Loading