summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLi Zhineng <[email protected]>2025-07-18 11:01:00 +0800
committerLi Zhineng <[email protected]>2025-07-18 11:01:00 +0800
commit9ebb82d9e8f41cf5a6b908e45ad6cfce8c747752 (patch)
tree0ed9f6a99662564a3880531ab35e26d448ee9ece
parent80abac4a1c2cfe001b3553c1b77f60d74cfb79f9 (diff)
downloadairmx-9ebb82d9e8f41cf5a6b908e45ad6cfce8c747752.tar.gz
airmx-9ebb82d9e8f41cf5a6b908e45ad6cfce8c747752.zip
apply fixes by Prettier
-rw-r--r--packages/airmx/src/airmx.test.ts61
-rw-r--r--packages/airmx/src/airmx.ts35
-rw-r--r--packages/airmx/src/eagle.test.ts21
-rw-r--r--packages/airmx/src/eagle.ts8
-rw-r--r--packages/airmx/src/messages.ts16
-rw-r--r--packages/airmx/src/snow.ts14
-rw-r--r--packages/airmx/src/types.ts16
-rw-r--r--packages/airmx/src/util.ts3
8 files changed, 107 insertions, 67 deletions
diff --git a/packages/airmx/src/airmx.test.ts b/packages/airmx/src/airmx.test.ts
index b184b80..9f726be 100644
--- a/packages/airmx/src/airmx.test.ts
+++ b/packages/airmx/src/airmx.test.ts
@@ -12,58 +12,67 @@ describe('topic', () => {
})
test('the topic format is expected to have 7 parts', () => {
- expect(() => Topic.parse('foo'))
- .toThrow('The topic format is expected to be airmx/+/+/+/+/+/+.')
+ expect(() => Topic.parse('foo')).toThrow(
+ 'The topic format is expected to be airmx/+/+/+/+/+/+.',
+ )
})
test('the 1st part is expected to be airmx', () => {
- expect(() => Topic.parse('foo/01/0/1/1/1/12345'))
- .toThrow('The 1st part of the topic must be "airmx".')
+ expect(() => Topic.parse('foo/01/0/1/1/1/12345')).toThrow(
+ 'The 1st part of the topic must be "airmx".',
+ )
})
test('the 2nd part is expected to be "01"', () => {
- expect(() => Topic.parse('airmx/00/0/1/1/1/12345'))
- .toThrow('The 2nd part of the topic must be "01".')
+ expect(() => Topic.parse('airmx/00/0/1/1/1/12345')).toThrow(
+ 'The 2nd part of the topic must be "01".',
+ )
})
test('the 3rd part is expected to be either "1" or "0"', () => {
expect(Topic.parse('airmx/01/0/1/1/1/12345')).toBeInstanceOf(Topic)
expect(Topic.parse('airmx/01/1/1/1/1/12345')).toBeInstanceOf(Topic)
- expect(() => Topic.parse('airmx/01/2/1/1/1/12345'))
- .toThrow('The 3rd part of the topic must be either "1" or "0".')
+ expect(() => Topic.parse('airmx/01/2/1/1/1/12345')).toThrow(
+ 'The 3rd part of the topic must be either "1" or "0".',
+ )
})
test('the 4th part is expected to be either "1" or "0"', () => {
expect(Topic.parse('airmx/01/0/0/1/1/12345')).toBeInstanceOf(Topic)
expect(Topic.parse('airmx/01/0/1/1/1/12345')).toBeInstanceOf(Topic)
- expect(() => Topic.parse('airmx/01/0/2/1/1/12345'))
- .toThrow('The 4th part of the topic must be either "1" or "0".')
+ expect(() => Topic.parse('airmx/01/0/2/1/1/12345')).toThrow(
+ 'The 4th part of the topic must be either "1" or "0".',
+ )
})
test('the 5th part is expected to be either "1" or "0"', () => {
expect(Topic.parse('airmx/01/0/1/0/1/12345')).toBeInstanceOf(Topic)
expect(Topic.parse('airmx/01/0/1/1/1/12345')).toBeInstanceOf(Topic)
- expect(() => Topic.parse('airmx/01/0/1/2/1/12345'))
- .toThrow('The 5th part of the topic must be either "1" or "0".')
+ expect(() => Topic.parse('airmx/01/0/1/2/1/12345')).toThrow(
+ 'The 5th part of the topic must be either "1" or "0".',
+ )
})
test('the 6th part is expected to be either "1" or "0"', () => {
expect(Topic.parse('airmx/01/0/1/1/0/12345')).toBeInstanceOf(Topic)
expect(Topic.parse('airmx/01/0/1/1/1/12345')).toBeInstanceOf(Topic)
- expect(() => Topic.parse('airmx/01/0/1/1/2/12345'))
- .toThrow('The 6th part of the topic must be either "1" or "0".')
+ expect(() => Topic.parse('airmx/01/0/1/1/2/12345')).toThrow(
+ 'The 6th part of the topic must be either "1" or "0".',
+ )
})
test('the 7th part is expected to be the device id', () => {
- expect(() => Topic.parse('airmx/01/0/1/1/1/'))
- .toThrow('The 7th part of the topic must be a device ID.')
+ expect(() => Topic.parse('airmx/01/0/1/1/1/')).toThrow(
+ 'The 7th part of the topic must be a device ID.',
+ )
- expect(() => Topic.parse('airmx/01/0/1/1/1/foo'))
- .toThrow('The 7th part of the topic must be a device ID.')
+ expect(() => Topic.parse('airmx/01/0/1/1/1/foo')).toThrow(
+ 'The 7th part of the topic must be a device ID.',
+ )
})
})
@@ -73,17 +82,17 @@ describe('airmx', () => {
beforeEach(() => {
mockMqttClient = {
on: jest.fn(),
- subscribe: jest.fn()
+ subscribe: jest.fn(),
} as unknown as jest.Mocked<MqttClient>
})
it('should subscribe to the topic when the client connects', () => {
new Airmx({ mqtt: mockMqttClient, devices: [] })
const connectHandler = mockMqttClient.on.mock.calls.find(
- ([event]) => event === 'connect'
- )?.[1] as (() => void) | undefined;
+ ([event]) => event === 'connect',
+ )?.[1] as (() => void) | undefined
connectHandler?.()
- expect(mockMqttClient.subscribe).toHaveBeenCalledWith('airmx/01/+/+/1/1/+');
+ expect(mockMqttClient.subscribe).toHaveBeenCalledWith('airmx/01/+/+/1/1/+')
})
describe('message validation', () => {
@@ -93,13 +102,15 @@ describe('airmx', () => {
beforeEach(() => {
new Airmx({ mqtt: mockMqttClient, devices: [testDevice] })
messageHandler = mockMqttClient.on.mock.calls.find(
- ([event]) => event === 'message'
+ ([event]) => event === 'message',
)?.[1] as ((topic: string, message: Buffer) => void) | undefined
expect(messageHandler).toBeDefined()
})
- const validMessage = '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"b8796682da77e8c929dddf7e6461afec"}'
- const invalidMessage = '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"invalid"}'
+ const validMessage =
+ '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"b8796682da77e8c929dddf7e6461afec"}'
+ const invalidMessage =
+ '{"cmdId": 210,"name":"eagleStatus","time":1752675701,"from":2,"data":{"version":"10.00.17","power":1,"heatStatus":0,"mode":0,"cadr":47,"prm":1320,"g4Percent": 100,"hepaPercent":100,"carbonId":"031","g4Id":"041","hepaId":"021","carbonPercent":17,"diffPressure1":99999,"diffPressure2":99999,"t0":35,"status":0,"denoise":1},"sig":"invalid"}'
it('should validate message signatures when receiving messages', () => {
expect(() => {
diff --git a/packages/airmx/src/airmx.ts b/packages/airmx/src/airmx.ts
index be67a58..5b397db 100644
--- a/packages/airmx/src/airmx.ts
+++ b/packages/airmx/src/airmx.ts
@@ -3,7 +3,12 @@ import { MqttClient } from 'mqtt'
import { EagleStatus } from './eagle.js'
import { EagleControlMesasge } from './messages.js'
import { SnowStatus } from './snow.js'
-import type { Config, SnowListener, EagleListener, EagleControlData } from './types.js'
+import type {
+ Config,
+ SnowListener,
+ EagleListener,
+ EagleControlData,
+} from './types.js'
import { Command } from './types.js'
import { Signer } from './util.js'
import type { CommandMessage } from './messages.js'
@@ -14,7 +19,7 @@ export class Topic {
public readonly unknown2: boolean,
public readonly unknown3: boolean,
public readonly unknown4: boolean,
- public readonly deviceId: number
+ public readonly deviceId: number,
) {
//
}
@@ -36,13 +41,15 @@ export class Topic {
for (let i = 2; i < 6; i++) {
if (components[i] !== '0' && components[i] !== '1') {
- const ordinal = `${i + 1}${(i + 1) === 3 ? 'rd' : 'th'}`
- throw new Error(`The ${ordinal} part of the topic must be either "1" or "0".`)
+ const ordinal = `${i + 1}${i + 1 === 3 ? 'rd' : 'th'}`
+ throw new Error(
+ `The ${ordinal} part of the topic must be either "1" or "0".`,
+ )
}
}
const deviceId = components[6]
- if (deviceId === '' || ! /^\d+$/.test(deviceId)) {
+ if (deviceId === '' || !/^\d+$/.test(deviceId)) {
throw new Error('The 7th part of the topic must be a device ID.')
}
@@ -51,27 +58,25 @@ export class Topic {
components[3] === '1',
components[4] === '1',
components[5] === '1',
- +deviceId
+ +deviceId,
)
}
}
export class Airmx {
#listeners: {
- eagle: EagleListener[],
+ eagle: EagleListener[]
snow: SnowListener[]
} = {
eagle: [],
- snow: []
+ snow: [],
}
#client: MqttClient
#signer
- constructor(
- private readonly config: Config
- ) {
+ constructor(private readonly config: Config) {
this.#client = this.config.mqtt
this.#client.on('connect', this.#handleConnect.bind(this))
this.#client.on('message', this.#handleMessage.bind(this))
@@ -126,7 +131,8 @@ export class Airmx {
#validateMessage(deviceId: number, message: string, sig: string) {
const device = this.#getDevice(deviceId)
const plainText = message.slice(1, message.lastIndexOf('"sig"'))
- const calculated = crypto.createHash('md5')
+ const calculated = crypto
+ .createHash('md5')
.update(plainText)
.update(device.key)
.digest('hex')
@@ -143,7 +149,10 @@ export class Airmx {
const device = this.#getDevice(deviceId)
const sig = this.#signer.sign(message, device.key)
const payload = { ...message.payload(), sig }
- this.#client.publish(`airmx/01/1/1/0/1/${deviceId}`, JSON.stringify(payload))
+ this.#client.publish(
+ `airmx/01/1/1/0/1/${deviceId}`,
+ JSON.stringify(payload),
+ )
}
#getDevice(deviceId: number) {
diff --git a/packages/airmx/src/eagle.test.ts b/packages/airmx/src/eagle.test.ts
index ca33dae..84c5ebb 100644
--- a/packages/airmx/src/eagle.test.ts
+++ b/packages/airmx/src/eagle.test.ts
@@ -39,7 +39,10 @@ test('isOff determines if the power is off', () => {
})
test('mode 2 is the silent mode', () => {
- const status = new EagleStatus(12345, createStubStatusData({ mode: EagleMode.Silent }))
+ const status = new EagleStatus(
+ 12345,
+ createStubStatusData({ mode: EagleMode.Silent }),
+ )
expect(status.isSilentMode()).toBe(true)
})
@@ -71,26 +74,26 @@ const createStubStatusData = (data: Partial<EagleStatusData> = {}) => ({
cmdId: 210,
data: {
cadr: 17,
- carbonId: "0222222",
+ carbonId: '0222222',
carbonPercent: 30,
denoise: 0,
diffPressure1: 99999,
diffPressure2: 99999,
- g4Id: "0111111",
+ g4Id: '0111111',
g4Percent: 20,
heatStatus: 0,
- hepaId: "0333333",
+ hepaId: '0333333',
hepaPercent: 40,
mode: 2,
power: 1,
prm: 660,
status: 0,
t0: 28,
- version: "10.00.17",
- ...data
+ version: '10.00.17',
+ ...data,
},
from: 2,
- name: "eagleStatus",
- sig: "foo",
- time: 1700000000
+ name: 'eagleStatus',
+ sig: 'foo',
+ time: 1700000000,
})
diff --git a/packages/airmx/src/eagle.ts b/packages/airmx/src/eagle.ts
index d50096c..b3f28e3 100644
--- a/packages/airmx/src/eagle.ts
+++ b/packages/airmx/src/eagle.ts
@@ -4,7 +4,7 @@ import { EagleMode } from './types.js'
export class EagleStatus {
constructor(
public readonly deviceId: number,
- public readonly message: Message<EagleStatusData>
+ public readonly message: Message<EagleStatusData>,
) {}
static from(deviceId: number, message: Message<EagleStatusData>) {
@@ -24,7 +24,7 @@ export class EagleStatus {
}
isOff() {
- return ! this.isOn()
+ return !this.isOn()
}
get mode() {
@@ -48,7 +48,7 @@ export class EagleStatus {
}
isDenoiseOff() {
- return ! this.isDenoiseOn()
+ return !this.isDenoiseOn()
}
get heatStatus() {
@@ -60,7 +60,7 @@ export class EagleStatus {
}
isHeaterOff() {
- return ! this.isHeaterOn()
+ return !this.isHeaterOn()
}
get cadr() {
diff --git a/packages/airmx/src/messages.ts b/packages/airmx/src/messages.ts
index 6bac4f5..66c63d1 100644
--- a/packages/airmx/src/messages.ts
+++ b/packages/airmx/src/messages.ts
@@ -7,7 +7,7 @@ export class CommandMessage<T> {
readonly commandName: string,
readonly data: T,
readonly time: number,
- readonly from: number
+ readonly from: number,
) {}
payload() {
@@ -37,10 +37,14 @@ export class InstantPushMessage extends CommandMessage<InstantPushData> {
static make(frequency: number, duration: number) {
const data = {
frequencyTime: frequency,
- durationTime: duration
+ durationTime: duration,
}
return new InstantPushMessage(
- this.commandId(), 'instantPush', data, current(), MessageSource.App_Android
+ this.commandId(),
+ 'instantPush',
+ data,
+ current(),
+ MessageSource.App_Android,
)
}
}
@@ -53,7 +57,11 @@ export class EagleControlMesasge extends CommandMessage<EagleControlData> {
static make(data: EagleControlData) {
const timestamp = Math.floor(new Date().getTime() / 1000)
return new EagleControlMesasge(
- this.commandId(), 'control', data, timestamp, MessageSource.App_Android
+ this.commandId(),
+ 'control',
+ data,
+ timestamp,
+ MessageSource.App_Android,
)
}
}
diff --git a/packages/airmx/src/snow.ts b/packages/airmx/src/snow.ts
index 98191de..38242fa 100644
--- a/packages/airmx/src/snow.ts
+++ b/packages/airmx/src/snow.ts
@@ -1,16 +1,24 @@
-import { Command, type Message, type SnowStatusData, BatteryState, SensorState } from './types.js'
+import {
+ Command,
+ type Message,
+ type SnowStatusData,
+ BatteryState,
+ SensorState,
+} from './types.js'
export class SnowStatus {
constructor(
public readonly deviceId: number,
- public readonly message: Message<SnowStatusData>
+ public readonly message: Message<SnowStatusData>,
) {
//
}
static from(deviceId: number, message: Message<SnowStatusData>) {
if (message.cmdId !== Command.SnowStatus) {
- throw new Error(`Snow status expects a message with command ID "${Command.SnowStatus}".`)
+ throw new Error(
+ `Snow status expects a message with command ID "${Command.SnowStatus}".`,
+ )
}
return new this(deviceId, message)
diff --git a/packages/airmx/src/types.ts b/packages/airmx/src/types.ts
index 5d1e01e..6d6bbc7 100644
--- a/packages/airmx/src/types.ts
+++ b/packages/airmx/src/types.ts
@@ -17,14 +17,14 @@ export type EagleListener = (status: EagleStatus) => void
export enum Command {
SnowStatus = 200,
- EagleStatus = 210
+ EagleStatus = 210,
}
export enum MessageSource {
Snow = 1,
Eagle = 2,
App_iOS = 3,
- App_Android = 4
+ App_Android = 4,
}
export interface Message<T> {
@@ -45,9 +45,9 @@ export interface EagleStatusData {
heatStatus: number
cadr: number
prm: number
- diffPressure1: number,
- diffPressure2: number,
- t0: number,
+ diffPressure1: number
+ diffPressure2: number
+ t0: number
g4Id: string
g4Percent: number
carbonId: string
@@ -57,16 +57,16 @@ export interface EagleStatusData {
}
export enum EagleMode {
- Silent = 2
+ Silent = 2,
}
export enum SensorState {
- Sampling = 'sampling'
+ Sampling = 'sampling',
}
export enum BatteryState {
Charging = 'charging',
- Discharge = 'discharge'
+ Discharge = 'discharge',
}
export interface SnowStatusData {
diff --git a/packages/airmx/src/util.ts b/packages/airmx/src/util.ts
index 0b21b0d..496ae56 100644
--- a/packages/airmx/src/util.ts
+++ b/packages/airmx/src/util.ts
@@ -4,7 +4,8 @@ import type { CommandMessage } from './messages.js'
export class Signer {
sign(message: CommandMessage<unknown>, key: string) {
const plainText = JSON.stringify(message.payload())
- return crypto.createHash('md5')
+ return crypto
+ .createHash('md5')
.update(plainText.slice(1, -1))
.update(',')
.update(key)