Koenkk / zigbee-herdsman-converters

Collection of device converters to be used with zigbee-herdsman
MIT License
886 stars 2.96k forks source link

Add TS0601 from _TZE200_i48qyn9s #5860

Open schluese opened 1 year ago

schluese commented 1 year ago

Please add the radiator valve TS0601 from _TZE200_i48qyn9s

this external converter worked, more or less, until the last update of zigbee2mqtt

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

const tuyaLocal = { dataPoints: { sh4Mode: 2, sh4HeatingSetpoint: 16, sh4LocalTemp: 24, sh4ChildLock: 30, sh4Battery: 34, sh4FaultCode: 45, sh4ComfortTemp: 101, sh4EcoTemp: 102, sh4VacationPeriod: 103, sh4TempCalibration: 104, sh4ScheduleTempOverride: 105, sh4RapidHeating: 106, sh4WindowStatus: 107, sh4Hibernate: 108, sh4ScheduleMon: 109, sh4ScheduleTue: 110, sh4ScheduleWed: 111, sh4ScheduleThu: 112, sh4ScheduleFri: 113, sh4ScheduleSat: 114, sh4ScheduleSun: 115, sh4OpenWindowTemp: 116, sh4OpenWindowTime: 117, sh4RapidHeatCntdownTimer: 118, sh4TempControl: 119, sh4RequestUpdate: 120, }, }; const fzLocal = { sh4_thermostat: { cluster: 'manuSpecificTuya', type: ['commandDataResponse', 'commandDataReport'], convert: (model, msg, publish, options, meta) => { //const dp = msg.data.dp; //const value = tuya.getDataValue(msg.data.datatype, msg.data.data); const dpValue = tuya.firstDpValue(msg, meta, 'sh4_thermostat'); const dp = dpValue.dp; const value = tuya.getDataValue(dpValue); function weeklySchedule(day, value) { // byte 0 - Day of Week (0~7 = Mon ~ Sun) <- redundant? // byte 1 - 1st period Temperature (1~59 = 0.5~29.5°C (0.5 step)) // byte 2 - 1st period end time (1~96 = 0:15 ~ 24:00 (15 min increment, i.e. 2 = 0:30, 3 = 0:45, ...)) // byte 3 - 2nd period Temperature // byte 4 - 2nd period end time // ... // byte 16 - 8th period end time // byte 17 - 9th period Temperature const weekDays=['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; // we get supplied in value only a weekday schedule, so we must add it to // the weekly schedule from meta.state, if it exists const weeklySchedule= meta.state.hasOwnProperty('weekly_schedule') ? meta.state.weekly_schedule : {}; meta.logger.info(JSON.stringify({'received day': day, 'received values': value})); let daySchedule = []; // result array for (let i=1; i<18 && value[i]; ++i) { const aTemp=value[i]; ++i; const time=value[i]; daySchedule=[...daySchedule, { temperature: Math.floor(aTemp/2), hour: Math.floor(time/4), minute: time % 4 15, }]; } meta.logger.info(JSON.stringify({'returned weekly schedule: ': daySchedule})); return {'weekly-schedule': {...weeklySchedule, [weekDays[day]]: daySchedule}}; } switch (dp) { case tuyaLocal.dataPoints.sh4Mode: // 2 // 0-Schedule; 1-Manual; 2-Away if (value == 0) { return { system_mode: 'auto', away_mode: 'OFF', current_heating_setpoint: meta.state.schedule_heating_setpoint_override }; } else if (value == 1) { return { system_mode: 'heat', away_mode: 'OFF', current_heating_setpoint: meta.state.manual_heating_setpoint }; } else if (value == 2) { return { system_mode: 'off', away_mode: 'ON', current_heating_setpoint: -1 // need implement read away_preset_temperature }; }; break; case tuyaLocal.dataPoints.sh4HeatingSetpoint: // 16 // 0 - Valve full OFF, 60 - Valve full ON : only in "manual" mode return { manual_heating_setpoint: (value / 2).toFixed(1), current_heating_setpoint: (value / 2).toFixed(1) }; case tuyaLocal.dataPoints.sh4LocalTemp: // 24 return {local_temperature: (value / 10).toFixed(1)}; case tuyaLocal.dataPoints.sh4ChildLock: // 30 return {child_lock: value ? 'LOCKED' : 'UNLOCKED'}; case tuyaLocal.dataPoints.sh4Battery: // 34 return { battery: value > 130 ? 100 : value < 70 ? 0 : ((value - 70)1.7).toFixed(1), battery_low: value < 90, }; case tuyaLocal.dataPoints.sh4FaultCode: // 45 break;
case tuyaLocal.dataPoints.sh4ComfortTemp: // 101 return {comfort_temp_preset: (value / 2).toFixed(1)}; case tuyaLocal.dataPoints.sh4EcoTemp: // 102 return {eco_temp_preset: (value / 2).toFixed(1)}; case tuyaLocal.dataPoints.sh4VacationPeriod: // 103 return { away_data: { year: value[0]+2000, month: value[1], day: value[2], hour: value[3], minute: value[4], temperature: (value[5] /2).toFixed(1), away_hours: value[6]<< 8 | value[7], }, }; // byte 0 - Start Year (0x00 = 2000) // byte 1 - Start Month // byte 2 - Start Day // byte 3 - Start Hour // byte 4 - Start Minute // byte 5 - Temperature (1~59 = 0.5~29.5°C (0.5 step)) // byte 6-7 - Duration in Hours (0~2400 (100 days)) case tuyaLocal.dataPoints.sh4TempCalibration: // 104 return {local_temperature_calibration: value > 55 ? ((value - 0x100000000)/10).toFixed(1): (value/ 10).toFixed(1)}; case tuyaLocal.dataPoints.sh4ScheduleTempOverride: // 105 if (meta.state.system_mode == 'auto') { return { schedule_heating_setpoint_override: (value / 2).toFixed(1), current_heating_setpoint: (value / 2).toFixed(1) } } else { return {schedule_heating_setpoint_override: (value / 2).toFixed(1)} } case tuyaLocal.dataPoints.sh4RapidHeating: // 106 break; case tuyaLocal.dataPoints.sh4WindowStatus: // 107 break; case tuyaLocal.dataPoints.sh4Hibernate: // 108 break;
case tuyaLocal.dataPoints.sh4ScheduleMon: // 109 return weeklySchedule(0, value); case tuyaLocal.dataPoints.sh4ScheduleTue: // 110 return weeklySchedule(1, value); case tuyaLocal.dataPoints.sh4ScheduleWed: // 111 return weeklySchedule(2, value); case tuyaLocal.dataPoints.sh4ScheduleThu: // 112 return weeklySchedule(3, value); case tuyaLocal.dataPoints.sh4ScheduleFri: // 113 return weeklySchedule(4, value); case tuyaLocal.dataPoints.sh4ScheduleSat: // 114 return weeklySchedule(5, value); case tuyaLocal.dataPoints.sh4ScheduleSun: // 115 return weeklySchedule(6, value); case tuyaLocal.dataPoints.sh4OpenWindowTemp: // 116 break; case tuyaLocal.dataPoints.sh4OpenWindowTime: // 117 break; case tuyaLocal.dataPoints.sh4RapidHeatCntdownTimer: // 118 break; case tuyaLocal.dataPoints.sh4TempControl: // 119 break; case tuyaLocal.dataPoints.sh4RequestUpdate: // 120 break; default: meta.logger.warn(zigbee-herdsman-converters:sh4Thermostat: NOT RECOGNIZED DP #${ dp} with data ${JSON.stringify(msg.data)}); } }, }, };

const tzLocal = { sh4_thermostat_current_heating_setpoint: { key: ['current_heating_setpoint'], convertSet: async (entity, key, value, meta) => { const temp = Math.round(value 2); if (meta.state.system_mode == 'heat') { await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4HeatingSetpoint, temp); } else if (meta.state.system_mode == 'auto') { await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4ScheduleTempOverride, temp); } }, convertGet: async (entity, key, value, meta) => { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4RequestUpdate, 0); }, }, sh4_thermostat_comfort_temp_preset: { key: ['comfort_temp_preset'], convertSet: async (entity, key, value, meta) => { const temp = Math.round(value 2); await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4ComfortTemp, temp); }, }, sh4_thermostat_eco_temp_preset: { key: ['eco_temp_preset'], convertSet: async (entity, key, value, meta) => { const temp = Math.round(value 2); await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4EcoTemp, temp); }, }, sh4_thermostat_schedule_override_setpoint: { key: ['schedule_override_setpoint'], convertSet: async (entity, key, value, meta) => { const temp = Math.round(value 2); await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4ScheduleTempOverride, temp); }, }, sh4_thermostat_get_data: { key: ['local_temperature'], convertGet: async (entity, key, value, meta) => { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4RequestUpdate, 0); }, }, sh4_thermostat_mode: { key: ['system_mode'], convertSet: async (entity, key, value, meta) => { if ( value == 'auto' ) { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4Mode, 0); } else if ( value == 'heat' ) { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4Mode, 1); } else if ( value == 'off') { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4Mode, 2); //await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4HeatingSetpoint, 0); } }, convertGet: async (entity, key, value, meta) => { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4RequestUpdate, 0); }, }, sh4_thermostat_away: { key: ['away_mode', 'away_data'], convertSet: async (entity, key, value, meta) => { if (key==='away_mode') { if ( value == 'ON' ) { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4Mode, 2); } else { await tuya.sendDataPointEnum(entity, tuyaLocal.dataPoints.sh4Mode, 0); } } else if (key==='away_data') { const output= new Buffer(8); // byte 0 - Start Year (0x00 = 2000) // byte 1 - Start Month // byte 2 - Start Day // byte 3 - Start Hour // byte 4 - Start Minute // byte 5 - Temperature (1~59 = 0.5~29.5°C (0.5 step)) // byte 6-7 - Duration in Hours (0~2400 (100 days)) output[0]=value.year > 2000 ? value.year-2000 : value.year; // year output[1]=value.month; // month output[2]=value.day; // day output[3]=value.hour; // hour output[4]=value.minute; // min output[5]=Math.round(value.temperature 2); output[7]=value.away_hours & 0xFF; output[6]=value.away_hours >> 8; meta.logger.info(JSON.stringify({'send to tuya': output, 'value was': value, 'key was': key})); await tuya.sendDataPointRaw(entity, tuyaLocal.dataPoints.sh4VacationPeriod, output); } }, }, sh4_thermostat_schedule: { key: ['weekly_schedule'], convertSet: async (entity, key, value, meta) => { const weekDays=['mon' , 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; // byte 0 - Day of Week (0~7 = Mon ~ Sun) <- redundant? // byte 1 - 1st period Temperature (1~59 = 0.5~29.5°C (0.5 step)) // byte 2 - 1st period end time (1~96 = 0:15 ~ 24:00 (15 min increment, i.e. 2 = 0:30, 3 = 0:45, ...)) // byte 3 - 2nd period Temperature // byte 4 - 2nd period end time // ... // byte 16 - 8th period end time // byte 17 - 9th period Temperature // we overwirte only the received days. The other ones keep stored on the device const keys = Object.keys(value); for (const dayName of keys) { // for loop in order to delete the empty day schedules const output= new Buffer(17); // empty output byte buffer const dayNo=weekDays.indexOf(dayName); output[0]=dayNo+1; const schedule=value[dayName]; schedule.forEach((el, Index) => { if (Index <=8) { output[1+2Index]=Math.round(el.temperature2); output[2+2Index]=el.hour4+Math.floor((el.minute/15)); } else { meta.logger.warn('more than 8 schedule points supplied for week-day '+dayName + ' additional schedule points will be ignored'); } }); await tuya.sendDataPointRaw(entity, tuyaLocal.dataPoints.sh4ScheduleMon+dayNo, output); await new Promise((r) => setTimeout(r, 2000)); // wait 2 seconds between schedule sends in order not to overload the device } }, }, sh4_thermostat_child_lock: { key: ['child_lock'], convertSet: async (entity, key, value, meta) => { await tuya.sendDataPointBool(entity, tuyaLocal.dataPoints.sh4ChildLock, ['LOCKED', 'ON', 'LOCK'].includes(value.toUpperCase())); }, }, sh4_thermostat_calibration: { key: ['local_temperature_calibration'], convertSet: async (entity, key, value, meta) => { if (value > 0) value = value10; if (value < 0) value = value*10 + 0x100000000; await tuya.sendDataPointValue(entity, tuyaLocal.dataPoints.sh4TempCalibration, value); }, }, };

const device = { fingerprint: [ { modelID: 'TS0601', manufacturerName: '_TZE200_i48qyn9s' }, ], model: 'SH4 Zigbee eTRV', vendor: 'Tuya', description: 'Zigbee Radiator Thermostat', fromZigbee: [ fz.ignore_basic_report, fzLocal.sh4_thermostat, ], toZigbee: [ tzLocal.sh4_thermostat_current_heating_setpoint, tzLocal.sh4_thermostat_comfort_temp_preset, tzLocal.sh4_thermostat_eco_temp_preset, tzLocal.sh4_thermostat_away, tzLocal.sh4_thermostat_mode, tzLocal.sh4_thermostat_child_lock, tzLocal.sh4_thermostat_calibration, tzLocal.sh4_thermostat_schedule_override_setpoint, tzLocal.sh4_thermostat_schedule, tzLocal.sh4_thermostat_get_data, tz.tuya_data_point_test, ], onEvent: tuya.onEventSetLocalTime, exposes: [ e.battery(), e.battery_low(), e.child_lock(), exposes.climate().withSetpoint('current_heating_setpoint', 0.5, 29.5, 0.5) .withLocalTemperature() .withSystemMode(['auto','heat','off']) ], };

module.exports = device;

Thanks in advance

github-actions[bot] commented 1 year ago

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

schluese commented 1 year ago

up

github-actions[bot] commented 1 year ago

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

schluese commented 1 year ago

up :-)

florian-asche commented 1 year ago

What device is this? Can you please link it? Interesting because it reports battery state in %.

schluese commented 1 year ago

Heizkörper-Thermostat ESSENTIALS, Zigbee https://amzn.eu/d/bfTXGAiAm 20.08.2023 um 04:07 schrieb Florian Asche @.***>: What device is this? Can you please link it? Interesting because it reports battery state in %.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you authored the thread.Message ID: @.***>

github-actions[bot] commented 1 year ago

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

schluese commented 1 year ago

Please help me :-)

b2un0 commented 1 year ago

working converter can be found here https://github.com/b2un0/z2m-device-converter

schluese commented 1 year ago

Thank you very much! - Now I can set the temperature again, but the schedules are not yet displayed again. The following info is shown to me (I hope it helps):

MQTT publish: topic 'zigbee2mqtt/Regina Fenster rechts', payload '{"away_data":{"away_hours":0,"day":1,"hour":0,"minute":0,"month":1,"temperature":"17.0","year":2019},"away_mode":"OFF","battery":"74.8","battery_low":false,"child_lock":"UNLOCKED","comfort_temp_preset":"21.0","current_heating_setpoint":"21.5","eco_temp_preset":"17.0","linkquality":40,"local_temperature":"19.8","local_temperature_calibration":"0.0","manual_heating_setpoint":"21.5","open_window":"OFF","preset":"manual","schedule":"NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0 NaN:NaN/0","schedule_heating_setpoint_override":"17.0","system_mode":"heat","weekly-schedule":{"fri":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"mon":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"sat":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"sun":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"thu":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"tue":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}],"wed":[{"hour":6,"minute":0,"temperature":17},{"hour":9,"minute":0,"temperature":21},{"hour":17,"minute":0,"temperature":17},{"hour":23,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":24,"minute":0,"temperature":17},{"hour":24,"minute":0,"temperature":21},{"hour":null,"minute":null,"temperature":17}]}}'

b2un0 commented 1 year ago

Poste mal deinen Code von oben in einem Markdown code block, so dass man den auch kopieren und adaptieren kann.

so wie er aktuell gepostet ist, kann ich ihn nicht kopieren und "ausführen".

schluese commented 1 year ago

das würde ich sehr gerne tun, wenn ich wüsste was du meinst - sorry

b2un0 commented 1 year ago

image

und dann über "Preview" überprüfen, oder als Datei anhängen..

schluese commented 1 year ago

nur noch einmal zum Verständnis, ich gehe in HA über die Z2MQTT-Oberfläche auf den Punkt "Logs", wähle filter dann nach dem Gerät und kopiere die Info mit den von dir gezeigten Klammern ein?

b2un0 commented 1 year ago

Nein, ich hätte gerne deinen Code oben, aus deinem erste Post, deinen "alten" converter der nicht mehr funktionierte. Diesen aber nicht einfach hier stumpf "rein kopiert" (wie oben), sondern als Datei Anhang, also die .js Datei.

die Logs aus HA helfen hier nicht, da wir uns hier im Z2M befinden, hier muss das gemacht werden. HA erkennt das dann automatisch.

schluese commented 1 year ago

Aah, vielen Dank - das habe ich jetzt verstanden - was soll ich sagen, blond und Frau ;-) Dann sollte es dieser hier sein..

tuya_sh4_etrv.js.zip

b2un0 commented 1 year ago

Mit dem was du angehängt hast, hätten die Scheduler auch vorher nicht angezeigt werden können. Sicher, das dass bei den Essentials/MOES Thermostaten mal funktioniert hat?

Sei es aber drum, ich programmier das gerade eh alles um (ich hatte gehofft das in deinem converter mehr drin steht), da dieses Thermostat liefert zu 95% die gleichen Datapoints wie das von Lidl Dann sind auch die ganzen fehlenden Modes auch in Z2M da (Holiday Mode, Scheduler usw).

schluese commented 1 year ago

Ich möchte sagen, dass es angezeigt wurde - möchte jetzt aber nicht drauf wetten..

By the way: es sieht auch haargenau aus wie das vom Lidl, habe diese bereits im Einsatz und dachte es wären die gleichen - leider nur optisch aber nicht von der Software.

Vielen Dank für deine Mühen!!

b2un0 commented 1 year ago

Sind fast identisch, es weichen nur 2 Datenpunkte ab. Ich hab selber 2 von Silvercrest (LIDL), und 9 von den Eseentials, daher kann ich die gut vergleichen. Deswegen programmier ich das gerade aus eigen Interesse damit es in HA und Apple Home vernünftig funktioniert ;) Meine Frau und Tochter müssen es am Ende ja bedienen können.

github-actions[bot] commented 11 months ago

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

schluese commented 11 months ago

Entschuldigung, ich habe gerade erst gesehen das du noch einmal geschrieben hattest.. Ich kann jedenfalls sagen, dass die Thermostate funktionieren - ohne Sheduler ;-)

github-actions[bot] commented 5 months ago

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 30 days