mirror of
https://github.com/bourquep/mysa-js-sdk.git
synced 2026-02-04 01:31:05 +00:00
feat: Partial support for the AC-V1-1 thermostat (#156)
## Feat. Add Support for Mysa AC-V1-1 Devices
### Overview
This PR aims to extend **mysa2mqtt** to support **Mysa AC-V1-1**
thermostats in addition to the existing baseboard models.
AC-V1 devices use different operating modes and fan modes, which
required updates to both mode translation and MQTT behavior.
Tested with `BB-V1-1` and `AC-V1-1`.
*Note: I'm no typescript expert so code might not look the best but is
fully tested*
### Key Changes
- Supports `cool`, `dry`, `fan_only`, and `auto` in addition to `off`
and `heat`.
- New fan modes: `auto`, `low`, `medium`, `high`, and `max`.
### Does not support yet
- Vertical swing
- Horizontal swing
### Technical stuff
`AC-V1-1` payload:
`"body":{"success":1,"type":2,"trig_src":3,"state":{"md":3,"sp":23.5,"lk":0,"ho":1,"br":100,"da":2,"fn":5,"ss":4,"ssh":12,"it":0}}}}`
Fan mode values: 1 = 'auto', 3 = 'low', 5 = 'medium', 7 = 'high', 8 =
'max'
**I named the value 8 max as I needed a 4th value but is not tied to
anything in HA or Mysa**
### Testing
```
npm run example
[23:13:06.300] INFO (example/3281203): [example] 'Office Room' status changed: 21.9°C, 49%, 0W
[23:13:21.701] INFO (example/3281203): [example] 'Office Room' status changed: 21.9°C, 49%, 0W
[23:13:21.938] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"heat","setPoint":23,"fanSpeed":"auto"}
[23:13:33.282] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"heat","setPoint":23.5,"fanSpeed":"auto"}
[23:13:38.132] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"heat","setPoint":23.5,"fanSpeed":"high"}
[23:13:44.380] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"fan_only","setPoint":23.5,"fanSpeed":"high"}
[23:13:52.609] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"cool","setPoint":23.5,"fanSpeed":"high"}
[23:13:57.942] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"heat","setPoint":23.5,"fanSpeed":"high"}
[23:14:01.052] INFO (example/3281203): [example] 'Family Room' state changed. {"deviceId":"<redacted>","mode":"heat","setPoint":23.5,"fanSpeed":"auto"}
```
PR to `mysa2mqtt` coming right after
This commit is contained in:
@@ -25,7 +25,7 @@ import { MqttPublishError, MysaApiError, UnauthenticatedError } from './Errors';
|
|||||||
import { Logger, VoidLogger } from './Logger';
|
import { Logger, VoidLogger } from './Logger';
|
||||||
import { MysaApiClientEventTypes } from './MysaApiClientEventTypes';
|
import { MysaApiClientEventTypes } from './MysaApiClientEventTypes';
|
||||||
import { MysaApiClientOptions } from './MysaApiClientOptions';
|
import { MysaApiClientOptions } from './MysaApiClientOptions';
|
||||||
import { MysaDeviceMode } from './MysaDeviceMode';
|
import { MysaDeviceMode, MysaFanSpeedMode } from './MysaDeviceMode';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
@@ -357,15 +357,20 @@ export class MysaApiClient {
|
|||||||
*
|
*
|
||||||
* // Set temperature and mode
|
* // Set temperature and mode
|
||||||
* await client.setDeviceState('device123', 20, 'heat');
|
* await client.setDeviceState('device123', 20, 'heat');
|
||||||
|
*
|
||||||
|
* // Set fan speed
|
||||||
|
* await client.setDeviceState('device123', undefined, undefined, 'auto');
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param deviceId - The ID of the device to control.
|
* @param deviceId - The ID of the device to control.
|
||||||
* @param setPoint - The target temperature set point (optional).
|
* @param setPoint - The target temperature set point (optional).
|
||||||
* @param mode - The operating mode to set ('off', 'heat', or undefined to leave unchanged).
|
* @param mode - The operating mode to set (one of MysaDeviceMode values, or undefined to leave unchanged).
|
||||||
|
* @param fanSpeed - The fan speed mode to set ('low', 'medium', 'high', 'max', 'auto', or undefined to leave
|
||||||
|
* unchanged).
|
||||||
* @throws {@link UnauthenticatedError} When the user is not authenticated.
|
* @throws {@link UnauthenticatedError} When the user is not authenticated.
|
||||||
* @throws {@link Error} When MQTT connection or command sending fails.
|
* @throws {@link Error} When MQTT connection or command sending fails.
|
||||||
*/
|
*/
|
||||||
async setDeviceState(deviceId: string, setPoint?: number, mode?: MysaDeviceMode) {
|
async setDeviceState(deviceId: string, setPoint?: number, mode?: MysaDeviceMode, fanSpeed?: MysaFanSpeedMode) {
|
||||||
this._logger.debug(`Setting device state for '${deviceId}'`);
|
this._logger.debug(`Setting device state for '${deviceId}'`);
|
||||||
|
|
||||||
if (!this._cachedDevices) {
|
if (!this._cachedDevices) {
|
||||||
@@ -380,6 +385,9 @@ export class MysaApiClient {
|
|||||||
const now = dayjs();
|
const now = dayjs();
|
||||||
|
|
||||||
this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
|
this._logger.debug(`Sending request to set device state for '${deviceId}'...`);
|
||||||
|
const modeMap = { off: 1, auto: 2, heat: 3, cool: 4, fan_only: 5, dry: 6 };
|
||||||
|
const fanSpeedMap = { auto: 1, low: 3, medium: 5, high: 7, max: 8 };
|
||||||
|
|
||||||
const payload = serializeMqttPayload<ChangeDeviceState>({
|
const payload = serializeMqttPayload<ChangeDeviceState>({
|
||||||
msg: InMessageType.CHANGE_DEVICE_STATE,
|
msg: InMessageType.CHANGE_DEVICE_STATE,
|
||||||
id: now.valueOf(),
|
id: now.valueOf(),
|
||||||
@@ -398,16 +406,19 @@ export class MysaApiClient {
|
|||||||
ver: 1,
|
ver: 1,
|
||||||
type: device.Model.startsWith('BB-V1')
|
type: device.Model.startsWith('BB-V1')
|
||||||
? 1
|
? 1
|
||||||
: device.Model.startsWith('BB-V2')
|
: device.Model.startsWith('AC-V1')
|
||||||
? device.Model.endsWith('-L')
|
? 2
|
||||||
? 5
|
: device.Model.startsWith('BB-V2')
|
||||||
: 4
|
? device.Model.endsWith('-L')
|
||||||
: 0,
|
? 5
|
||||||
|
: 4
|
||||||
|
: 0,
|
||||||
cmd: [
|
cmd: [
|
||||||
{
|
{
|
||||||
tm: -1,
|
tm: -1,
|
||||||
sp: setPoint,
|
sp: setPoint,
|
||||||
md: mode === 'off' ? 1 : mode === 'heat' ? 3 : undefined
|
md: mode ? modeMap[mode] : undefined,
|
||||||
|
fn: fanSpeed ? fanSpeedMap[fanSpeed] : undefined
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -792,13 +803,31 @@ export class MysaApiClient {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case OutMessageType.DEVICE_STATE_CHANGE:
|
case OutMessageType.DEVICE_STATE_CHANGE: {
|
||||||
|
const modeMap: Record<number, MysaDeviceMode> = {
|
||||||
|
1: 'off',
|
||||||
|
2: 'auto',
|
||||||
|
3: 'heat',
|
||||||
|
4: 'cool',
|
||||||
|
5: 'fan_only',
|
||||||
|
6: 'dry'
|
||||||
|
};
|
||||||
|
const fanSpeedMap: Record<number, MysaFanSpeedMode> = {
|
||||||
|
1: 'auto',
|
||||||
|
3: 'low',
|
||||||
|
5: 'medium',
|
||||||
|
7: 'high',
|
||||||
|
8: 'max'
|
||||||
|
};
|
||||||
|
|
||||||
this.emitter.emit('stateChanged', {
|
this.emitter.emit('stateChanged', {
|
||||||
deviceId: parsedPayload.src.ref,
|
deviceId: parsedPayload.src.ref,
|
||||||
mode: parsedPayload.body.state.md === 1 ? 'off' : parsedPayload.body.state.md === 3 ? 'heat' : undefined,
|
mode: parsedPayload.body.state.md ? modeMap[parsedPayload.body.state.md] : undefined,
|
||||||
setPoint: parsedPayload.body.state.sp
|
setPoint: parsedPayload.body.state.sp,
|
||||||
|
fanSpeed: parsedPayload.body.state.fn !== undefined ? fanSpeedMap[parsedPayload.body.state.fn] : undefined
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -4,4 +4,11 @@
|
|||||||
* Defines the possible operational states that a Mysa thermostat or heating device can be set to. These modes control
|
* Defines the possible operational states that a Mysa thermostat or heating device can be set to. These modes control
|
||||||
* the device's heating behavior and power consumption.
|
* the device's heating behavior and power consumption.
|
||||||
*/
|
*/
|
||||||
export type MysaDeviceMode = 'off' | 'heat';
|
export type MysaDeviceMode = 'off' | 'heat' | 'cool' | 'dry' | 'fan_only' | 'auto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Union type representing the available fan speed modes for Mysa devices.
|
||||||
|
*
|
||||||
|
* Defines the possible fan speed states that a Mysa thermostat device can be set to.
|
||||||
|
*/
|
||||||
|
export type MysaFanSpeedMode = 'auto' | 'low' | 'medium' | 'high' | 'max';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MysaDeviceMode } from '@/api/MysaDeviceMode';
|
import { MysaDeviceMode, MysaFanSpeedMode } from '@/api/MysaDeviceMode';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface representing a device state change event for a Mysa device.
|
* Interface representing a device state change event for a Mysa device.
|
||||||
@@ -14,4 +14,6 @@ export interface StateChange {
|
|||||||
mode?: MysaDeviceMode;
|
mode?: MysaDeviceMode;
|
||||||
/** Current temperature setpoint after the state change */
|
/** Current temperature setpoint after the state change */
|
||||||
setPoint: number;
|
setPoint: number;
|
||||||
|
/** Optional fan speed (1 = auto, 3 = low, 5 = medium, 7 = high, 8 = max). AC only */
|
||||||
|
fanSpeed?: MysaFanSpeedMode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export interface ChangeDeviceState extends MsgPayload<InMessageType.CHANGE_DEVIC
|
|||||||
md?: number;
|
md?: number;
|
||||||
/** Unknown, should always be -1 */
|
/** Unknown, should always be -1 */
|
||||||
tm: number;
|
tm: number;
|
||||||
|
/** Optional fan speed (1 = auto, 3 = low, 5 = medium, 7 = high, 8 = max). AC only */
|
||||||
|
fn?: number;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -25,10 +25,12 @@ export interface DeviceStateChange extends MsgPayload<OutMessageType.DEVICE_STAT
|
|||||||
ho: number;
|
ho: number;
|
||||||
/** Unknown */
|
/** Unknown */
|
||||||
lk: number;
|
lk: number;
|
||||||
/** Device mode (1 = OFF, 3 = HEAT) */
|
/** Device mode (1 = OFF, 2 = AUTO, 3 = HEAT, 4 = COOL, 5 = FAN_ONLY, 6 = DRY) */
|
||||||
md: number;
|
md: number;
|
||||||
/** Temperature setpoint */
|
/** Temperature setpoint */
|
||||||
sp: number;
|
sp: number;
|
||||||
|
/** Optional fan speed (1 = auto, 3 = low, 5 = medium, 7 = high, 8 = max). AC only */
|
||||||
|
fn?: number;
|
||||||
};
|
};
|
||||||
/** Success indicator for the state change operation (1 = success, 0 = failure) */
|
/** Success indicator for the state change operation (1 = success, 0 = failure) */
|
||||||
success: number;
|
success: number;
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ export interface DeviceState {
|
|||||||
Humidity: TimestampedValue<number>;
|
Humidity: TimestampedValue<number>;
|
||||||
/** Lock status */
|
/** Lock status */
|
||||||
Lock: TimestampedValue<number>;
|
Lock: TimestampedValue<number>;
|
||||||
|
/** Fan speed */
|
||||||
|
FanSpeed?: TimestampedValue<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user