Skip to content

fix: restore sh1btabb spec, add a7sghmms #5567

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

Merged
merged 9 commits into from
Mar 15, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
275 changes: 181 additions & 94 deletions devices/giex.js
Original file line number Diff line number Diff line change
@@ -1,135 +1,222 @@
const exposes = require('../lib/exposes');
const tuya = require('../lib/tuya');
const e = exposes.presets;
const ea = exposes.access;

const {presets: ep, access: ea} = exposes;

const CAPACITY = 'capacity';
const DURATION = 'duration';
const MINUTES_IN_A_DAY = 1440;
const OFF = 'OFF';
const ON = 'ON';
const SAFETY_MIN_SECS = 10;
const SECONDS_IN_12_HOURS = 43200;

const toLocalTime = (time, timezone) => {
if (time === '--:--:--') {
return time;
}

const local = new Date(`2000-01-01T${time}.000${timezone}`); // Using 1970 instead produces edge cases
return local.toTimeString().split(' ').shift();
};

const keys = {
giexWaterValve: {
battery: 'battery',
currentTemperature: 'current_temperature',
cycleIrrigationInterval: 'cycle_irrigation_interval',
cycleIrrigationNumTimes: 'cycle_irrigation_num_times',
irrigationEndTime: 'irrigation_end_time',
irrigationStartTime: 'irrigation_start_time',
irrigationTarget: 'irrigation_target',
lastIrrigationDuration: 'last_irrigation_duration',
mode: 'mode',
state: 'state',
waterConsumed: 'water_consumed',
},
};

const dataPoints = {
// GIEX Water Valve
giexWaterValveState: 2,
giexWaterValveMode: 1,
giexWaterValveIrrigationTarget: 104,
giexWaterValveWaterConsumed: 111,
giexWaterValveIrrigationStartTime: 101,
giexWaterValveIrrigationEndTime: 102,
giexWaterValveLastIrrigationDuration: 114,
giexWaterValveBattery: 108,
giexWaterValveCycleIrrigationNumTimes: 103,
giexWaterValveCycleIrrigationInterval: 105,
giexWaterValveCurrentTempurature: 106,
giexWaterValve: {
battery: 108,
currentTemperature: 106,
cycleIrrigationInterval: 105,
cycleIrrigationNumTimes: 103,
irrigationEndTime: 102,
irrigationStartTime: 101,
irrigationTarget: 104,
lastIrrigationDuration: 114,
mode: 1,
state: 2,
waterConsumed: 111,
},
};

const fzModelConverters = {
QT06_1: {
// _TZE200_sh1btabb timezone is GMT+8
time: (value) => toLocalTime(value, '+08:00'),
},
};

const fzLocal = {
giex_water_valve:
{
giexWaterValve: {
cluster: 'manuSpecificTuya',
type: ['commandDataResponse', 'commandDataReport'],
convert: (model, msg, publish, options, meta) => {
const modelConverters = fzModelConverters[model.model] || {};
for (const dpValue of msg.data.dpValues) {
const value = tuya.getDataValue(dpValue);
const dp = dpValue.dp;
const {dp} = dpValue;
switch (dp) {
case dataPoints.giexWaterValveState: {
return {state: value ? 'ON': 'OFF'};
}
case dataPoints.giexWaterValveMode: {
return {mode: value ? 'capacity': 'duration'};
}
case dataPoints.giexWaterValveIrrigationTarget: {
return {irrigation_target: value};
}
case dataPoints.giexWaterValveCycleIrrigationNumTimes: {
return {cycle_irrigation_num_times: value};
}
case dataPoints.giexWaterValveCycleIrrigationInterval: {
return {cycle_irrigation_interval: value};
}
case dataPoints.giexWaterValveWaterConsumed: {
return {water_consumed: value};
}
case dataPoints.giexWaterValveIrrigationStartTime: {
return {irrigation_start_time: value};
}
case dataPoints.giexWaterValveIrrigationEndTime: {
return {irrigation_end_time: value};
}
case dataPoints.giexWaterValveLastIrrigationDuration: {
return {last_irrigation_duration: value};
}
case dataPoints.giexWaterValveBattery: {
return {battery: value};
}
case dataPoints.giexWaterValveCurrentTempurature: {
return; // Do Nothing - value ignored because isn't a valid tempurature reading. Misdocumented and usage unclear
}
default: {
meta.logger.warn(`fz:giex_water_valve: NOT RECOGNIZED DP #${dp} with VALUE = ${value}`);
}
case dataPoints.giexWaterValve.state:
return {[keys.giexWaterValve.state]: value ? ON: OFF};
case dataPoints.giexWaterValve.mode:
return {[keys.giexWaterValve.mode]: value ? CAPACITY: DURATION};
case dataPoints.giexWaterValve.irrigationTarget:
return {[keys.giexWaterValve.irrigationTarget]: value};
case dataPoints.giexWaterValve.cycleIrrigationNumTimes:
return {[keys.giexWaterValve.cycleIrrigationNumTimes]: value};
case dataPoints.giexWaterValve.cycleIrrigationInterval:
return {[keys.giexWaterValve.cycleIrrigationInterval]: value};
case dataPoints.giexWaterValve.waterConsumed:
return {[keys.giexWaterValve.waterConsumed]: value};
case dataPoints.giexWaterValve.irrigationStartTime:
return {[keys.giexWaterValve.irrigationStartTime]: modelConverters.time?.(value) || value};
case dataPoints.giexWaterValve.irrigationEndTime:
return {[keys.giexWaterValve.irrigationEndTime]: modelConverters.time?.(value) || value};
case dataPoints.giexWaterValve.lastIrrigationDuration:
return {[keys.giexWaterValve.lastIrrigationDuration]: value.split(',').shift()}; // Remove meaningless ,0 suffix
case dataPoints.giexWaterValve.battery:
return {[keys.giexWaterValve.battery]: value};
case dataPoints.giexWaterValve.currentTemperature:
return; // Do Nothing - value ignored because it isn't a valid temperature reading (misdocumented and usage unclear)
default: // Unknown data point warning
meta.logger.warn(`fzLocal.giexWaterValve: Unrecognized DP #${dp} with VALUE = ${value}`);
}
}
},
},
};

const tzModelConverters = {
QT06_2: {
// _TZE200_a7sghmms irrigation time should not be less than 10 secs as per GiEX advice
irrigationTarget: (value, mode) => value > 0 && value < SAFETY_MIN_SECS && mode === DURATION ? SAFETY_MIN_SECS : value,
},
};

const tzLocal = {
giex_water_valve:
giexWaterValve:
{
key: ['mode', 'irrigation_target', 'state', 'cycle_irrigation_num_times', 'cycle_irrigation_interval'],
key: [
keys.giexWaterValve.mode,
keys.giexWaterValve.irrigationTarget,
keys.giexWaterValve.state,
keys.giexWaterValve.cycleIrrigationNumTimes,
keys.giexWaterValve.cycleIrrigationInterval,
],
convertSet: async (entity, key, value, meta) => {
const modelConverters = tzModelConverters[meta.mapped?.model] || {};
switch (key) {
case 'mode': {
let mode = 0;
if (value === 'duration') mode = 0;
else if (value === 'capacity') mode = 1;
await tuya.sendDataPointBool(entity, dataPoints.giexWaterValveMode, mode);
return {state: {mode: value}};
}
case 'irrigation_target':
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValveIrrigationTarget, value);
return {state: {irrigation_target: value}};
case 'state':
await tuya.sendDataPointBool(entity, dataPoints.giexWaterValveState, value === 'ON');
case keys.giexWaterValve.state:
await tuya.sendDataPointBool(entity, dataPoints.giexWaterValve.state, value === ON);
break;
case 'cycle_irrigation_num_times':
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValveCycleIrrigationNumTimes, value);
return {state: {cycle_irrigation_num_times: value}};
case 'cycle_irrigation_interval':
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValveCycleIrrigationInterval, value);
return {state: {cycle_irrigation_interval: value}};
case keys.giexWaterValve.mode:
await tuya.sendDataPointBool(entity, dataPoints.giexWaterValve.mode, value === CAPACITY);
return {state: {[keys.giexWaterValve.mode]: value}};
case keys.giexWaterValve.irrigationTarget: {
const mode = meta.state?.[keys.giexWaterValve.mode];
const sanitizedValue = modelConverters.irrigationTarget?.(value, mode) || value;
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValve.irrigationTarget, sanitizedValue);
return {state: {[keys.giexWaterValve.irrigationTarget]: sanitizedValue}};
}
case keys.giexWaterValve.cycleIrrigationNumTimes:
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValve.cycleIrrigationNumTimes, value);
return {state: {[keys.giexWaterValve.cycleIrrigationNumTimes]: value}};
case keys.giexWaterValve.cycleIrrigationInterval:
await tuya.sendDataPointValue(entity, dataPoints.giexWaterValve.cycleIrrigationInterval, value);
return {state: {[keys.giexWaterValve.cycleIrrigationInterval]: value}};
default: // Unknown key warning
meta.logger.warn(`tz.GIEX_water_valve: Unhandled key ${key}`);
meta.logger.warn(`tzLocal.giexWaterValve: Unhandled KEY ${key}`);
}
},
},
};

const exportTemplates = {
giexWaterValve: {
vendor: 'GiEX',
description: 'Water irrigation valve',
onEvent: tuya.onEventSetLocalTime,
fromZigbee: [fzLocal.giexWaterValve],
toZigbee: [tzLocal.giexWaterValve],
exposes: [
ep.battery(),
exposes.binary(keys.giexWaterValve.state, ea.STATE_SET, ON, OFF)
.withDescription('State'),
exposes.enum(keys.giexWaterValve.mode, ea.STATE_SET, [DURATION, CAPACITY])
.withDescription('Irrigation mode'),
exposes.numeric(keys.giexWaterValve.cycleIrrigationNumTimes, ea.STATE_SET)
.withValueMin(0)
.withValueMax(100)
.withDescription('Number of cycle irrigation times, set to 0 for single cycle'),
exposes.numeric(keys.giexWaterValve.irrigationStartTime, ea.STATE)
.withDescription('Last irrigation start time'),
exposes.numeric(keys.giexWaterValve.irrigationEndTime, ea.STATE)
.withDescription('Last irrigation end time'),
exposes.numeric(keys.giexWaterValve.lastIrrigationDuration, ea.STATE)
.withDescription('Last irrigation duration'),
exposes.numeric(keys.giexWaterValve.waterConsumed, ea.STATE)
.withUnit('L')
.withDescription('Last irrigation water consumption'),
],
},
};

module.exports = [
// _TZE200_sh1btabb uses minutes, timezone is GMT+8
{
...exportTemplates.giexWaterValve,
model: 'QT06_1',
fingerprint: [
{modelID: 'TS0601', manufacturerName: '_TZE200_sh1btabb'},
],
exposes: [
...exportTemplates.giexWaterValve.exposes,
exposes.numeric(keys.giexWaterValve.irrigationTarget, ea.STATE_SET)
.withValueMin(0)
.withValueMax(MINUTES_IN_A_DAY)
.withUnit('minutes or litres')
.withDescription('Irrigation target, duration in minutes or capacity in litres (depending on mode)'),
exposes.numeric(keys.giexWaterValve.cycleIrrigationInterval, ea.STATE_SET)
.withValueMin(0)
.withValueMax(MINUTES_IN_A_DAY)
.withUnit('min')
.withDescription('Cycle irrigation interval'),
],
},
// _TZE200_a7sghmms uses seconds, timezone is local
{
...exportTemplates.giexWaterValve,
model: 'QT06_2',
fingerprint: [
{modelID: 'TS0601', manufacturerName: '_TZE200_a7sghmms'},
],
model: 'QT06',
vendor: 'GiEX',
description: 'Water irrigation valve',
onEvent: tuya.onEventSetLocalTime,
fromZigbee: [fzLocal.giex_water_valve],
toZigbee: [tzLocal.giex_water_valve],
exposes: [
e.battery(),
exposes.binary('state', ea.STATE_SET, 'ON', 'OFF').withDescription('State'),
exposes.enum('mode', ea.STATE_SET, ['duration', 'capacity']).withDescription('Irrigation mode'),
exposes.numeric('irrigation_target', exposes.access.STATE_SET).withValueMin(0).withValueMax(3600).withUnit('seconds or litres')
.withDescription('Irrigation target, duration in seconds or capacity in litres (depending on mode)'),
exposes.numeric('cycle_irrigation_num_times', exposes.access.STATE_SET).withValueMin(0).withValueMax(100)
.withDescription('Number of cycle irrigation times, set to 0 for single cycle'),
exposes.numeric('cycle_irrigation_interval', exposes.access.STATE_SET).withValueMin(0).withValueMax(3600).withUnit('sec')
...exportTemplates.giexWaterValve.exposes,
exposes.numeric(keys.giexWaterValve.irrigationTarget, ea.STATE_SET)
.withValueMin(0)
.withValueMax(SECONDS_IN_12_HOURS)
.withUnit('seconds or litres')
.withDescription('Irrigation target, duration in seconds or capacity in litres (depending on mode), ' +
'set to 0 to leave the valve on indefinitely, ' +
'for safety reasons the target will be forced to a minimum of 10 seconds in duration mode'),
exposes.numeric(keys.giexWaterValve.cycleIrrigationInterval, ea.STATE_SET)
.withValueMin(0)
.withValueMax(SECONDS_IN_12_HOURS)
.withUnit('sec')
.withDescription('Cycle irrigation interval'),
exposes.numeric('irrigation_start_time', ea.STATE).withDescription('Last irrigation start time (GMT)'),
exposes.numeric('irrigation_end_time', ea.STATE).withDescription('Last irrigation end time (GMT)'),
exposes.numeric('last_irrigation_duration', exposes.access.STATE)
.withDescription('Last irrigation duration'),
exposes.numeric('water_consumed', exposes.access.STATE).withUnit('L')
.withDescription('Last irrigation water consumption'),
],
},
];