From 0968cddb62dac9618c2a09a4b204562a1b55516a Mon Sep 17 00:00:00 2001 From: Everton Correia <1169768+evertonstz@users.noreply.github.com> Date: Fri, 19 Apr 2024 11:28:43 -0300 Subject: [PATCH] Add support for color theme messages (#35) * initial commit * fix lint --- package.json | 2 +- pnpm-lock.yaml | 66 ++++---- .../usePaxBluetoothServices.ts | 2 +- src/pax/containers/api/get.ts | 4 + src/pax/core/crypt/Pax3Imp.ts | 2 +- src/pax/core/messages/ColorThemeMessage.ts | 153 ++++++++++++++++++ .../messages/tests/ColorThemeMessage.test.ts | 91 +++++++++++ src/pax/shared/types/colorTheme.ts | 21 +++ 8 files changed, 305 insertions(+), 36 deletions(-) create mode 100644 src/pax/core/messages/ColorThemeMessage.ts create mode 100644 src/pax/core/messages/tests/ColorThemeMessage.test.ts create mode 100644 src/pax/shared/types/colorTheme.ts diff --git a/package.json b/package.json index 1acb650..286b330 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "ts-node": "^10.9.2", "typescript": "^5.2.2", "vite": "^5.0.8", - "vitest": "^1.3.1" + "vitest": "^1.4.0" }, "packageManager": "pnpm@8.15.4+sha256.cea6d0bdf2de3a0549582da3983c70c92ffc577ff4410cbf190817ddc35137c2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0c372f..da26c0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,7 +57,7 @@ devDependencies: version: 7.23.3(@babel/core@7.24.0) '@testing-library/jest-dom': specifier: ^6.3.0 - version: 6.4.2(@types/jest@29.5.12)(jest@29.7.0)(vitest@1.3.1) + version: 6.4.2(@types/jest@29.5.12)(jest@29.7.0)(vitest@1.5.0) '@testing-library/react': specifier: ^14.1.2 version: 14.2.1(react-dom@18.2.0)(react@18.2.0) @@ -137,8 +137,8 @@ devDependencies: specifier: ^5.0.8 version: 5.1.5(@types/node@20.11.24) vitest: - specifier: ^1.3.1 - version: 1.3.1(@types/node@20.11.24) + specifier: ^1.4.0 + version: 1.5.0(@types/node@20.11.24) packages: @@ -1892,7 +1892,7 @@ packages: pretty-format: 27.5.1 dev: true - /@testing-library/jest-dom@6.4.2(@types/jest@29.5.12)(jest@29.7.0)(vitest@1.3.1): + /@testing-library/jest-dom@6.4.2(@types/jest@29.5.12)(jest@29.7.0)(vitest@1.5.0): resolution: {integrity: sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==} engines: {node: '>=14', npm: '>=6', yarn: '>=1'} peerDependencies: @@ -1923,7 +1923,7 @@ packages: jest: 29.7.0(@types/node@20.11.24)(ts-node@10.9.2) lodash: 4.17.21 redent: 3.0.0 - vitest: 1.3.1(@types/node@20.11.24) + vitest: 1.5.0(@types/node@20.11.24) dev: true /@testing-library/react@14.2.1(react-dom@18.2.0)(react@18.2.0): @@ -2260,38 +2260,38 @@ packages: - '@swc/helpers' dev: true - /@vitest/expect@1.3.1: - resolution: {integrity: sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==} + /@vitest/expect@1.5.0: + resolution: {integrity: sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==} dependencies: - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 + '@vitest/spy': 1.5.0 + '@vitest/utils': 1.5.0 chai: 4.4.1 dev: true - /@vitest/runner@1.3.1: - resolution: {integrity: sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==} + /@vitest/runner@1.5.0: + resolution: {integrity: sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==} dependencies: - '@vitest/utils': 1.3.1 + '@vitest/utils': 1.5.0 p-limit: 5.0.0 pathe: 1.1.2 dev: true - /@vitest/snapshot@1.3.1: - resolution: {integrity: sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==} + /@vitest/snapshot@1.5.0: + resolution: {integrity: sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==} dependencies: magic-string: 0.30.8 pathe: 1.1.2 pretty-format: 29.7.0 dev: true - /@vitest/spy@1.3.1: - resolution: {integrity: sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==} + /@vitest/spy@1.5.0: + resolution: {integrity: sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==} dependencies: tinyspy: 2.2.1 dev: true - /@vitest/utils@1.3.1: - resolution: {integrity: sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==} + /@vitest/utils@1.5.0: + resolution: {integrity: sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==} dependencies: diff-sequences: 29.6.3 estree-walker: 3.0.3 @@ -6030,8 +6030,8 @@ packages: resolution: {integrity: sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==} dev: true - /tinypool@0.8.2: - resolution: {integrity: sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==} + /tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} engines: {node: '>=14.0.0'} dev: true @@ -6326,8 +6326,8 @@ packages: - '@types/react-dom' dev: false - /vite-node@1.3.1(@types/node@20.11.24): - resolution: {integrity: sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==} + /vite-node@1.5.0(@types/node@20.11.24): + resolution: {integrity: sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true dependencies: @@ -6383,15 +6383,15 @@ packages: fsevents: 2.3.3 dev: true - /vitest@1.3.1(@types/node@20.11.24): - resolution: {integrity: sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==} + /vitest@1.5.0(@types/node@20.11.24): + resolution: {integrity: sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 1.3.1 - '@vitest/ui': 1.3.1 + '@vitest/browser': 1.5.0 + '@vitest/ui': 1.5.0 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -6409,11 +6409,11 @@ packages: optional: true dependencies: '@types/node': 20.11.24 - '@vitest/expect': 1.3.1 - '@vitest/runner': 1.3.1 - '@vitest/snapshot': 1.3.1 - '@vitest/spy': 1.3.1 - '@vitest/utils': 1.3.1 + '@vitest/expect': 1.5.0 + '@vitest/runner': 1.5.0 + '@vitest/snapshot': 1.5.0 + '@vitest/spy': 1.5.0 + '@vitest/utils': 1.5.0 acorn-walk: 8.3.2 chai: 4.4.1 debug: 4.3.4 @@ -6425,9 +6425,9 @@ packages: std-env: 3.7.0 strip-literal: 2.0.0 tinybench: 2.6.0 - tinypool: 0.8.2 + tinypool: 0.8.4 vite: 5.1.5(@types/node@20.11.24) - vite-node: 1.3.1(@types/node@20.11.24) + vite-node: 1.5.0(@types/node@20.11.24) why-is-node-running: 2.2.2 transitivePeerDependencies: - less diff --git a/src/hooks/usePaxBluetoothServices/usePaxBluetoothServices.ts b/src/hooks/usePaxBluetoothServices/usePaxBluetoothServices.ts index 0b81f3a..f2e18f3 100644 --- a/src/hooks/usePaxBluetoothServices/usePaxBluetoothServices.ts +++ b/src/hooks/usePaxBluetoothServices/usePaxBluetoothServices.ts @@ -14,7 +14,7 @@ export interface UsePaxBluetoothServicesState { isListenerAdded: boolean; }; readFromMainService: () => Promise; - writeToMainService: (message: Pax.lib.PaxEncryptedPacket) => Promise; + writeToMainService: (packet: Pax.lib.PaxEncryptedPacket) => Promise; } export const usePaxBluetoothServices = ( diff --git a/src/pax/containers/api/get.ts b/src/pax/containers/api/get.ts index 5442205..208c0e3 100644 --- a/src/pax/containers/api/get.ts +++ b/src/pax/containers/api/get.ts @@ -1,3 +1,5 @@ +import { ColorThemeMessage } from '@/pax/core/messages/ColorThemeMessage'; + import { ActualTemperatureMessage, HeaterSetPointMessage, @@ -29,6 +31,8 @@ export const decodeDecryptedPacket = ( return HeaterSetPointMessage.createWithPacket(packet); case Messages.ATTRIBUTE_HEATING_STATE: return new HeatingStateMessage(packet); + case Messages.ATTRIBUTE_COLOR_THEME: + return ColorThemeMessage.createWithPacket(packet); default: return new UnknownMessage(messageType, packet); } diff --git a/src/pax/core/crypt/Pax3Imp.ts b/src/pax/core/crypt/Pax3Imp.ts index 1d10cc1..cff3749 100644 --- a/src/pax/core/crypt/Pax3Imp.ts +++ b/src/pax/core/crypt/Pax3Imp.ts @@ -53,7 +53,7 @@ export class Pax3Imp extends PaxAbs { } { return { iv: CryptoJS.enc.Hex.parse(hexPacket.slice(-32)), - hexPacketToDecrypt: CryptoJS.enc.Hex.parse(hexPacket.slice(0, 32)), + hexPacketToDecrypt: CryptoJS.enc.Hex.parse(hexPacket.slice(0, -32)), }; } diff --git a/src/pax/core/messages/ColorThemeMessage.ts b/src/pax/core/messages/ColorThemeMessage.ts new file mode 100644 index 0000000..049a7e7 --- /dev/null +++ b/src/pax/core/messages/ColorThemeMessage.ts @@ -0,0 +1,153 @@ +import { PaxDecryptedPacket } from '@/pax/containers/lib'; +import { Messages } from '@/pax/shared/enums'; +import { ColorMode, ColorTheme } from '@/pax/shared/types/colorTheme'; + +import { MessageAbs } from './MessageAbs'; +import { ReadAndWriteMessageAbs } from './ReadAndWriteMessageAbs'; + +export class ColorThemeMessage + extends ReadAndWriteMessageAbs + implements MessageAbs +{ + readonly theme: ColorTheme; + readonly messageType: Messages; + readonly packet: PaxDecryptedPacket; + + constructor( + builder: + | ColorThemeMessageBuilderFromPacket + | ColorThemeMessageBuilderFromValue, + ) { + super(); + this.messageType = Messages.ATTRIBUTE_COLOR_THEME; + if (builder instanceof ColorThemeMessageBuilderFromPacket) { + this.packet = builder.getPacket(); + const colorModesCount = this.packet.getUint8(1); + + const colorModes: ColorMode[] = []; + + for (let i = 0; i < colorModesCount; i++) { + const byteOffset = 2 + i * 8; + const colorBytes = new Uint8Array(this.packet.buffer, byteOffset, 8); + const colorMode = this.bytesToColorMode(colorBytes); + colorModes.push(colorMode); + } + const colorTheme = { + heating: colorModes[1], + regulating: colorModes[2], + standby: colorModes[3], + startup: colorModes[0], + }; + this.theme = colorTheme; + } else if (builder instanceof ColorThemeMessageBuilderFromValue) { + this.theme = builder.getTheme(); + + const extractIntFromColorMode = (colorMode: ColorMode): number[] => { + return [ + colorMode.color1.red, + colorMode.color1.green, + colorMode.color1.blue, + colorMode.color2.red, + colorMode.color2.green, + colorMode.color2.blue, + colorMode.animation, + colorMode.frequency, + ]; + }; + + const arr = new Uint8Array([ + this.messageType, + Object.keys(this.theme).length, + ...extractIntFromColorMode(this.theme.startup), + ...extractIntFromColorMode(this.theme.heating), + ...extractIntFromColorMode(this.theme.regulating), + ...extractIntFromColorMode(this.theme.standby), + ]); + + const view = new PaxDecryptedPacket( + arr.buffer, + arr.byteOffset, + arr.byteLength, + ); + view.setUint8(0, this.messageType); + this.packet = view; + } else { + throw new Error('Invalid builder'); + } + } + + bytesToColorMode(array: Uint8Array): ColorMode { + return { + animation: array[6], + color1: { + blue: array[2], + green: array[1], + red: array[0], + }, + color2: { + blue: array[5], + green: array[4], + red: array[3], + }, + frequency: array[7], + }; + } + + static createWithPacket(packet: PaxDecryptedPacket): ColorThemeMessage { + const builder = new ColorThemeMessageBuilderFromPacket(); + builder.setPacket(packet); + return new ColorThemeMessage(builder); + } + + static createWithTheme(theme: ColorTheme): ColorThemeMessage { + const builder = new ColorThemeMessageBuilderFromValue(); + builder.setTheme(theme); + return new ColorThemeMessage(builder); + } +} + +export class ColorThemeMessageBuilderFromPacket { + private packet?: PaxDecryptedPacket; + + setPacket(packet: PaxDecryptedPacket): ColorThemeMessageBuilderFromPacket { + this.packet = packet; + return this; + } + + getPacket(): PaxDecryptedPacket { + if (!this.packet) { + throw new Error('Packet is not set'); + } + return this.packet; + } + + build(ctor: new (builder: ColorThemeMessageBuilderFromPacket) => T): T { + if (!this.packet) { + throw new Error('Packet is not set'); + } + return new ctor(this); + } +} + +export class ColorThemeMessageBuilderFromValue { + private theme?: ColorTheme; + + setTheme(theme: ColorTheme): ColorThemeMessageBuilderFromValue { + this.theme = theme; + return this; + } + + getTheme(): ColorTheme { + if (!this.theme) { + throw new Error('Theme is not set'); + } + return this.theme; + } + + build(ctor: new (builder: ColorThemeMessageBuilderFromValue) => T): T { + if (!this.theme) { + throw new Error('Theme is not set'); + } + return new ctor(this); + } +} diff --git a/src/pax/core/messages/tests/ColorThemeMessage.test.ts b/src/pax/core/messages/tests/ColorThemeMessage.test.ts new file mode 100644 index 0000000..2cd07ca --- /dev/null +++ b/src/pax/core/messages/tests/ColorThemeMessage.test.ts @@ -0,0 +1,91 @@ +import { PaxDecryptedPacket } from '@/pax/containers/lib'; +import { hexToBuffer } from '@/pax/shared/utils/hexToBuffer'; +import { describe, expect, it } from 'vitest'; + +import { ColorThemeMessage } from '../ColorThemeMessage'; + +const theme = { + heating: { + animation: 0, + color1: { + blue: 40, + green: 115, + red: 255, + }, + color2: { + blue: 104, + green: 0, + red: 153, + }, + frequency: 15, + }, + regulating: { + animation: 2, + color1: { + blue: 40, + green: 149, + red: 253, + }, + color2: { + blue: 136, + green: 0, + red: 153, + }, + frequency: 11, + }, + standby: { + animation: 1, + color1: { + blue: 125, + green: 114, + red: 255, + }, + color2: { + blue: 154, + green: 64, + red: 0, + }, + frequency: 6, + }, + startup: { + animation: 1, + color1: { + blue: 40, + green: 115, + red: 255, + }, + color2: { + blue: 104, + green: 0, + red: 153, + }, + frequency: 5, + }, +}; + +describe('ColorThemeMessage.ts', () => { + // eslint-disable-next-line max-len + const decryptedHexPacket = `1404ff73289900680105ff7328990068000ffd9528990088020bff727d00409a01063af54f71f8899a493279d0a9e0fa`; + + const paxDecryptedPacket = new PaxDecryptedPacket( + hexToBuffer(decryptedHexPacket), + ); + + describe('Build from packet', () => { + it('should be created with a packet using the static method', () => { + const message = ColorThemeMessage.createWithPacket(paxDecryptedPacket); + + expect(message).toBeInstanceOf(ColorThemeMessage); + expect(message.packet).toBe(paxDecryptedPacket); + }); + }); + + describe('Build from temperature', () => { + it('should be created with a packet using the static method', () => { + const message = ColorThemeMessage.createWithTheme(theme); + + expect(message).toBeInstanceOf(ColorThemeMessage); + expect(decryptedHexPacket).contain(message.packet.toHex()); + }); + }); +}); diff --git a/src/pax/shared/types/colorTheme.ts b/src/pax/shared/types/colorTheme.ts new file mode 100644 index 0000000..4514cff --- /dev/null +++ b/src/pax/shared/types/colorTheme.ts @@ -0,0 +1,21 @@ +export interface ColorTheme { + heating: ColorMode; + regulating: ColorMode; + standby: ColorMode; + startup: ColorMode; +} + +export interface ColorMode { + animation: number; + color1: { + blue: number; + green: number; + red: number; + }; + color2: { + blue: number; + green: number; + red: number; + }; + frequency: number; +}