Skip to content
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

Closed
rodtrip2 opened this issue May 7, 2023 · 35 comments
Closed

Add SAS980SWT-7-Z01 valve please #5727

rodtrip2 opened this issue May 7, 2023 · 35 comments
Labels

Comments

@rodtrip2
Copy link

rodtrip2 commented May 7, 2023

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!

          Hello,

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

  • 1: "switch",
  • 5: "water_once", # last irrigation volume
  • 7: "battery", # indicates the level of the battery in % in Tuya logs
  • 11: "irrigation_time", # Corresponds to remaining time
  • 12: "work_state", # idle or manual
  • 15: "once_using_time", # last irrigation duration

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:

image

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

const fz = require('zigbee-herdsman-converters/converters/fromZigbee');
const tz = require('zigbee-herdsman-converters/converters/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;

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 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};
            }
            default: {
                meta.logger.warn(`zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP ` +
                    `#${dp} with data ${JSON.stringify(dpValue)}`);
            }
            }
        },
    },
};

const tzLocal = {
    SAS980SWT_timer: {
        key: ['timer'],
        convertSet: async (entity, key, value, meta) => {
            // input in minutes with maximum of 600 minutes (equals 10 hours)
            const timer = 60 * Math.abs(Math.min(value, 600));
            // sendTuyaDataPoint* functions take care of converting the data to proper format
            await tuya.sendDataPointValue(entity, 11, timer, 'dataRequest', 1);
            return {state: {timer: value}};
        },
    },
};

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.tuya_switch_state, tzLocal.SAS980SWT_timer],
    exposes: [e.switch().setAccess('state', ea.STATE_SET), e.battery(),
        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')],
};

module.exports = definition;

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)

@Koenkk
Copy link
Owner

Koenkk commented May 7, 2023

I see the ZVG1 also supports features like cycle_timer_1, does the SAS980SWt-7-Z01 not support this?

@ScratMan
Copy link
Contributor

ScratMan commented May 7, 2023

In Tuya IOT Cloud Development platform, the debug section shows the following data points:

  • 1: "switch", # the switch state
  • 3: "flow_state", # No data in tuya logs
  • 4: "failure_to_report", # No data in tuya logs
  • 5: "water_once", # last irrigation volume
  • 7: "battery", # indicates the level of the battery in % in Tuya logs
  • 8: "battery_state", # No data in tuya logs
  • 9: "accumulated_usage_time", # No data in tuya logs
  • 10: "weather_delay", # disabled, 24h, etc...
  • 11: "irrigation_time", # Corresponds to remaining time
  • 12: "work_state", # idle, auto or manual
  • 13: "smart_weather", # sunny, cloudy, etc...
  • 14: "smart_weather_switch", # No data in tuya logs
  • 15: "once_using_time", # last irrigation duration
  • 16: "cycle_irrigation", # a string of ascii characters when cycle timer is set
  • 17: "normal_timer", # a string of ascii characters when normal timer is set

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.
But I tried to program a daily cycle & soak timer in Smart Life app from 19h00 to 21h00 (2 hours) with 2 mins cycle and 5 minutes soaking, but the valve reports a trigger at 14h59 with 34 minutes duration, so exactly the 2 minutes on cycles repeated every 7 minutes during 2 hours (I can see the on/off cycles in the Tuya IOT logs). So the program seems to work, but it looks like the valve is not on time... 😞
Don't know how to solve this time shift.

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.
I'll try to provide some logs.

@rodtrip2
Copy link
Author

rodtrip2 commented May 7, 2023

I see the ZVG1 also supports features like cycle_timer_1, does the SAS980SWt-7-Z01 not support this?

It has also cycle timers capabilities, which I don't use.
The main problem is that the 'water_consumed' doesn't come out right with the ZVG1 converter. Also I'm often having DP6 errors :
rtxzvg1valve: not recognized dp #6 with data {"dp":6,"datatype":2,"data":{"type":"buffer","data":[0,0,0,0]}}

@ScratMan
Copy link
Contributor

ScratMan commented May 7, 2023

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 :

2023-05-07 10:23:49 | Report | Normal timer | AA== | device itself |  
2023-05-07 10:23:43 | Report | Cycle irrigation | AA== | device itself |  
2023-05-07 10:23:38 | Report | Once using time | 180s | device itself |  
2023-05-07 10:23:37 | Report | Cycle irrigation | AAEBfwMWA1IABgAGZA== | device itself |  
2023-05-07 10:23:37 | Report | Cycle irrigation | AAEBEAPAA/wAAwAKZAIBfwMWA1IABgAGZA== | device itself |  
2023-05-07 10:19:45 | Report | Battery | 50% | device itself


Then I set the follwowing timers :

cycle_timer_1: 13:00 / 14:00 / 5 / 10 / MoTuWeThFr / 1
cycle_timer_2: 21:00 / 21:30 / 2 / 13 / SaSu / 1
normal_timer_1: 08:00 / 10 / MoWeFrSa / 1
normal_timer_2: 12:17 / 25 / FrSaSu / 1

And this is the corresponding logs :

2023-05-07 10:30:24 | Report | Normal timer | AAEB4AAKamQBB+cFBwECAuEAGWFkAQfnBQcB | device itself |  
2023-05-07 10:29:38 | Report | Normal timer | AAEB4AAKamQBB+cFBwE= | device itself |  
2023-05-07 10:28:52 | Report | Cycle irrigation | AAEBPgMMA0gABQAKZAIBQQTsBQoAAgANZA== | device itself |  
2023-05-07 10:28:11 | Report | Cycle irrigation | AAEBPgMMA0gABQAKZA== | device itself |  


Now, let's add the same timers to my other instance connected through Z2M.
I first delete the existing timers, then set the same timers as on the Smart Life app, and grab the log below (as there is no "send" button I pressed Enter after each timer, then click on the next one).

log.txt

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 AAEBPgMMA0gABQAKZA== when setting first cycle timer, then AAEBPgMMA0gABQAKZAIBQQTsBQoAAgANZA== when adding the second cycle timer while in Z2M I only get data with either, 3, 13 or 14 bytes.

In the log I could also see the messages that may explain the time shift I have with the valve connected to Tuya gateway:

2023-05-06T06:43:12.084Z zigbee-herdsman:controller:endpoint Command 0xb4e3f9fffe0c6ff2/1 manuSpecificTuya.mcuSyncTime({"payloadSize":8,"payload":[100,85,247,0,100,86,19,32]}, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":false,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})
2023-05-06T06:45:45.597Z zigbee-herdsman:controller:log Received 'zcl' data '{"frame":{"Header":{"frameControl":{"frameType":1,"manufacturerSpecific":false,"direction":1,"disableDefaultResponse":true,"reservedBits":0},"transactionSequenceNumber":65,"manufacturerCode":null,"commandIdentifier":36},"Payload":{"payloadSize":41734},"Command":{"ID":36,"parameters":[{"name":"payloadSize","type":33}],"name":"mcuSyncTime"}},"address":40477,"endpoint":1,"linkquality":252,"groupID":0,"wasBroadcast":false,"destinationEndpoint":1}'
Zigbee2MQTT:debug 2023-05-06 08:45:45: Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":41734}' from endpoint 1 with groupID 0
Zigbee2MQTT:debug 2023-05-06 08:45:45: No converter available for 'SAS980SWT-7-Z01' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":41734}'
2023-05-06T06:45:45.606Z zigbee-herdsman:controller:endpoint Command 0xb4e3f9fffe0c6ff2/1 manuSpecificTuya.mcuSyncTime({"payloadSize":8,"payload":[100,85,247,154,100,86,19,186]}, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":false,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})

So it seems there is time synchronization feature for the timers, but the way to use it is missing in the converters.

@github-actions
Copy link
Contributor

github-actions bot commented Jun 7, 2023

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

@github-actions github-actions bot added the stale label Jun 7, 2023
@ScratMan
Copy link
Contributor

ScratMan commented Jun 7, 2023

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 had to revert Z2M to 1.30.4

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.
@Koenkk , is there some documentation on how to convert external converters to new version, please ?

@github-actions github-actions bot removed the stale label Jun 8, 2023
@Koenkk
Copy link
Owner

Koenkk commented Jun 8, 2023

is there some documentation on how to convert external converters to new version, please ?

I suggest to make the toZigbee and fromZigbee empty, once you've done that add them back one-by-one to see which one causes the error.

@ScratMan
Copy link
Contributor

ScratMan commented Jun 8, 2023

Thanks for the advice. The issue was caused by
tz.tuya_switch_state I had to add legacy also for Tuya.

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.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 legacy be removed in the future?

@Koenkk
Copy link
Owner

Koenkk commented Jun 8, 2023

Question, will the legacy be removed in the future?

Maybe, but not any time soon.

@ScratMan
Copy link
Contributor

ScratMan commented Jun 9, 2023

Thanks.

I tried to work on the timers, but it seems it's not working. I first tried to add the commandMcuSyncTime, I changed the fromZigbee adding the ignore_tuya_set_time and added the onEvent declaration, I got inspired by some other Tuya devices with timers.

fromZigbee: [fzLocal.SAS980SWT, fz.ignore_basic_report, fz.ignore_tuya_set_time],
onEvent: tuya.onEventSetLocalTime,

As the convertRawToTimer and convertRawToCycleTimer has moved in legacy also, I updated the lines 62 and 81 in declaration of the fzLocal.SAS980SWT :

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:

2023-06-09 11:29:40 Exception while calling fromZigbee converter: Cannot read properties of undefined (reading 'convertRawToCycleTimer')}
 2023-06-09 11:29:40 Exception while calling fromZigbee converter: Cannot read properties of undefined (reading 'convertRawToTimer')}

The full log...

Debug 2023-06-09 11:35:38Received Zigbee message from 'Vanne remplissage piscine', type 'read', cluster 'genTime', data '["localTime"]' from endpoint 1 with groupID 0
Debug 2023-06-09 11:35:42Received Zigbee message from 'Coordinator', type 'commandNotification', cluster 'greenPower', data '{"data":[25,46,2,11,254,0],"type":"Buffer"}' from endpoint 242 with groupID null, ignoring since it is from coordinator
Debug 2023-06-09 11:35:42Received Zigbee message from 'Vanne remplissage piscine', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0],"type":"Buffer"},"datatype":1,"dp":1}],"seq":768}' from endpoint 1 with groupID 0
Info 2023-06-09 11:35:42MQTT publish: topic 'zigbee2mqtt/Vanne remplissage piscine', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_valve_open_duration":10,"linkquality":255,"normal_schedule_timer_1":"11:27 / 5 / Fr / 1","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":1,"timer_state":"enabled","timer_time_left":1,"water_consumed":"0.40","weather_delay":"disabled"}'
Debug 2023-06-09 11:35:42Received Zigbee message from 'Vanne remplissage piscine', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[2],"type":"Buffer"},"datatype":4,"dp":12}],"seq":1024}' from endpoint 1 with groupID 0
Info 2023-06-09 11:35:42MQTT publish: topic 'zigbee2mqtt/Vanne remplissage piscine', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_valve_open_duration":10,"linkquality":255,"normal_schedule_timer_1":"11:27 / 5 / Fr / 1","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":1,"timer_state":"enabled","timer_time_left":1,"water_consumed":"0.40","weather_delay":"disabled"}'
Debug 2023-06-09 11:35:43Received Zigbee message from 'Vanne remplissage piscine', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,100],"type":"Buffer"},"datatype":2,"dp":7}],"seq":1280}' from endpoint 1 with groupID 0
Info 2023-06-09 11:35:43MQTT publish: topic 'zigbee2mqtt/Vanne remplissage piscine', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_valve_open_duration":10,"linkquality":255,"normal_schedule_timer_1":"11:27 / 5 / Fr / 1","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":1,"timer_state":"enabled","timer_time_left":1,"water_consumed":"0.40","weather_delay":"disabled"}'
Debug 2023-06-09 11:35:43Received Zigbee message from 'Vanne remplissage piscine', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0],"type":"Buffer"},"datatype":0,"dp":16}],"seq":1536}' from endpoint 1 with groupID 0
Error 2023-06-09 11:35:43Exception while calling fromZigbee converter: Cannot read properties of undefined (reading 'convertRawToCycleTimer')}
Debug 2023-06-09 11:35:43TypeError: Cannot read properties of undefined (reading 'convertRawToCycleTimer') at Object.convert (/app/data/extension/externally-loaded.js:62:47) at Receive.onDeviceMessage (/app/lib/extension/receive.ts:150:51) at EventEmitter.emit (node:events:525:35) at EventBus.emitDeviceMessage (/app/lib/eventBus.ts:102:22) at Controller.<anonymous> (/app/lib/zigbee.ts:106:27) at Controller.emit (node:events:513:28) at Controller.selfAndDeviceEmit (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:515:14) at Controller.onZclOrRawData (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:726:18) at EZSPAdapter.<anonymous> (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:144:70) at EZSPAdapter.emit (node:events:513:28)
Debug 2023-06-09 11:35:43Received Zigbee message from 'Vanne remplissage piscine', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0],"type":"Buffer"},"datatype":0,"dp":17}],"seq":1792}' from endpoint 1 with groupID 0
Error 2023-06-09 11:35:43Exception while calling fromZigbee converter: Cannot read properties of undefined (reading 'convertRawToTimer')}
Debug 2023-06-09 11:35:43TypeError: Cannot read properties of undefined (reading 'convertRawToTimer') at Object.convert (/app/data/extension/externally-loaded.js:81:47) at Receive.onDeviceMessage (/app/lib/extension/receive.ts:150:51) at EventEmitter.emit (node:events:525:35) at EventBus.emitDeviceMessage (/app/lib/eventBus.ts:102:22) at Controller.<anonymous> (/app/lib/zigbee.ts:106:27) at Controller.emit (node:events:513:28) at Controller.selfAndDeviceEmit (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:515:14) at Controller.onZclOrRawData (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:726:18) at EZSPAdapter.<anonymous> (/app/node_modules/zigbee-herdsman/src/controller/controller.ts:144:70) at EZSPAdapter.emit (node:events:513:28)
Debug 2023-06-09 11:35:43Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:35:48Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:35:53Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:35:59Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:04Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:09Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:14Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:20Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:25Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":0}' from endpoint 1 with groupID 0
Debug 2023-06-09 11:36:30Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":4608}' from endpoint 1 with groupID 0

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;

@ScratMan
Copy link
Contributor

ScratMan commented Jun 9, 2023

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.
Unfortunately, it seems the time is not correct, I set a cycle timer from 16:00 to 23:00, and it didn't start at 16:00 😞

The js file as txt :
saswell_sas980swt-7-z01.txt

@ScratMan
Copy link
Contributor

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.
And as seen in Tuya Smart Life app, there is a 6 hours offset Vs the local time.

@ScratMan
Copy link
Contributor

I managed to fix all issues with timers:

  • Days were mixed between fz and tz, Mo becoming Sa...
  • Reversed days ordering in fz
  • Mismatch in cycle timers data length between encoding and writing, corrupting cycle timers 2 to 4

Now all normal and cycle schedulers are working fine; except for the time offset of 360 minutes.
Also the timers are only editable in ZigBee2MQTT Expose interface, I can't edit them in HA. @Koenkk is it normal?

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;

@Koenkk
Copy link
Owner

Koenkk commented Jun 20, 2023

@ScratMan the timers are exposed as a text and are therefore not editable from HA, so that is expected.

@ScratMan
Copy link
Contributor

@ScratMan the timers are exposed as a text and are therefore not editable from HA, so that is expected.

OK. Is there something else to use as expose to get read & write access from HA ?

@Koenkk
Copy link
Owner

Koenkk commented Jun 21, 2023

Maybe we can use https://www.home-assistant.io/integrations/text.mqtt/, I will check it

@Koenkk
Copy link
Owner

Koenkk commented Jun 25, 2023

@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)

@ScratMan
Copy link
Contributor

ScratMan commented Jun 26, 2023

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:
2023-06-26 11:44:13 Exception while calling fromZigbee converter: tuya.firstDpValue is not a function}

@Koenkk
Copy link
Owner

Koenkk commented Jun 26, 2023

Can you provide the debug log of this?

See https://www.zigbee2mqtt.io/guide/usage/debug.html on how to enable debug logging.

@ScratMan
Copy link
Contributor

ScratMan commented Jul 3, 2023

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
log.txt

Edit2: fix incomplete log for good

@ScratMan
Copy link
Contributor

ScratMan commented Jul 3, 2023

It seems issue comes from the change ece02be31b93040d75d030a3d637b18198555567 that removed a lot of tuya functions from the exports at the end.
And in the meantime, the legacy didn't expose these functions either. So they disappeared and my external converter can't access them now 😢

Koenkk added a commit that referenced this issue Jul 4, 2023
@Koenkk
Copy link
Owner

Koenkk commented Jul 4, 2023

@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)

@ScratMan
Copy link
Contributor

ScratMan commented Jul 4, 2023

@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.
I kept the convertRawToCycleTimer and convertRawToTimer functions locally to keep the days ordering more user friendly, as the functions from legacy are reversing the weekdays. I'll make a PR for this.

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;

@github-actions
Copy link
Contributor

github-actions bot commented Aug 4, 2023

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

@github-actions github-actions bot added the stale label Aug 4, 2023
@ScratMan
Copy link
Contributor

ScratMan commented Aug 4, 2023

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.
When setting week days, the returned value from the valve is messed up, setting MoTu will be changed to FrSa.

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.

@ScratMan
Copy link
Contributor

ScratMan commented Aug 4, 2023

When looking at the code I had previously with all legacy functions copied locally, I saw convertWeekdaysTo1ByteHexArray used the same binary values than the convertRawToTimer function:

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.

@ScratMan
Copy link
Contributor

ScratMan commented Aug 4, 2023

Still getting errors regarding time sync.

2023-08-04T22:04:07.918Z zigbee-herdsman:controller:log Received 'zcl' data '{"frame":{"Header":{"frameControl":{"frameType":1,"manufacturerSpecific":false,"direction":1,"disableDefaultResponse":true,"reservedBits":0},"transactionSequenceNumber":96,"manufacturerCode":null,"commandIdentifier":36},"Payload":{"payloadSize":18691},"Command":{"ID":36,"parameters":[{"name":"payloadSize","type":33}],"name":"mcuSyncTime"}},"address":42405,"endpoint":1,"linkquality":240,"groupID":0,"wasBroadcast":false,"destinationEndpoint":1}'
Zigbee2MQTT:debug 2023-08-05 00:04:07: Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":18691}' from endpoint 1 with groupID 0
Zigbee2MQTT:debug 2023-08-05 00:04:07: No converter available for 'SAS980SWT-7-Z01' with cluster 'manuSpecificTuya' and type 'commandMcuSyncTime' and data '{"payloadSize":18691}'
2023-08-04T22:04:07.923Z zigbee-herdsman:controller:endpoint Command 0xb4e3f9fffe0c6ff2/1 manuSpecificTuya.mcuSyncTime({"payloadSize":8,"payload":[44,96,50,88,44,96,78,120]}, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":false,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})

I removed the fz.ignore_tuya_set_time and added

onEvent: tuya.onEventSetTime,
configure: tuya.configureMagicPacket,

without success. Timers are still out of sync.

@github-actions github-actions bot removed the stale label Aug 5, 2023
@ScratMan
Copy link
Contributor

ScratMan commented Aug 6, 2023

I investigated based on some Github issues I could find talking about commandMcuSyncTime, like here or here

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.

2023-08-06T15:24:38.138959605Z 2023-08-06T15:24:38.138Z zigbee-herdsman:controller:log Received 'zcl' data '{"frame":{"Header":{"frameControl":{"frameType":1,"manufacturerSpecific":false,"direction":1,"disableDefaultResponse":true,"reservedBits":0},"transactionSequenceNumber":40,"manufacturerCode":null,"commandIdentifier":36},"Payload":{"payloadSize":54275},"Command":{"ID":36,"parameters":[{"name":"payloadSize","type":33}],"name":"mcuSyncTime"}},"address":42405,"endpoint":1,"linkquality":252,"groupID":0,"wasBroadcast":false,"destinationEndpoint":1}'
2023-08-06T15:24:38.139515358Z Zigbee2MQTT:debug 2023-08-06 17:24:38: Received Zigbee message from 'Vanne remplissage piscine', type 'commandMcuSyncTime', cluster 'manuSpecificTuya', data '{"payloadSize":54275}' from endpoint 1 with groupID 0
2023-08-06T15:24:38.140699401Z Zigbee2MQTT:info  2023-08-06 17:24:38: Send commandMcuSyncTime message to SaswellSAS980SWTValve
2023-08-06T15:24:38.141633058Z Zigbee2MQTT:info  2023-08-06 17:24:38: commandMcuSyncTime - SaswellSAS980SWTValve tzo = -7200
2023-08-06T15:24:38.142349365Z Zigbee2MQTT:info  2023-08-06 17:24:38: commandMcuSyncTime - SaswellSAS980SWTValve gap = 28800
2023-08-06T15:24:38.143174745Z Zigbee2MQTT:info  2023-08-06 17:24:38: commandMcuSyncTime - SaswellSAS980SWTValve sent utc time = 744650678
2023-08-06T15:24:38.143844997Z Zigbee2MQTT:info  2023-08-06 17:24:38: commandMcuSyncTime - SaswellSAS980SWTValve sent local time = 744657878
2023-08-06T15:24:38.145704663Z 2023-08-06T15:24:38.145Z zigbee-herdsman:controller:endpoint Command 0xb4e3f9fffe0c6ff2/1 manuSpecificTuya.mcuSyncTime({"payloadSize":8,"payload":[44,98,119,182,44,98,147,214]}, {"sendWhen":"immediate","timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":false,"direction":0,"srcEndpoint":null,"reservedBits":0,"manufacturerCode":null,"transactionSequenceNumber":null,"writeUndiv":false})

When converting the final timestamp utcTime sent in the payload, using Python method datetime.utcfromtimestamp(utcTime) it gives me 1993-08-06 15:24:38, so except the date shift (August 6th was Friday while it's Sunday in 2023), the time seems OK; and same conversion on localTime it gives the same date and 17:24:38, so the local time in France the sync request was received.
But when setting the normal timer 1 at 08:05 everyday of the week, the valve triggered at 18:13:53.

I'll try some other computing methods to see if I can alter this 10:08:53 offset by time sync function.

@hadjedjvincent
Copy link

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 is how to reproduce:

  1. Set a timer to 60 min
  2. Toggle ON (timer_time_left is correctly set to 60)
  3. Toggle OFF (timer_time_left is correctly set to 60)
  4. Toggle ON (timer_time_left is switching its value to 10)

Here are the details of the payload for each action:

Update config - SET timer to 60 :

debug 2023-08-17 23:22:06Received MQTT message on 'zigbee2mqtt/Arrosage Jardin/set' with data '{"timer":60}'
debug 2023-08-17 23:22:06Publishing 'set' 'timer' to 'Arrosage Jardin'
info 2023-08-17 23:22:06MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307266483,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"enabled","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:07Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,14,16],"type":"Buffer"},"datatype":2,"dp":11}],"seq":53248}' from endpoint 1 with groupID null
info 2023-08-17 23:22:07MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307327357,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'

toggle ON (60 min OK)

debug 2023-08-17 23:22:28Received MQTT message on 'zigbee2mqtt/Arrosage Jardin/set' with data '{"state":"ON"}'
debug 2023-08-17 23:22:28Publishing 'set' 'state' to 'Arrosage Jardin'
info 2023-08-17 23:22:28MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307327357,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:28Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[1],"type":"Buffer"},"datatype":1,"dp":1}],"seq":53504}' from endpoint 1 with groupID null
info 2023-08-17 23:22:28MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307348901,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:29Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[1],"type":"Buffer"},"datatype":4,"dp":12}],"seq":53760}' from endpoint 1 with groupID null
info 2023-08-17 23:22:29MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307349029,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:29Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,14,16],"type":"Buffer"},"datatype":2,"dp":11}],"seq":54016}' from endpoint 1 with groupID null
info 2023-08-17 23:22:29MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307349148,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'

toggle OFF (60min is still displayed OK):

debug 2023-08-17 23:22:45Received MQTT message on 'zigbee2mqtt/Arrosage Jardin/set' with data '{"state":"OFF"}'
debug 2023-08-17 23:22:45Publishing 'set' 'state' to 'Arrosage Jardin'
info 2023-08-17 23:22:45MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307349148,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:46Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0],"type":"Buffer"},"datatype":1,"dp":1}],"seq":54272}' from endpoint 1 with groupID null
info 2023-08-17 23:22:46MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366257,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:46Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":15}],"seq":54528}' from endpoint 1 with groupID null
info 2023-08-17 23:22:46MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366379,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:46Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":6}],"seq":54784}' from endpoint 1 with groupID null
warning 2023-08-17 23:22:46zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP #6 with data {"dp":6,"datatype":2,"data":{"type":"Buffer","data":[0,0,0,0]}}
info 2023-08-17 23:22:46MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366505,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:46Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":5}],"seq":55040}' from endpoint 1 with groupID null
info 2023-08-17 23:22:46MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366623,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:22:46Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[2],"type":"Buffer"},"datatype":4,"dp":12}],"seq":55296}' from endpoint 1 with groupID null
info 2023-08-17 23:22:46MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366733,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'

toggle ON (60min before toggle ON, reset to 10min after toggle ON -> timer_time_left=10 instead of timer_time_left=60):

debug 2023-08-17 23:23:03Received MQTT message on 'zigbee2mqtt/Arrosage Jardin/set' with data '{"state":"ON"}'
debug 2023-08-17 23:23:03Publishing 'set' 'state' to 'Arrosage Jardin'
info 2023-08-17 23:23:03MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307366733,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:05Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[1],"type":"Buffer"},"datatype":1,"dp":1}],"seq":55552}' from endpoint 1 with groupID null
info 2023-08-17 23:23:05MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307385153,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"enabled","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:05Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[1],"type":"Buffer"},"datatype":4,"dp":12}],"seq":55808}' from endpoint 1 with groupID null
info 2023-08-17 23:23:05MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307385281,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"active","timer_time_left":60,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:05Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,2,88],"type":"Buffer"},"datatype":2,"dp":11}],"seq":56064}' from endpoint 1 with groupID null
info 2023-08-17 23:23:05MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307385403,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"ON","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'

toggle OFF :

debug 2023-08-17 23:23:49Received MQTT message on 'zigbee2mqtt/Arrosage Jardin/set' with data '{"state":"OFF"}'
debug 2023-08-17 23:23:49Publishing 'set' 'state' to 'Arrosage Jardin'
info 2023-08-17 23:23:49MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307385403,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:49Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0],"type":"Buffer"},"datatype":1,"dp":1}],"seq":56320}' from endpoint 1 with groupID null
info 2023-08-17 23:23:49MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307429719,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:49Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":15}],"seq":56576}' from endpoint 1 with groupID null
info 2023-08-17 23:23:49MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307429842,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:49Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":6}],"seq":56832}' from endpoint 1 with groupID null
warning 2023-08-17 23:23:49zigbee-herdsman-converters:SaswellSAS980SWTValve: NOT RECOGNIZED DP #6 with data {"dp":6,"datatype":2,"data":{"type":"Buffer","data":[0,0,0,0]}}
info 2023-08-17 23:23:49MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307429956,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:50Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[0,0,0,0],"type":"Buffer"},"datatype":2,"dp":5}],"seq":57088}' from endpoint 1 with groupID null
info 2023-08-17 23:23:50MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307430086,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"active","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'
debug 2023-08-17 23:23:50Received Zigbee message from 'Arrosage Jardin', type 'commandDataResponse', cluster 'manuSpecificTuya', data '{"dpValues":[{"data":{"data":[2],"type":"Buffer"},"datatype":4,"dp":12}],"seq":57344}' from endpoint 1 with groupID null
info 2023-08-17 23:23:50MQTT publish: topic 'zigbee2mqtt/Arrosage Jardin', payload '{"battery":100,"cycle_timer_1":"","cycle_timer_2":"","cycle_timer_3":"","cycle_timer_4":"","last_seen":1692307430197,"last_valve_open_duration":0,"linkquality":255,"normal_schedule_timer_1":"","normal_schedule_timer_2":"","normal_schedule_timer_3":"","normal_schedule_timer_4":"","state":"OFF","timer":60,"timer_state":"enabled","timer_time_left":10,"water_consumed":"0.00","weather_delay":"disabled"}'

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...
Is there a way to force the converter to always send/update the timer value to the one saved into the configuration before updating state to ON ?

Thanks :)

@ScratMan
Copy link
Contributor

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.

@hadjedjvincent
Copy link

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.

Too bad :(

You need to adapt your usage by first setting timer value each time before toggling the valve on.

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 ?
Sorry to ask, never developed on Zigbee2MQTT or zigbee-herdsman-converters before but i've made homebridge plugins.

I would be happy to add this as an option (like, force timer after OFF) to make it available for everyone of course!

@ScratMan
Copy link
Contributor

For example, I'm using this valve combined with the HASmartIrrigation integration in Home Assistant.
The valve is managed by an automation with the following yaml that gets the computed irrigation duration, set this duration into the valve timer setting, wait 1 minute to make sure the ZigBee controller has sent the command (I could wait for the timer sensor to be equal to the set value, instead) and switch the valve on.

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

@hadjedjvincent
Copy link

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.
Like a replacing tz.legacy.tuya_switch_state by a custom method which will first, set the timer value from configuration, and then, switch the state.

@ScratMan
Copy link
Contributor

Not sure this would be possible, the converter doesn't store data, it only translates raw data to human ready data at communication level.

@github-actions
Copy link
Contributor

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

@github-actions github-actions bot added the stale label Sep 18, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Sep 25, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants