-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add SAS980SWT-7-Z01 valve please #5727
Comments
I see the ZVG1 also supports features like |
In Tuya IOT Cloud Development platform, the debug section shows the following data points:
Regarding the DP#16 and DP#17, I saw on the Saswell web site and on the shop's website that the SAS980SWT-7-Z01 has programming capabilities, with normal irrigation timer (start time + duration) and cycle & soak timer (start time + end time + on/off periods). When connecting my valve to Smart Life app I can set these timers. On the valve connected on Z2M, I tried to reuse the timers declaration from the ZVG1, and set the timer through the Z2M UI, but there have been no action yet. |
It has also cycle timers capabilities, which I don't use. |
I made some trials on Tuya Smart Life app, I deleted the existing test timers (2 cycle timers and 1 normal timer) I previously set on the valve connected through the Tuya gateway. In the IOT logs I can see these lines once deleted :
Then I set the follwowing timers :
And this is the corresponding logs :
Now, let's add the same timers to my other instance connected through Z2M. I can see many lines with "dp":16, inside but it seems the attached data is not accumulating when adding a timer as in Tuya logs where I get In the log I could also see the messages that may explain the time shift I have with the valve connected to Tuya gateway:
So it seems there is time synchronization feature for the timers, but the way to use it is missing in the converters. |
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days |
Starting from zigbee2mqtt 1.31.0, the external converter makes Z2M crash, the same way as reported in Koenkk/zigbee2mqtt#17863 I tried to fix the converter, adding some "legacy", but couldn't find the solution, it always fails. I also need to add a Lexman device which is a copycat of NodOn SIN-4-1-20, would be good to make it compatible with new Z2M releases. |
I suggest to make the |
Thanks for the advice. The issue was caused by The updated fileconst legacy = require('zigbee-herdsman-converters/lib/legacy');
const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee};
const tz = {...require('zigbee-herdsman-converters/converters/toZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').toZigbee};
const tuya = {...require('zigbee-herdsman-converters/lib/tuya'), legacy: require('zigbee-herdsman-converters/lib/legacy').tuya};
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const fzLocal = {
SAS980SWT: {
cluster: 'manuSpecificTuya',
type: 'commandDataResponse',
convert: (model, msg, publish, options, meta) => {
const dpValue = tuya.firstDpValue(msg, meta, 'SAS980SWT');
const value = tuya.getDataValue(dpValue);
const dp = dpValue.dp;
switch (dp) {
case tuya.dataPoints.state: {
return {state: value ? 'ON': 'OFF'};
}
case 5: {
// Seems value is reported in tenths of liters
return {water_consumed: (value / 10.0).toFixed(2)};
}
case 7: {
return {battery: value};
}
case 10: {
let data = 'disabled';
if (value == 1) {
data = '24h';
} else if (value == 2) {
data = '48h';
} else if (value == 3) {
data = '72h';
}
return {weather_delay: data};
}
case 11: {
// value reported in seconds
return {timer_time_left: value / 60};
}
case 12: {
if (value === 0) return {timer_state: 'disabled'};
else if (value === 1) return {timer_state: 'active'};
else return {timer_state: 'enabled'};
}
case 15: {
// value reported in seconds
return {last_valve_open_duration: value / 60};
}
case 16: {
const tresult = {
cycle_timer_1: '',
cycle_timer_2: '',
cycle_timer_3: '',
cycle_timer_4: '',
};
for (let index = 0; index < 40; index += 12) {
const timer = tuya.convertRawToCycleTimer(value.slice(index));
if (timer.irrigationDuration > 0) {
tresult['cycle_timer_' + (index / 13 + 1)] = timer.starttime +
' / ' + timer.endtime + ' / ' +
timer.irrigationDuration + ' / ' +
timer.pauseDuration + ' / ' +
timer.weekdays + ' / ' + timer.active;
}
}
return tresult;
}
case 17: {
const tresult = {
normal_schedule_timer_1: '',
normal_schedule_timer_2: '',
normal_schedule_timer_3: '',
normal_schedule_timer_4: '',
};
for (let index = 0; index < 40; index += 13) {
const timer = tuya.convertRawToTimer(value.slice(index));
if (timer.duration > 0) {
tresult['normal_schedule_timer_' + (index / 13 + 1)] = timer.time +
' / ' + timer.duration +
' / ' + timer.weekdays +
' / ' + timer.active;
}
}
return tresult;
}
default: {
meta.logger.warn(`zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
};
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_akjefhj5'}],
model: 'SAS980SWT-7-Z01',
vendor: 'SASWELL',
description: 'Zigbee smart water valve',
onEvent: tuya.onEventSetLocalTime,
fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report],
toZigbee: [tz.legacy.tuya_switch_state, tz.legacy.ZVG1_weather_delay, tz.legacy.ZVG1_timer, tz.legacy.ZVG1_cycle_timer, tz.legacy.ZVG1_normal_schedule_timer],
exposes: [e.switch().setAccess('state', ea.STATE_SET), e.battery(),
exposes.enum('weather_delay', ea.STATE_SET, ['disabled', '24h', '48h', '72h']),
exposes.enum('timer_state', ea.STATE, ['disabled', 'active', 'enabled']),
exposes.numeric('timer', ea.STATE_SET).withValueMin(0).withValueMax(60).withUnit('min')
.withDescription('Auto off after specific time'),
exposes.numeric('timer_time_left', ea.STATE).withUnit('min')
.withDescription('Auto off timer time left'),
exposes.numeric('last_valve_open_duration', ea.STATE).withUnit('min')
.withDescription('Time the valve was open when state on'),
exposes.numeric('water_consumed', ea.STATE).withUnit('l')
.withDescription('Liters of water consumed'),
exposes.text('cycle_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)')],
};
module.exports = definition; Question, will the |
Maybe, but not any time soon. |
Thanks. I tried to work on the timers, but it seems it's not working. I first tried to add the fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report, fz.ignore_tuya_set_time], onEvent: tuya.onEventSetLocalTime, As the const timer = tuya.legacy.convertRawToCycleTimer(value.slice(index));
// ...
const timer = tuya.legacy.convertRawToTimer(value.slice(index)); I tried with legacy.convertRawTo... and legacy.tuya.convertRawTo... but it leads to "is not a function" errors. With tuya.legacy.convertRawTo... it seems to work better, except that when I remove batteries and put them back in the valve, I get some errors in the log:
The full log...
At least, the time sync seems to work as it doesn't return any error. The updated file...const legacy = require('zigbee-herdsman-converters/lib/legacy');
const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee};
const tz = {...require('zigbee-herdsman-converters/converters/toZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').toZigbee};
const tuya = {...require('zigbee-herdsman-converters/lib/tuya'), legacy: require('zigbee-herdsman-converters/lib/legacy').tuya};
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const fzLocal = {
SAS980SWT: {
cluster: 'manuSpecificTuya',
type: 'commandDataResponse',
convert: (model, msg, publish, options, meta) => {
const dpValue = tuya.firstDpValue(msg, meta, 'SAS980SWT');
const value = tuya.getDataValue(dpValue);
const dp = dpValue.dp;
switch (dp) {
case tuya.dataPoints.state: {
return {state: value ? 'ON': 'OFF'};
}
case 5: {
// Seems value is reported in tenths of liters
return {water_consumed: (value / 10.0).toFixed(2)};
}
case 7: {
return {battery: value};
}
case 10: {
let data = 'disabled';
if (value == 1) {
data = '24h';
} else if (value == 2) {
data = '48h';
} else if (value == 3) {
data = '72h';
}
return {weather_delay: data};
}
case 11: {
// value reported in seconds
return {timer_time_left: value / 60};
}
case 12: {
if (value === 0) return {timer_state: 'disabled'};
else if (value === 1) return {timer_state: 'active'};
else return {timer_state: 'enabled'};
}
case 15: {
// value reported in seconds
return {last_valve_open_duration: value / 60};
}
case 16: {
const tresult = {
cycle_timer_1: '',
cycle_timer_2: '',
cycle_timer_3: '',
cycle_timer_4: '',
};
for (let index = 0; index < 40; index += 12) {
const timer = tuya.legacy.convertRawToCycleTimer(value.slice(index));
if (timer.irrigationDuration > 0) {
tresult['cycle_timer_' + (index / 13 + 1)] = timer.starttime +
' / ' + timer.endtime + ' / ' +
timer.irrigationDuration + ' / ' +
timer.pauseDuration + ' / ' +
timer.weekdays + ' / ' + timer.active;
}
}
return tresult;
}
case 17: {
const tresult = {
normal_schedule_timer_1: '',
normal_schedule_timer_2: '',
normal_schedule_timer_3: '',
normal_schedule_timer_4: '',
};
for (let index = 0; index < 40; index += 13) {
const timer = tuya.legacy.convertRawToTimer(value.slice(index));
if (timer.duration > 0) {
tresult['normal_schedule_timer_' + (index / 13 + 1)] = timer.time +
' / ' + timer.duration +
' / ' + timer.weekdays +
' / ' + timer.active;
}
}
return tresult;
}
default: {
meta.logger.warn(`zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
};
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_akjefhj5'}],
model: 'SAS980SWT-7-Z01',
vendor: 'SASWELL',
description: 'Zigbee smart water valve',
onEvent: tuya.onEventSetLocalTime,
fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report, fz.ignore_tuya_set_time],
toZigbee: [tz.legacy.tuya_switch_state, tz.legacy.ZVG1_weather_delay, tz.legacy.ZVG1_timer, tz.legacy.ZVG1_cycle_timer, tz.legacy.ZVG1_normal_schedule_timer],
onEvent: tuya.onEventSetLocalTime,
exposes: [e.switch().setAccess('state', ea.STATE_SET), e.battery(),
exposes.enum('weather_delay', ea.STATE_SET, ['disabled', '24h', '48h', '72h']),
exposes.enum('timer_state', ea.STATE, ['disabled', 'active', 'enabled']),
exposes.numeric('timer', ea.STATE_SET).withValueMin(0).withValueMax(60).withUnit('min')
.withDescription('Auto off after specific time'),
exposes.numeric('timer_time_left', ea.STATE).withUnit('min')
.withDescription('Auto off timer time left'),
exposes.numeric('last_valve_open_duration', ea.STATE).withUnit('min')
.withDescription('Time the valve was open when state on'),
exposes.numeric('water_consumed', ea.STATE).withUnit('l')
.withDescription('Liters of water consumed'),
exposes.text('cycle_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)')],
};
module.exports = definition; |
I finally brought everything locally, to check the functionality; and it seems to be working, I added a few info in log when addressing the timers and the sent data is read back from the data point. The js file as txt : |
Some news about timers integration. I didn't use the valve for, and let the timers set on the valve while monitoring the state in HA, and it didn't report any cycling; so I thought it wasn't working. But I used the valve yesterday to adjust the level of the swimming pool, and left the water tap opened. This morning after 10am I noticed a water flow noise, and checking at the pool I could confirm the valve was opened, and looking at the pool water temperature (sensor being close to water filling pipe) I could se the temperature going up and down with a period of 15 minutes. So it confirms the programming of the timers is working fine, but when it opens the valve, the main switch remains off, so there is no report that valve is opened. |
I managed to fix all issues with timers:
Now all normal and cycle schedulers are working fine; except for the time offset of 360 minutes. Full file...const legacy = require('zigbee-herdsman-converters/lib/legacy');
const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee};
const tz = {...require('zigbee-herdsman-converters/converters/toZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').toZigbee};
const tuya = {...require('zigbee-herdsman-converters/lib/tuya'), legacy: require('zigbee-herdsman-converters/lib/legacy').tuya};
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
const dataTypes = {
raw: 0, // [ bytes ]
bool: 1, // [0/1]
value: 2, // [ 4 byte value ]
string: 3, // [ N byte string ]
enum: 4, // [ 0-255 ]
bitmap: 5, // [ 1,2,4 bytes ] as bits
};
function dpValueFromRaw(dp, rawBuffer) {
return {dp, datatype: dataTypes.raw, data: rawBuffer};
}
async function sendDataPoints(entity, dpValues, cmd, seq=undefined) {
if (seq === undefined) {
if (sendDataPoints.seq === undefined) {
sendDataPoints.seq = 0;
} else {
sendDataPoints.seq++;
sendDataPoints.seq %= 0xFFFF;
}
seq = sendDataPoints.seq;
}
await entity.command(
'manuSpecificTuya',
cmd || 'dataRequest',
{
seq,
dpValues,
},
{disableDefaultResponse: true},
);
return seq;
}
async function sendDataPointRaw(entity, dp, value, cmd, seq=undefined) {
return await sendDataPoints(entity, [dpValueFromRaw(dp, value)], cmd, seq);
}
function convertRawToCycleTimer(value) {
let timernr = 0;
let starttime = '00:00';
let endtime = '00:00';
let irrigationDuration = 0;
let pauseDuration = 0;
let weekdays = 'once';
let timeractive = 0;
if (value.length > 11) {
timernr = value[1];
timeractive = value[2];
if (value[3] > 0) {
weekdays = (value[3] & 0x01 ? 'Su' : '') +
(value[3] & 0x02 ? 'Mo' : '') +
(value[3] & 0x04 ? 'Tu' : '') +
(value[3] & 0x08 ? 'We' : '') +
(value[3] & 0x10 ? 'Th' : '') +
(value[3] & 0x20 ? 'Fr' : '') +
(value[3] & 0x40 ? 'Sa' : '');
} else {
weekdays = 'once';
}
let minsincemidnight = value[4] * 256 + value[5];
starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
minsincemidnight = value[6] * 256 + value[7];
endtime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
irrigationDuration = value[8] * 256 + value[9];
pauseDuration = value[10] * 256 + value[11];
}
return {
timernr: timernr,
starttime: starttime,
endtime: endtime,
irrigationDuration: irrigationDuration,
pauseDuration: pauseDuration,
weekdays: weekdays,
active: timeractive,
};
}
function convertRawToTimer(value) {
let timernr = 0;
let starttime = '00:00';
let duration = 0;
let weekdays = 'once';
let timeractive = '';
if (value.length > 12) {
timernr = value[1];
const minsincemidnight = value[2] * 256 + value[3];
starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
duration = value[4] * 256 + value[5];
if (value[6] > 0) {
weekdays = (value[6] & 0x01 ? 'Su' : '') +
(value[6] & 0x02 ? 'Mo' : '') +
(value[6] & 0x04 ? 'Tu' : '') +
(value[6] & 0x08 ? 'We' : '') +
(value[6] & 0x10 ? 'Th' : '') +
(value[6] & 0x20 ? 'Fr' : '') +
(value[6] & 0x40 ? 'Sa' : '');
} else {
weekdays = 'once';
}
timeractive = value[8];
}
return {timernr: timernr, time: starttime, duration: duration, weekdays: weekdays, active: timeractive};
}
function convertTimeTo2ByteHexArray(time) {
const timeArray = time.split(':');
if (timeArray.length != 2) {
throw new Error('Time format incorrect');
}
const timeHour = parseInt(timeArray[0]);
const timeMinute = parseInt(timeArray[1]);
if (timeHour > 23 || timeMinute > 59) {
throw new Error('Time incorrect');
}
return convertDecimalValueTo2ByteHexArray(timeHour * 60 + timeMinute);
}
function convertWeekdaysTo1ByteHexArray(weekdays) {
let nr = 0;
if (weekdays == 'once') {
return nr;
}
if (weekdays.includes('Sa')) {
nr |= 0x40;
}
if (weekdays.includes('Fr')) {
nr |= 0x20;
}
if (weekdays.includes('Th')) {
nr |= 0x10;
}
if (weekdays.includes('We')) {
nr |= 0x08;
}
if (weekdays.includes('Tu')) {
nr |= 0x04;
}
if (weekdays.includes('Mo')) {
nr |= 0x02;
}
if (weekdays.includes('Su')) {
nr |= 0x01;
}
return [nr];
}
function convertDecimalValueTo4ByteHexArray(value) {
const hexValue = Number(value).toString(16).padStart(8, '0');
const chunk1 = hexValue.substr(0, 2);
const chunk2 = hexValue.substr(2, 2);
const chunk3 = hexValue.substr(4, 2);
const chunk4 = hexValue.substr(6);
return [chunk1, chunk2, chunk3, chunk4].map((hexVal) => parseInt(hexVal, 16));
}
function convertDecimalValueTo2ByteHexArray(value) {
const hexValue = Number(value).toString(16).padStart(4, '0');
const chunk1 = hexValue.substr(0, 2);
const chunk2 = hexValue.substr(2);
return [chunk1, chunk2].map((hexVal) => parseInt(hexVal, 16));
}
const fzLocal = {
SAS980SWT: {
cluster: 'manuSpecificTuya',
type: 'commandDataResponse',
convert: (model, msg, publish, options, meta) => {
const dpValue = tuya.firstDpValue(msg, meta, 'SAS980SWT');
const value = tuya.getDataValue(dpValue);
const dp = dpValue.dp;
switch (dp) {
case tuya.dataPoints.state: {
return {state: value ? 'ON': 'OFF'};
}
case 5: {
// Seems value is reported in tenths of liters
return {water_consumed: (value / 10.0).toFixed(2)};
}
case 7: {
return {battery: value};
}
case 10: {
let data = 'disabled';
if (value == 1) {
data = '24h';
} else if (value == 2) {
data = '48h';
} else if (value == 3) {
data = '72h';
}
return {weather_delay: data};
}
case 11: {
// value reported in seconds
return {timer_time_left: value / 60};
}
case 12: {
if (value === 0) return {timer_state: 'disabled'};
else if (value === 1) return {timer_state: 'active'};
else return {timer_state: 'enabled'};
}
case 15: {
// value reported in seconds
return {last_valve_open_duration: value / 60};
}
case 16: {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: Received DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
const tresult = {
cycle_timer_1: '',
cycle_timer_2: '',
cycle_timer_3: '',
cycle_timer_4: '',
};
for (let index = 0; index < 40; index += 12) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} decoding value for index ${index} = ${value.slice(index)}`);
const timer = convertRawToCycleTimer(value.slice(index));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.starttime} / ${timer.endtime} / ` +
`${timer.irrigationDuration} / ${timer.pauseDuration} / ` +
`${timer.weekdays} / ${timer.active}`);
if (timer.irrigationDuration > 0) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.starttime} / ${timer.endtime} / ` +
`${timer.irrigationDuration} / ${timer.pauseDuration} / ` +
`${timer.weekdays} / ${timer.active}`);
tresult['cycle_timer_' + (index / 12 + 1)] = timer.starttime +
' / ' + timer.endtime + ' / ' +
timer.irrigationDuration + ' / ' +
timer.pauseDuration + ' / ' +
timer.weekdays + ' / ' + timer.active;
}
}
return tresult;
}
case 17: {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: Received DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
const tresult = {
normal_schedule_timer_1: '',
normal_schedule_timer_2: '',
normal_schedule_timer_3: '',
normal_schedule_timer_4: '',
};
for (let index = 0; index < 40; index += 13) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} decoding value for index ${index} = ${value.slice(index)}`);
const timer = convertRawToTimer(value.slice(index));
if (timer.duration > 0) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.time} / ${timer.duration} / ${timer.weekdays} / ${timer.active}`);
tresult['normal_schedule_timer_' + (index / 13 + 1)] = timer.time +
' / ' + timer.duration +
' / ' + timer.weekdays +
' / ' + timer.active;
}
}
return tresult;
}
default: {
meta.logger.warn(`zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
};
const tzLocal = {
ZVG1_cycle_timer: {
key: ['cycle_timer_1', 'cycle_timer_2', 'cycle_timer_3', 'cycle_timer_4'],
convertSet: async (entity, key, value, meta) => {
let data = [0];
const footer = [0x64];
if (value == '') {
// delete
data.push(0x04);
data.push(parseInt(key.substr(-1)));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#16 sending raw data ${data}`);
await sendDataPointRaw(entity, 16, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
} else {
if ((meta.state.hasOwnProperty(key) && meta.state[key] == '') ||
!meta.state.hasOwnProperty(key)) {
data.push(0x03);
} else {
data.push(0x02);
data.push(parseInt(key.substr(-1)));
}
}
const tarray = value.replace(/ /g, '').split('/');
if (tarray.length < 4) {
throw new Error('Please check the format of the timer string');
}
if (tarray.length < 5) {
tarray.push('MoTuWeThFrSaSu');
}
if (tarray.length < 6) {
tarray.push('1');
}
const starttime = tarray[0];
const endtime = tarray[1];
const irrigationDuration = tarray[2];
const pauseDuration = tarray[3];
const weekdays = tarray[4];
const active = parseInt(tarray[5]);
if (!(active == 0 || active == 1)) {
throw new Error('Active value only 0 or 1 allowed');
}
data.push(active);
const weekdaysPart = convertWeekdaysTo1ByteHexArray(weekdays);
data = data.concat(weekdaysPart);
data = data.concat(convertTimeTo2ByteHexArray(starttime));
data = data.concat(convertTimeTo2ByteHexArray(endtime));
data = data.concat(convertDecimalValueTo2ByteHexArray(irrigationDuration));
data = data.concat(convertDecimalValueTo2ByteHexArray(pauseDuration));
data = data.concat(footer);
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#16 sending raw data ${data}`);
await sendDataPointRaw(entity, 16, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
},
},
ZVG1_normal_schedule_timer: {
key: ['normal_schedule_timer_1', 'normal_schedule_timer_2', 'normal_schedule_timer_3', 'normal_schedule_timer_4'],
convertSet: async (entity, key, value, meta) => {
let data = [0];
const footer = [0x07, 0xe6, 0x08, 0x01, 0x01];
if (value == '') {
// delete
data.push(0x04);
data.push(parseInt(key.substr(-1)));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#17 sending raw data ${data}`);
await sendDataPointRaw(entity, 17, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
} else {
if ((meta.state.hasOwnProperty(key) && meta.state[key] == '') || !meta.state.hasOwnProperty(key)) {
data.push(0x03);
} else {
data.push(0x02);
data.push(parseInt(key.substr(-1)));
}
}
const tarray = value.replace(/ /g, '').split('/');
if (tarray.length < 2) {
throw new Error('Please check the format of the timer string');
}
if (tarray.length < 3) {
tarray.push('MoTuWeThFrSaSu');
}
if (tarray.length < 4) {
tarray.push('1');
}
const time = tarray[0];
const duration = tarray[1];
const weekdays = tarray[2];
const active = parseInt(tarray[3]);
if (!(active == 0 || active == 1)) {
throw new Error('Active value only 0 or 1 allowed');
}
data = data.concat(convertTimeTo2ByteHexArray(time));
const durationPart = convertDecimalValueTo2ByteHexArray(duration);
data = data.concat(durationPart);
const weekdaysPart = convertWeekdaysTo1ByteHexArray(weekdays);
data = data.concat(weekdaysPart);
data = data.concat([64, active]);
data = data.concat(footer);
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#17 sending raw data ${data}`);
await sendDataPointRaw(entity, 17, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
},
},
};
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_akjefhj5'}],
model: 'SAS980SWT-7-Z01',
vendor: 'SASWELL',
description: 'Zigbee smart water valve',
fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report, fz.ignore_tuya_set_time],
toZigbee: [tz.legacy.tuya_switch_state, tz.legacy.ZVG1_weather_delay, tz.legacy.ZVG1_timer, tzLocal.ZVG1_cycle_timer, tzLocal.ZVG1_normal_schedule_timer],
onEvent: tuya.onEventSetLocalTime,
exposes: [e.switch().setAccess('state', ea.STATE_SET), e.battery(),
exposes.enum('weather_delay', ea.STATE_SET, ['disabled', '24h', '48h', '72h']),
exposes.enum('timer_state', ea.STATE, ['disabled', 'active', 'enabled']),
exposes.numeric('timer', ea.STATE_SET).withValueMin(0).withValueMax(60).withUnit('min')
.withDescription('Auto off after specific time'),
exposes.numeric('timer_time_left', ea.STATE).withUnit('min')
.withDescription('Auto off timer time left'),
exposes.numeric('last_valve_open_duration', ea.STATE).withUnit('min')
.withDescription('Time the valve was open when state on'),
exposes.numeric('water_consumed', ea.STATE).withUnit('l')
.withDescription('Liters of water consumed'),
exposes.text('cycle_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)')],
};
module.exports = definition; |
@ScratMan the timers are exposed as a |
OK. Is there something else to use as expose to get read & write access from HA ? |
Maybe we can use https://www.home-assistant.io/integrations/text.mqtt/, I will check it |
@ScratMan added in Koenkk/zigbee2mqtt#18114 Changes will be available in the dev branch in a few hours from now. (https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) |
Thanks, the timer fields can now be modified in HA. But unfortunately I'm getting an error when trying to control the valve with Dev branch: |
Can you provide the debug log of this? See https://www.zigbee2mqtt.io/guide/usage/debug.html on how to enable debug logging. |
Here it is, captured with Z2M release 1.32, error starting at 2023-07-03T19:08:41.200Z Edit2: fix incomplete log for good |
It seems issue comes from the change ece02be31b93040d75d030a3d637b18198555567 that removed a lot of tuya functions from the exports at the end. |
@ScratMan added the exports, let me know if you need any more. Changes will be available in the dev branch in a few hours from now. (https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html) |
Thanks, it works great. I updated my external converter to use exported legacy functions, and it works perfectly fine. Updated file for release Z2M 1.32.1+ with zigbee-herdsman-converters 15.35.0+const legacy = require('zigbee-herdsman-converters/lib/legacy');
const fz = {...require('zigbee-herdsman-converters/converters/fromZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').fromZigbee};
const tz = {...require('zigbee-herdsman-converters/converters/toZigbee'), legacy: require('zigbee-herdsman-converters/lib/legacy').toZigbee};
const tuya = require('zigbee-herdsman-converters/lib/tuya');
const exposes = require('zigbee-herdsman-converters/lib/exposes');
const reporting = require('zigbee-herdsman-converters/lib/reporting');
const extend = require('zigbee-herdsman-converters/lib/extend');
const e = exposes.presets;
const ea = exposes.access;
function convertRawToCycleTimer(value) {
let timernr = 0;
let starttime = '00:00';
let endtime = '00:00';
let irrigationDuration = 0;
let pauseDuration = 0;
let weekdays = 'once';
let timeractive = 0;
if (value.length > 11) {
timernr = value[1];
timeractive = value[2];
if (value[3] > 0) {
weekdays = (value[3] & 0x01 ? 'Su' : '') +
(value[3] & 0x02 ? 'Mo' : '') +
(value[3] & 0x04 ? 'Tu' : '') +
(value[3] & 0x08 ? 'We' : '') +
(value[3] & 0x10 ? 'Th' : '') +
(value[3] & 0x20 ? 'Fr' : '') +
(value[3] & 0x40 ? 'Sa' : '');
} else {
weekdays = 'once';
}
let minsincemidnight = value[4] * 256 + value[5];
starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
minsincemidnight = value[6] * 256 + value[7];
endtime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
irrigationDuration = value[8] * 256 + value[9];
pauseDuration = value[10] * 256 + value[11];
}
return {
timernr: timernr,
starttime: starttime,
endtime: endtime,
irrigationDuration: irrigationDuration,
pauseDuration: pauseDuration,
weekdays: weekdays,
active: timeractive,
};
}
function convertRawToTimer(value) {
let timernr = 0;
let starttime = '00:00';
let duration = 0;
let weekdays = 'once';
let timeractive = '';
if (value.length > 12) {
timernr = value[1];
const minsincemidnight = value[2] * 256 + value[3];
starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
duration = value[4] * 256 + value[5];
if (value[6] > 0) {
weekdays = (value[6] & 0x01 ? 'Su' : '') +
(value[6] & 0x02 ? 'Mo' : '') +
(value[6] & 0x04 ? 'Tu' : '') +
(value[6] & 0x08 ? 'We' : '') +
(value[6] & 0x10 ? 'Th' : '') +
(value[6] & 0x20 ? 'Fr' : '') +
(value[6] & 0x40 ? 'Sa' : '');
} else {
weekdays = 'once';
}
timeractive = value[8];
}
return {timernr: timernr, time: starttime, duration: duration, weekdays: weekdays, active: timeractive};
}
const fzLocal = {
SAS980SWT: {
cluster: 'manuSpecificTuya',
type: 'commandDataResponse',
convert: (model, msg, publish, options, meta) => {
const dpValue = legacy.firstDpValue(msg, meta, 'SAS980SWT');
const value = legacy.getDataValue(dpValue);
const dp = dpValue.dp;
switch (dp) {
case legacy.dataPoints.state: {
return {state: value ? 'ON': 'OFF'};
}
case 5: {
// Seems value is reported in tenths of liters
return {water_consumed: (value / 10.0).toFixed(2)};
}
case 7: {
return {battery: value};
}
case 10: {
let data = 'disabled';
if (value == 1) {
data = '24h';
} else if (value == 2) {
data = '48h';
} else if (value == 3) {
data = '72h';
}
return {weather_delay: data};
}
case 11: {
// value reported in seconds
return {timer_time_left: value / 60};
}
case 12: {
if (value === 0) return {timer_state: 'disabled'};
else if (value === 1) return {timer_state: 'active'};
else return {timer_state: 'enabled'};
}
case 15: {
// value reported in seconds
return {last_valve_open_duration: value / 60};
}
case 16: {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: Received DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
const tresult = {
cycle_timer_1: '',
cycle_timer_2: '',
cycle_timer_3: '',
cycle_timer_4: '',
};
for (let index = 0; index < 40; index += 12) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} decoding value for index ${index} = ${value.slice(index)}`);
const timer = convertRawToCycleTimer(value.slice(index));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.starttime} / ${timer.endtime} / ` +
`${timer.irrigationDuration} / ${timer.pauseDuration} / ` +
`${timer.weekdays} / ${timer.active}`);
if (timer.irrigationDuration > 0) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.starttime} / ${timer.endtime} / ` +
`${timer.irrigationDuration} / ${timer.pauseDuration} / ` +
`${timer.weekdays} / ${timer.active}`);
tresult['cycle_timer_' + (index / 12 + 1)] = timer.starttime +
' / ' + timer.endtime + ' / ' +
timer.irrigationDuration + ' / ' +
timer.pauseDuration + ' / ' +
timer.weekdays + ' / ' + timer.active;
}
}
return tresult;
}
case 17: {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: Received DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
const tresult = {
normal_schedule_timer_1: '',
normal_schedule_timer_2: '',
normal_schedule_timer_3: '',
normal_schedule_timer_4: '',
};
for (let index = 0; index < 40; index += 13) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} decoding value for index ${index} = ${value.slice(index)}`);
const timer = convertRawToTimer(value.slice(index));
if (timer.duration > 0) {
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: DP ` +
`#${dp} timer ${index} = ${timer.time} / ${timer.duration} / ${timer.weekdays} / ${timer.active}`);
tresult['normal_schedule_timer_' + (index / 13 + 1)] = timer.time +
' / ' + timer.duration +
' / ' + timer.weekdays +
' / ' + timer.active;
}
}
return tresult;
}
default: {
meta.logger.warn(`zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP ` +
`#${dp} with data ${JSON.stringify(dpValue)}`);
}
}
},
},
};
const tzLocal = {
ZVG1_cycle_timer: {
key: ['cycle_timer_1', 'cycle_timer_2', 'cycle_timer_3', 'cycle_timer_4'],
convertSet: async (entity, key, value, meta) => {
let data = [0];
const footer = [0x64];
if (value == '') {
// delete
data.push(0x04);
data.push(parseInt(key.substr(-1)));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#16 sending raw data ${data}`);
await legacy.sendDataPointRaw(entity, 16, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
} else {
if ((meta.state.hasOwnProperty(key) && meta.state[key] == '') ||
!meta.state.hasOwnProperty(key)) {
data.push(0x03);
} else {
data.push(0x02);
data.push(parseInt(key.substr(-1)));
}
}
const tarray = value.replace(/ /g, '').split('/');
if (tarray.length < 4) {
throw new Error('Please check the format of the timer string');
}
if (tarray.length < 5) {
tarray.push('MoTuWeThFrSaSu');
}
if (tarray.length < 6) {
tarray.push('1');
}
const starttime = tarray[0];
const endtime = tarray[1];
const irrigationDuration = tarray[2];
const pauseDuration = tarray[3];
const weekdays = tarray[4];
const active = parseInt(tarray[5]);
if (!(active == 0 || active == 1)) {
throw new Error('Active value only 0 or 1 allowed');
}
data.push(active);
const weekdaysPart = legacy.convertWeekdaysTo1ByteHexArray(weekdays);
data = data.concat(weekdaysPart);
data = data.concat(legacy.convertTimeTo2ByteHexArray(starttime));
data = data.concat(legacy.convertTimeTo2ByteHexArray(endtime));
data = data.concat(legacy.convertDecimalValueTo2ByteHexArray(irrigationDuration));
data = data.concat(legacy.convertDecimalValueTo2ByteHexArray(pauseDuration));
data = data.concat(footer);
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#16 sending raw data ${data}`);
await legacy.sendDataPointRaw(entity, 16, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
},
},
ZVG1_normal_schedule_timer: {
key: ['normal_schedule_timer_1', 'normal_schedule_timer_2', 'normal_schedule_timer_3', 'normal_schedule_timer_4'],
convertSet: async (entity, key, value, meta) => {
let data = [0];
const footer = [0x07, 0xe6, 0x08, 0x01, 0x01];
if (value == '') {
// delete
data.push(0x04);
data.push(parseInt(key.substr(-1)));
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#17 sending raw data ${data}`);
await legacy.sendDataPointRaw(entity, 17, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
} else {
if ((meta.state.hasOwnProperty(key) && meta.state[key] == '') || !meta.state.hasOwnProperty(key)) {
data.push(0x03);
} else {
data.push(0x02);
data.push(parseInt(key.substr(-1)));
}
}
const tarray = value.replace(/ /g, '').split('/');
if (tarray.length < 2) {
throw new Error('Please check the format of the timer string');
}
if (tarray.length < 3) {
tarray.push('MoTuWeThFrSaSu');
}
if (tarray.length < 4) {
tarray.push('1');
}
const time = tarray[0];
const duration = tarray[1];
const weekdays = tarray[2];
const active = parseInt(tarray[3]);
if (!(active == 0 || active == 1)) {
throw new Error('Active value only 0 or 1 allowed');
}
data = data.concat(legacy.convertTimeTo2ByteHexArray(time));
const durationPart = legacy.convertDecimalValueTo2ByteHexArray(duration);
data = data.concat(durationPart);
const weekdaysPart = legacy.convertWeekdaysTo1ByteHexArray(weekdays);
data = data.concat(weekdaysPart);
data = data.concat([64, active]);
data = data.concat(footer);
meta.logger.info(`zigbee-herdsman-converters:SaswellSAS980SWTValve: ${entity}, DP ` +
`#17 sending raw data ${data}`);
await legacy.sendDataPointRaw(entity, 17, data);
const ret = {state: {}};
ret['state'][key] = value;
return ret;
},
},
};
const definition = {
fingerprint: [{modelID: 'TS0601', manufacturerName: '_TZE200_akjefhj5'}],
model: 'SAS980SWT-7-Z01',
vendor: 'SASWELL',
description: 'Zigbee smart water valve',
fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report, fz.ignore_tuya_set_time],
toZigbee: [tz.legacy.tuya_switch_state, tz.legacy.ZVG1_weather_delay, tz.legacy.ZVG1_timer, tzLocal.ZVG1_cycle_timer, tzLocal.ZVG1_normal_schedule_timer],
onEvent: tuya.onEventSetLocalTime,
// onEvent: tuya.onEventSetTime,
exposes: [e.switch().setAccess('state', ea.STATE_SET), e.battery(),
exposes.enum('weather_delay', ea.STATE_SET, ['disabled', '24h', '48h', '72h']),
exposes.enum('timer_state', ea.STATE, ['disabled', 'active', 'enabled']),
exposes.numeric('timer', ea.STATE_SET).withValueMin(0).withValueMax(60).withUnit('min')
.withDescription('Auto off after specific time'),
exposes.numeric('timer_time_left', ea.STATE).withUnit('min')
.withDescription('Auto off timer time left'),
exposes.numeric('last_valve_open_duration', ea.STATE).withUnit('min')
.withDescription('Time the valve was open when state on'),
exposes.numeric('water_consumed', ea.STATE).withUnit('l')
.withDescription('Liters of water consumed'),
exposes.text('cycle_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('cycle_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 20:00 / 15 / 60 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'20:00 = end time ' +
'15 = irrigation duration in minutes ' +
'60 = pause duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_1', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_2', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_3', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)'),
exposes.text('normal_schedule_timer_4', ea.STATE_SET).withDescription('Format 08:00 / 15 / MoTuWeThFrSaSu / 1 (' +
'08:00 = start time ' +
'15 = duration in minutes ' +
'MoTu..= active weekdays ' +
'1 = deactivate timer with 0)')],
};
module.exports = definition; |
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days |
Sorry, I could not get free time to continue investigating on the code. I'm running on the 1.32.2, and I'm still facing an issue with the normal and cycle timers. Looking at the code, the binary values for the days are different between the weekdays to raw and raw to timer functions : function convertWeekdaysTo1ByteHexArray(weekdays: string) {
let nr = 0;
if (weekdays == 'once') {
return nr;
}
if (weekdays.includes('Mo')) {
nr |= 0x40;
}
if (weekdays.includes('Tu')) {
nr |= 0x20;
}
if (weekdays.includes('We')) {
nr |= 0x10;
}
if (weekdays.includes('Th')) {
nr |= 0x08;
}
if (weekdays.includes('Fr')) {
nr |= 0x04;
}
if (weekdays.includes('Sa')) {
nr |= 0x02;
}
if (weekdays.includes('Su')) {
nr |= 0x01;
}
return [nr];
}
function convertRawToTimer(value: any) {
let timernr = 0;
let starttime = '00:00';
let duration = 0;
let weekdays = 'once';
let timeractive = '';
if (value.length > 12) {
timernr = value[1];
const minsincemidnight = value[2] * 256 + value[3];
// @ts-ignore
starttime = String(parseInt(minsincemidnight / 60)).padStart(2, '0') + ':' + String(minsincemidnight % 60).padStart(2, '0');
duration = value[4] * 256 + value[5];
if (value[6] > 0) {
weekdays = (value[6] & 0x01 ? 'Su' : '') +
(value[6] & 0x02 ? 'Mo' : '') +
(value[6] & 0x04 ? 'Tu' : '') +
(value[6] & 0x08 ? 'We' : '') +
(value[6] & 0x10 ? 'Th' : '') +
(value[6] & 0x20 ? 'Fr' : '') +
(value[6] & 0x40 ? 'Sa' : '');
} else {
weekdays = 'once';
}
timeractive = value[8];
}
return {timernr: timernr, time: starttime, duration: duration, weekdays: weekdays, active: timeractive};
} But I don't know which one is correct. Is there a Tuya documentation somewhere to check for that ? Trial and error method would be time consuming, considering the local time is not set correctly, I need to set different timers at various time to see when they are triggered. |
When looking at the code I had previously with all legacy functions copied locally, I saw function convertWeekdaysTo1ByteHexArray(weekdays) {
let nr = 0;
if (weekdays == 'once') {
return nr;
}
if (weekdays.includes('Sa')) {
nr |= 0x40;
}
if (weekdays.includes('Fr')) {
nr |= 0x20;
}
if (weekdays.includes('Th')) {
nr |= 0x10;
}
if (weekdays.includes('We')) {
nr |= 0x08;
}
if (weekdays.includes('Tu')) {
nr |= 0x04;
}
if (weekdays.includes('Mo')) {
nr |= 0x02;
}
if (weekdays.includes('Su')) {
nr |= 0x01;
}
return [nr];
} Definitely need some Tuya documentation to fix this, I think. |
Still getting errors regarding time sync.
I removed the
without success. Timers are still out of sync. |
I investigated based on some Github issues I could find talking about I made a local converter to manage sync requests from device, adding function const sas980swt_set_time_request = {
cluster: 'manuSpecificTuya',
type: ['commandMcuSyncTime'],
convert: async (model, msg, publish, options, meta) => {
const hour = new Date().getHours();
if (hour < 9) {
meta.logger.info('SKIP commandMcuSyncTime message to SaswellSAS980SWTValve, to solve gap problem');
} else {
meta.logger.info('Send commandMcuSyncTime message to SaswellSAS980SWTValve');
const OneJanuary2000 = new Date('January 01, 2000 00:00:00 UTC+00:00').getTime();
const tzo = new Date().getTimezoneOffset() * 60; // Hours in minute with sign.
const gap = 8 * 3600;
const utcTime = Math.round(((new Date()).getTime() - OneJanuary2000) / 1000);
const localTime = utcTime - (new Date()).getTimezoneOffset() * 60;
// const currentTime = Math.round(((new Date()).getTime()) / 1000);
// const utcTime = currentTime - tzo - gap;
// const localTime = 0;
const endpoint = msg.endpoint;
meta.logger.info(`commandMcuSyncTime - SaswellSAS980SWTValve tzo = ${tzo}`);
meta.logger.info(`commandMcuSyncTime - SaswellSAS980SWTValve gap = ${gap}`);
meta.logger.info(`commandMcuSyncTime - SaswellSAS980SWTValve sent utc time = ${utcTime}`);
meta.logger.info(`commandMcuSyncTime - SaswellSAS980SWTValve sent local time = ${localTime}`);
const payload = {
payloadSize: 8,
payload: [
...legacy.convertDecimalValueTo4ByteHexArray(utcTime),
...legacy.convertDecimalValueTo4ByteHexArray(localTime),
],
};
await endpoint.command('manuSpecificTuya', 'mcuSyncTime', payload, {});
};
},
}; But strange thing is that when the sync request is received, I can see the timestamp from Z2M is my local time (UTC +2:00) (log example below with a sync request received at 17:24:38), and the Herdsman controller is at UTC time.
When converting the final timestamp I'll try some other computing methods to see if I can alter this 10:08:53 offset by time sync function. |
Hi, i've tried your converter on a _TZE200_81isopgh device in order to replace the current RTX implementation because I was having a bug with the valve open time which get changed to 10min after a ON/OFF sequence. The "bug" can be reproduced using the RTX & this implementation too.
Here are the details of the payload for each action: Update config - SET timer to 60 :
toggle ON (60 min OK)
toggle OFF (60min is still displayed OK):
toggle ON (60min before toggle ON, reset to 10min after toggle ON -> timer_time_left=10 instead of timer_time_left=60):
toggle OFF :
It looks like this is a common issue to most SAS980SWT-7-Z01 / RTX valves (found some topics regarding this specific issue). People are mostly triggering the ON/OFF state 6x time if they want to let it open for 60 minutes 😵💫 I'm not familar with the way converters are working... Thanks :) |
I think it's the normal behaviour of the valve, it resets to the default 10mn after switching off, but it doesn't report the set time. In Smart Life app you can change the timer value and it will revert to 10 once valve is stopped. You need to adapt your usage by first setting timer value each time before toggling the valve on. |
Too bad :(
I want to 😀 Is there any example on how to do it using the current converter (some hints?) ? Or maybe using some kind of event ? I would be happy to add this as an option (like, force timer after OFF) to make it available for everyone of course! |
For example, I'm using this valve combined with the HASmartIrrigation integration in Home Assistant. alias: Arrosage automatique du potager
description: >-
Démarre l'arrosage du potager. Se base sur le composant SmartIrrigation pour
obtenir la durée d'arrosage journalier en secondes.
trigger:
- platform: time
at: "19:59:00"
condition:
- condition: numeric_state
entity_id: sensor.smart_irrigation_daily_adjusted_run_time
above: "0"
action:
- service: number.set_value
data:
value: >-
{{ (((states("sensor.smart_irrigation_daily_adjusted_run_time")| float)/
60.0) | round(0, 'ceil')) | float}}
target:
entity_id: number.vanne_arrosage_potager_timer
- delay:
hours: 0
minutes: 1
seconds: 0
milliseconds: 0
- service: persistent_notification.create
data:
message: >-
Arrosage pendant {{
(states("sensor.smart_irrigation_daily_adjusted_run_time")| int / 60) |
round(0, 'ceil') | int}} mn
- type: turn_on
device_id: bf95e862526c3583c794c369bc4c296c
entity_id: switch.vanne_arrosage_potager_switch
domain: switch
- wait_for_trigger:
- platform: state
entity_id:
- switch.vanne_arrosage_potager_switch
from: "on"
to: "off"
timeout:
hours: 1
minutes: 0
seconds: 0
milliseconds: 0
- service: smart_irrigation.smart_irrigation_reset_bucket
data: {}
mode: single
max: 10
|
Thank you for your feedback. I'm using homebridge on my side, I was more talking about an option that could added to the converter itself. |
Not sure this would be possible, the converter doesn't store data, it only translates raw data to human ready data at communication level. |
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days |
The following post is from Scratman, I would like to have this device natively supported, as the generic RTX ZVG1 controller doesn't work well with my Saswell SAS980SWt-7-Z01 valve.
Thanks a lot!
I found this issue by looking for some infos on my device, willing to add it to Zigbee2MQTT.
I own two Saswell SAS980SWt-7-Z01 smart valves reported as TS0601 / _TZE200_akjefhj5.
I first tried to use it with ZHA, but I couldn't get the corresponding entity in Home Assistant, so I decided to buy a second controller and add Zigbee2MQTT to my system to add these valves.
In the converters, this fingerprint is considered as a RTX ZVG1 valve, but when looking at the datapoint available in Tuya IOT app, I only have the following data point
When adding the device directly to Zigbee2MQTT, the additional datapoints from ZVG1 doesn't work (weather delay, cycle timers, etc...) but these are not an issue.
The main problem is that the water consumed information from DP 5 is wrong. The ZVG1 converter considers the data as fl oz and divides the value by 33.8140226, while my valves seem to report tenths of liters. I could see the following info in Tuya IOT app:
So I made my own external converter and added it to my configuration I'm adding below, and after removing the valve and adding it back to the network, it's now recognized as a SASWELL SAS980SWT-7-Z01
saswell_sas980swt-7-z01.js
Now, it seems the water consumed parameter is reported correctly in liters. I could set the timer to 1 minute, when switched ON the valve stopped after 1 minute and the water consumed entity reported 6.00 L... and the water level in the bucket was just below the 6 L graduation.
@jacekpaszkowski : do your device reports a _TZE200_akjefhj5 model with working DP 10, 16 and 17; and correct water consumed volume with the divider by 33.8140226 ?
Or should I create a PR to add my converter for _TZE200_akjefhj5 ?
Thanks
Originally posted by @ScratMan in #2795 (comment)
The text was updated successfully, but these errors were encountered: