From d225bf463e9479fd63a20a8dab825e47cbe00600 Mon Sep 17 00:00:00 2001 From: Oleksandr Burdun Date: Fri, 20 Sep 2024 13:26:04 +0300 Subject: [PATCH 1/2] idea with wekdays programs for tuya-moes thermostats --- src/devices/moes.ts | 66 +++++++++++++++------------------------------ src/lib/exposes.ts | 21 ++++++++++++--- src/lib/legacy.ts | 46 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 48 deletions(-) diff --git a/src/devices/moes.ts b/src/devices/moes.ts index 797022f9850ef..1e1c0743ab87e 100644 --- a/src/devices/moes.ts +++ b/src/devices/moes.ts @@ -15,11 +15,7 @@ const fzZosung = zosung.fzZosung; const tzZosung = zosung.tzZosung; const ez = zosung.presetsZosung; -const exposesLocal = { - hour: (name: string) => e.numeric(name, ea.STATE_SET).withUnit('h').withValueMin(0).withValueMax(23), - minute: (name: string) => e.numeric(name, ea.STATE_SET).withUnit('m').withValueMin(0).withValueMax(59), - program_temperature: (name: string) => e.numeric(name, ea.STATE_SET).withUnit('°C').withValueMin(5).withValueMax(35).withValueStep(0.5), -}; +const programText = (name: string) => e.text(name, ea.STATE_SET).withPattern(/\d\d:\d\d\/\d\d°C/); const definitions: DefinitionWithExtend[] = [ { @@ -122,7 +118,7 @@ const definitions: DefinitionWithExtend[] = [ legacy.tz.moes_thermostat_deadzone_temperature, legacy.tz.moes_thermostat_max_temperature_limit, legacy.tz.moes_thermostat_min_temperature_limit, - legacy.tz.moes_thermostat_program_schedule, + legacy.tz.moes_thermostat_program_schedule_v2, ], whiteLabel: [tuya.whitelabel('Moes', 'BHT-002/BHT-006', 'Smart heating thermostat', ['_TZE204_aoclfnxz'])], exposes: (device, options) => { @@ -143,44 +139,26 @@ const definitions: DefinitionWithExtend[] = [ .withPreset(['hold', 'program']), e.temperature_sensor_select(['IN', 'AL', 'OU']), e - .composite('program', 'program', ea.STATE_SET) - .withDescription('Time of day and setpoint to use when in program mode') - .withFeature(exposesLocal.hour('weekdays_p1_hour')) - .withFeature(exposesLocal.minute('weekdays_p1_minute')) - .withFeature(exposesLocal.program_temperature('weekdays_p1_temperature')) - .withFeature(exposesLocal.hour('weekdays_p2_hour')) - .withFeature(exposesLocal.minute('weekdays_p2_minute')) - .withFeature(exposesLocal.program_temperature('weekdays_p2_temperature')) - .withFeature(exposesLocal.hour('weekdays_p3_hour')) - .withFeature(exposesLocal.minute('weekdays_p3_minute')) - .withFeature(exposesLocal.program_temperature('weekdays_p3_temperature')) - .withFeature(exposesLocal.hour('weekdays_p4_hour')) - .withFeature(exposesLocal.minute('weekdays_p4_minute')) - .withFeature(exposesLocal.program_temperature('weekdays_p4_temperature')) - .withFeature(exposesLocal.hour('saturday_p1_hour')) - .withFeature(exposesLocal.minute('saturday_p1_minute')) - .withFeature(exposesLocal.program_temperature('saturday_p1_temperature')) - .withFeature(exposesLocal.hour('saturday_p2_hour')) - .withFeature(exposesLocal.minute('saturday_p2_minute')) - .withFeature(exposesLocal.program_temperature('saturday_p2_temperature')) - .withFeature(exposesLocal.hour('saturday_p3_hour')) - .withFeature(exposesLocal.minute('saturday_p3_minute')) - .withFeature(exposesLocal.program_temperature('saturday_p3_temperature')) - .withFeature(exposesLocal.hour('saturday_p4_hour')) - .withFeature(exposesLocal.minute('saturday_p4_minute')) - .withFeature(exposesLocal.program_temperature('saturday_p4_temperature')) - .withFeature(exposesLocal.hour('sunday_p1_hour')) - .withFeature(exposesLocal.minute('sunday_p1_minute')) - .withFeature(exposesLocal.program_temperature('sunday_p1_temperature')) - .withFeature(exposesLocal.hour('sunday_p2_hour')) - .withFeature(exposesLocal.minute('sunday_p2_minute')) - .withFeature(exposesLocal.program_temperature('sunday_p2_temperature')) - .withFeature(exposesLocal.hour('sunday_p3_hour')) - .withFeature(exposesLocal.minute('sunday_p3_minute')) - .withFeature(exposesLocal.program_temperature('sunday_p3_temperature')) - .withFeature(exposesLocal.hour('sunday_p4_hour')) - .withFeature(exposesLocal.minute('sunday_p4_minute')) - .withFeature(exposesLocal.program_temperature('sunday_p4_temperature')), + .composite('program_v2', 'program_v2', ea.STATE_SET) + .withDescription( + 'PROGRAMMING MODE ⏱ - In this mode, ' + + 'the device executes a preset week programming temperature time and temperature. ' + + 'You can set up to 4 stages of temperature every for WEEKDAY ➀➁➂➃➄, SATURDAY ➅ and SUNDAY ➆.', + ) + .withFeature(programText('weekdays_p1')) + .withFeature(programText('weekdays_p2')) + .withFeature(programText('weekdays_p3')) + .withFeature(programText('weekdays_p4')) + + .withFeature(programText('saturday_p1')) + .withFeature(programText('saturday_p2')) + .withFeature(programText('saturday_p3')) + .withFeature(programText('saturday_p4')) + + .withFeature(programText('sunday_p1')) + .withFeature(programText('sunday_p2')) + .withFeature(programText('sunday_p3')) + .withFeature(programText('sunday_p4')), ]; }, onEvent: tuya.onEventSetLocalTime, diff --git a/src/lib/exposes.ts b/src/lib/exposes.ts index 1ee5d2c938fc9..bc178bd97e09c 100644 --- a/src/lib/exposes.ts +++ b/src/lib/exposes.ts @@ -4,6 +4,7 @@ import {Access, Range} from './types'; import {getLabelFromName} from './utils'; export type Feature = Numeric | Binary | Enum | Composite | List | Text; +export type FeatureList = Numeric | Binary | Composite | Text; export class Base { name: string; @@ -196,11 +197,11 @@ export class Binary extends Base { export class List extends Base { property: string = ''; - item_type: Numeric | Binary | Composite | Text; + item_type: FeatureList; length_min?: number; length_max?: number; - constructor(name: string, access: number, itemType: Numeric | Binary | Composite | Text) { + constructor(name: string, access: number, itemType: FeatureList) { super(); this.type = 'list'; this.name = name; @@ -310,6 +311,7 @@ export class Enum extends Base { export class Text extends Base { property: string = ''; + pattern?: RegExp; constructor(name: string, access: number) { super(); @@ -320,6 +322,17 @@ export class Text extends Base { this.access = access; } + withPattern(pattern: RegExp): Text { + this.pattern = pattern; + return this; + } + + checkPatternMatch() { + if (!this.pattern.test(this.property)) { + throw new Error('String format pattern check failed:' + this.label); + } + } + clone(): Text { const clone = new Text(this.name, this.access); this.copy(clone); @@ -887,7 +900,7 @@ export const presets = { light: () => new Light(), numeric: (name: string, access: number) => new Numeric(name, access), text: (name: string, access: number) => new Text(name, access), - list: (name: string, access: number, itemType: Feature) => new List(name, access, itemType), + list: (name: string, access: number, itemType: FeatureList) => new List(name, access, itemType), switch_: () => new Switch(), // Specific ac_frequency: () => @@ -1362,5 +1375,5 @@ exports.light = () => new Light(); exports.numeric = (name: string, access: number) => new Numeric(name, access); exports.switch = () => new Switch(); exports.text = (name: string, access: number) => new Text(name, access); -exports.list = (name: string, access: number, itemType: Feature) => new List(name, access, itemType); +exports.list = (name: string, access: number, itemType: FeatureList) => new List(name, access, itemType); exports.lock = () => new Lock(); diff --git a/src/lib/legacy.ts b/src/lib/legacy.ts index 5618afa5e1421..6f6bae8fc1bdf 100644 --- a/src/lib/legacy.ts +++ b/src/lib/legacy.ts @@ -6531,6 +6531,52 @@ const toZigbee2 = { await sendDataPointRaw(entity, dataPoints.moesSchedule, payload); }, } satisfies Tz.Converter, + moes_thermostat_program_schedule_v2: { + key: ['program_v2'], + convertSet: async (entity, key, value: any, meta) => { + if (!meta.state.program) { + logger.warning(`Existing program state not set.`, 'zhc:legacy:tz:moes_bht_002'); + return; + } + + /* Merge modified value into existing state and send all over in one go */ + const newProgram = { + // @ts-expect-error ignore + ...meta.state.program, + ...value, + }; + + const getNumbers = (input: exposes.Text): number[] => { + input.checkPatternMatch(); + + const hourTemperature = input.property.split('/'); + const hourMinute = hourTemperature[0].split(':', 2); + const clamp = (value: number, min: number, max: number): number => { + return Math.max(min, Math.min(max, value)); + }; + + return [clamp(parseInt(hourMinute[0]), 0, 23), clamp(parseInt(hourMinute[1]), 0, 59), clamp(parseInt(hourTemperature[1]) * 2, 5, 35)]; + }; + + const payload = getNumbers(newProgram.weekdays_p1) + .concat(getNumbers(newProgram.weekdays_p2)) + .concat(getNumbers(newProgram.weekdays_p2)) + .concat(getNumbers(newProgram.weekdays_p3)) + + .concat(getNumbers(newProgram.weekdays_p4)) + .concat(getNumbers(newProgram.saturday_p1)) + .concat(getNumbers(newProgram.saturday_p2)) + .concat(getNumbers(newProgram.saturday_p3)) + .concat(getNumbers(newProgram.saturday_p4)) + + .concat(getNumbers(newProgram.sunday_p1)) + .concat(getNumbers(newProgram.sunday_p2)) + .concat(getNumbers(newProgram.sunday_p3)) + .concat(getNumbers(newProgram.sunday_p4)); + + await sendDataPointRaw(entity, dataPoints.moesSchedule, payload); + }, + } satisfies Tz.Converter, moesS_thermostat_system_mode: { key: ['system_mode'], convertSet: async (entity, key, value, meta) => { From 4b7fff5ea80c1bbbccabdbc87dfd0ef700c56e78 Mon Sep 17 00:00:00 2001 From: Oleksandr Burdun Date: Fri, 20 Sep 2024 13:37:48 +0300 Subject: [PATCH 2/2] add pattern error message --- src/devices/moes.ts | 2 +- src/lib/exposes.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/devices/moes.ts b/src/devices/moes.ts index 1e1c0743ab87e..ca2db73547bab 100644 --- a/src/devices/moes.ts +++ b/src/devices/moes.ts @@ -15,7 +15,7 @@ const fzZosung = zosung.fzZosung; const tzZosung = zosung.tzZosung; const ez = zosung.presetsZosung; -const programText = (name: string) => e.text(name, ea.STATE_SET).withPattern(/\d\d:\d\d\/\d\d°C/); +const programText = (name: string) => e.text(name, ea.STATE_SET).withPattern(/\d\d:\d\d\/\d\d°C/, 'Template is: 00:00/00°C'); const definitions: DefinitionWithExtend[] = [ { diff --git a/src/lib/exposes.ts b/src/lib/exposes.ts index bc178bd97e09c..da7e011699dc9 100644 --- a/src/lib/exposes.ts +++ b/src/lib/exposes.ts @@ -312,6 +312,7 @@ export class Enum extends Base { export class Text extends Base { property: string = ''; pattern?: RegExp; + patternComment?: string; constructor(name: string, access: number) { super(); @@ -322,14 +323,15 @@ export class Text extends Base { this.access = access; } - withPattern(pattern: RegExp): Text { + withPattern(pattern: RegExp, comment: string): Text { this.pattern = pattern; + this.patternComment = comment; return this; } checkPatternMatch() { if (!this.pattern.test(this.property)) { - throw new Error('String format pattern check failed:' + this.label); + throw new Error('String format pattern check failed:' + this.label + '\n' + this.patternComment); } }