diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000000000..67c4920dbc7b8 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,39 @@ +name: Run Tests and Lint + +on: + push: + branches: + - main + - add-egi-hvac-adapter + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: '16' # Use the Node.js version Zigbee2MQTT supports + + - name: Install pnpm + run: npm install -g pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run ESLint + run: pnpm run eslint --fix + + - name: Format Code + run: pnpm run pretty:write + + - name: Build Project + run: pnpm run build + + - name: Run Tests + run: pnpm test diff --git a/src/devices/egihavc.ts b/src/devices/egihavc.ts new file mode 100644 index 0000000000000..a7341a326757a --- /dev/null +++ b/src/devices/egihavc.ts @@ -0,0 +1,158 @@ +import fz from '../converters/fromZigbee'; +import tz from '../converters/toZigbee'; +import * as exposes from '../lib/exposes'; +import * as reporting from '../lib/reporting'; +import {Definition} from '../lib/types'; + +const e = exposes.presets; +const ea = exposes.access; + +const definitions: Definition[] = [ + { + zigbeeModel: ['TS0601'], + model: 'EGI_HVAC_Adapter', + vendor: 'Tuya', + description: 'EGI HVAC Climate Control Adapter', + fromZigbee: [ + { + cluster: 'manuSpecificTuya', + type: ['commandDataResponse', 'commandDataReport'], + convert: (model, msg, publish, options, meta) => { + const parsedData: Record = {}; + + if (msg.data && msg.data.dpValues) { + msg.data.dpValues.forEach((dpValue: { dp: number; data: Buffer }) => { + const dp = dpValue.dp; + const value = dpValue.data.readUIntBE(0, dpValue.data.length); + + switch (dp) { + case 1: + parsedData.state = value === 1 ? 'ON' : 'OFF'; + break; + case 2: + parsedData.temperature_set = value; + break; + case 3: + parsedData.temperature_current = value; + break; + case 4: + const modes = ['Cool', 'Heat', 'Dehumidify', 'Fan']; + parsedData.mode = modes[value] || 'unknown'; + break; + case 5: + const fanSpeeds = ['Low', 'Medium', 'High', 'Auto']; + parsedData.fan_speed = fanSpeeds[value] || 'unknown'; + break; + case 7: + parsedData.set_as_slave = value === 1 ? 'ENABLE' : 'DISABLE'; + break; + default: + console.warn(`Unrecognized datapoint ${dp} with value ${value}`); + } + }); + + publish(parsedData); + } + + return parsedData; + }, + }, + ], + toZigbee: [ + tz.on_off, + { + key: ['temperature_set'], + convertSet: async (entity, key, value, meta) => { + const data = Buffer.alloc(4); + data.writeUIntBE(Math.round(value as number), 0, 4); + await entity.command( + 'manuSpecificTuya', + 'dataRequest', + { + seq: (meta as any).seq || 0, + dpValues: [{ dp: 2, datatype: 2, data }], + }, + { disableDefaultResponse: true } + ); + }, + }, + { + key: ['fan_speed'], + convertSet: async (entity, key, value, meta) => { + const fanMapping: Record = { Low: 0, Medium: 1, High: 2, Auto: 3 }; + await entity.command( + 'manuSpecificTuya', + 'dataRequest', + { + seq: (meta as any).seq || 0, + dpValues: [{ dp: 5, datatype: 4, data: Buffer.from([fanMapping[value as string]]) }], + }, + { disableDefaultResponse: true } + ); + }, + }, + { + key: ['mode'], + convertSet: async (entity, key, value, meta) => { + const modeMapping: Record = { Cool: 0, Heat: 1, Dehumidify: 2, Fan: 3 }; + await entity.command( + 'manuSpecificTuya', + 'dataRequest', + { + seq: (meta as any).seq || 0, + dpValues: [{ dp: 4, datatype: 4, data: Buffer.from([modeMapping[value as string]]) }], + }, + { disableDefaultResponse: true } + ); + }, + }, + { + key: ['set_as_slave'], + convertSet: async (entity, key, value, meta) => { + await entity.command( + 'manuSpecificTuya', + 'dataRequest', + { + seq: (meta as any).seq || 0, + dpValues: [{ dp: 7, datatype: 1, data: Buffer.from([value === 'ENABLE' ? 1 : 0]) }], + }, + { disableDefaultResponse: true } + ); + }, + }, + ], + exposes: [ + e.binary('state', ea.STATE_SET, 'ON', 'OFF').withDescription('Power state'), + e.numeric('temperature_set', ea.STATE_SET) + .withValueMin(16) + .withValueMax(32) + .withUnit('°C') + .withDescription('Target temperature'), + e.numeric('temperature_current', ea.STATE) + .withValueMin(-20) + .withValueMax(50) + .withUnit('°C') + .withDescription('Current room temperature'), + e.enum('mode', ea.STATE_SET, ['Cool', 'Heat', 'Dehumidify', 'Fan']).withDescription('System mode'), + e.enum('fan_speed', ea.STATE_SET, ['Low', 'Medium', 'High', 'Auto']).withDescription('Fan speed mode'), + e.binary('set_as_slave', ea.STATE_SET, 'ENABLE', 'DISABLE').withDescription('Set as Slave mode'), + ], + meta: { + tuyaDatapoints: [ + [1, 'state'], + [2, 'temperature_set'], + [3, 'temperature_current'], + [4, 'mode'], + [5, 'fan_speed'], + [7, 'set_as_slave'], + ], + }, + configure: async (device, coordinatorEndpoint) => { + const endpoint = device.getEndpoint(1); + await reporting.bind(endpoint, coordinatorEndpoint, ['genBasic']); + await endpoint.read('genBasic', ['modelId', 'manufacturerName']); + }, + }, +]; + +export default definitions; diff --git a/src/devices/index.ts b/src/devices/index.ts index 6f7ae12590d30..770e2b456599d 100644 --- a/src/devices/index.ts +++ b/src/devices/index.ts @@ -71,6 +71,7 @@ import ecosmart from './ecosmart'; import ecozy from './ecozy'; import edp from './edp'; import efekta from './efekta'; +import egihvac from './devices/egihvac'; import eglo from './eglo'; import elko from './elko'; import enbrighten from './enbrighten'; @@ -390,6 +391,7 @@ export default [ ...ecozy, ...edp, ...efekta, + ...egihvac, ...eglo, ...elko, ...enbrighten,