mirror of
https://github.com/bourquep/mysa2mqtt.git
synced 2025-12-16 13:12:34 +00:00
feat: Added partial support for AC-V1-1 thermostat (#74)
## Add support for AC-V1-1 Devices This PR follows https://github.com/bourquep/mysa-js-sdk/pull/156 and should be merged once the `mysa-js-sdk` is updated and we probably need to bump the version in here too? ### What Changed - Added detection for AC model devices (`isAC`) and extended mode handling: - Supports additional modes: `cool`, `dry`, `fan_only`, and `auto` (alongside `heat` and `off`). - Supports fans speed for fan compatible modes with 4 speeds as advertised on the Mysa app ** Note: I'm no typescript expert so code might not look the best ** ### Testing - Verified mode changes and temperature updates for both heat-only and AC devices. - Verified fan speed changes for AC devices - Verified action changes for both heat-only and AC devices - Tested interactions from both Mysa app and HA https://github.com/user-attachments/assets/ada413dc-c681-49de-af09-7b21826be8f3 ### Next Steps - Add full AC-V1 support: - Swing / vane control --------- Co-authored-by: Pascal Bourque <pascal@cosmos.moi>
This commit is contained in:
14
README.md
14
README.md
@@ -24,13 +24,13 @@ home automation platforms.
|
||||
|
||||
## Supported hardware
|
||||
|
||||
| Model Number | Description | Supported |
|
||||
| ------------ | --------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
|
||||
| `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
|
||||
| `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not report power consumption |
|
||||
| `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
|
||||
| `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | 🚫 Not supported (yet) |
|
||||
| Model Number | Description | Supported |
|
||||
| ------------ | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
|
||||
| `BB-V2-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V2 | ⚠️ Partially working, in progress |
|
||||
| `BB-V2-X-L` | Mysa Smart Thermostat LITE for Electric Baseboard Heaters | ⚠️ Partially working, in progress; does not report power consumption |
|
||||
| `unknown` | Mysa Smart Thermostat for Electric In-Floor Heating | ⚠️ Should work but not tested |
|
||||
| `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | ⚠️ Partially working, in progress; missing swing and position functions |
|
||||
|
||||
## Disclaimer
|
||||
|
||||
|
||||
@@ -30,9 +30,39 @@ import {
|
||||
OriginConfiguration,
|
||||
Sensor
|
||||
} from 'mqtt2ha';
|
||||
import { DeviceBase, FirmwareDevice, MysaApiClient, MysaDeviceMode, StateChange, Status } from 'mysa-js-sdk';
|
||||
import {
|
||||
DeviceBase,
|
||||
FirmwareDevice,
|
||||
MysaApiClient,
|
||||
MysaDeviceMode,
|
||||
MysaFanSpeedMode,
|
||||
StateChange,
|
||||
Status
|
||||
} from 'mysa-js-sdk';
|
||||
import { version } from './options';
|
||||
|
||||
type DeviceType = 'AC' | 'BB';
|
||||
|
||||
const HA_HEAT_ONLY_MODES: Partial<MysaDeviceMode>[] = ['off', 'heat'];
|
||||
const HA_AC_MODES: Partial<MysaDeviceMode>[] = ['off', 'heat', 'cool', 'dry', 'fan_only', 'auto'];
|
||||
const MYSA_RAW_MODE_TO_DEVICE_MODE: Partial<Record<number, MysaDeviceMode>> = {
|
||||
1: 'off',
|
||||
2: 'auto',
|
||||
3: 'heat',
|
||||
4: 'cool',
|
||||
5: 'fan_only',
|
||||
6: 'dry'
|
||||
};
|
||||
|
||||
const FAN_SPEED_MODES: Partial<MysaFanSpeedMode>[] = ['auto', 'low', 'medium', 'high', 'max'];
|
||||
const MYSA_RAW_FAN_SPEED_TO_FAN_SPEED_MODE: Partial<Record<number, MysaFanSpeedMode>> = {
|
||||
1: 'auto',
|
||||
3: 'low',
|
||||
5: 'medium',
|
||||
7: 'high',
|
||||
8: 'max'
|
||||
};
|
||||
|
||||
export class Thermostat {
|
||||
private isStarted = false;
|
||||
private readonly mqttDevice: DeviceConfiguration;
|
||||
@@ -45,6 +75,8 @@ export class Thermostat {
|
||||
private readonly mysaStatusUpdateHandler = this.handleMysaStatusUpdate.bind(this);
|
||||
private readonly mysaStateChangeHandler = this.handleMysaStateChange.bind(this);
|
||||
|
||||
private readonly deviceType: DeviceType;
|
||||
|
||||
constructor(
|
||||
public readonly mysaApiClient: MysaApiClient,
|
||||
public readonly mysaDevice: DeviceBase,
|
||||
@@ -71,6 +103,9 @@ export class Thermostat {
|
||||
support_url: 'https://github.com/bourquep/mysa2mqtt'
|
||||
};
|
||||
|
||||
const isAC = mysaDevice.Model.startsWith('AC');
|
||||
this.deviceType = isAC ? 'AC' : 'BB';
|
||||
|
||||
this.mqttClimate = new Climate(
|
||||
{
|
||||
mqtt: this.mqttSettings,
|
||||
@@ -83,37 +118,54 @@ export class Thermostat {
|
||||
name: 'Thermostat',
|
||||
min_temp: mysaDevice.MinSetpoint,
|
||||
max_temp: mysaDevice.MaxSetpoint,
|
||||
modes: ['off', 'heat'], // TODO: AC
|
||||
modes: isAC ? HA_AC_MODES : HA_HEAT_ONLY_MODES,
|
||||
fan_modes: isAC ? FAN_SPEED_MODES : undefined,
|
||||
precision: is_celsius ? 0.1 : 1.0,
|
||||
temp_step: is_celsius ? 0.5 : 1.0,
|
||||
temperature_unit: 'C',
|
||||
optimistic: true
|
||||
}
|
||||
},
|
||||
[
|
||||
'action_topic',
|
||||
'current_humidity_topic',
|
||||
'current_temperature_topic',
|
||||
'mode_state_topic',
|
||||
'temperature_state_topic'
|
||||
],
|
||||
isAC
|
||||
? [
|
||||
'action_topic',
|
||||
'current_humidity_topic',
|
||||
'current_temperature_topic',
|
||||
'mode_state_topic',
|
||||
'temperature_state_topic',
|
||||
'fan_mode_state_topic'
|
||||
]
|
||||
: [
|
||||
'action_topic',
|
||||
'current_humidity_topic',
|
||||
'current_temperature_topic',
|
||||
'mode_state_topic',
|
||||
'temperature_state_topic'
|
||||
],
|
||||
async () => {},
|
||||
['mode_command_topic', 'power_command_topic', 'temperature_command_topic'],
|
||||
isAC
|
||||
? ['mode_command_topic', 'power_command_topic', 'temperature_command_topic', 'fan_mode_command_topic']
|
||||
: ['mode_command_topic', 'power_command_topic', 'temperature_command_topic'],
|
||||
async (topic, message) => {
|
||||
switch (topic) {
|
||||
case 'mode_command_topic':
|
||||
this.mysaApiClient.setDeviceState(
|
||||
this.mysaDevice.Id,
|
||||
undefined,
|
||||
message === 'off' ? 'off' : message === 'heat' ? 'heat' : undefined
|
||||
);
|
||||
case 'mode_command_topic': {
|
||||
const messageAsMode = message as MysaDeviceMode;
|
||||
const mode: MysaDeviceMode | undefined = isAC
|
||||
? HA_AC_MODES.includes(messageAsMode)
|
||||
? messageAsMode
|
||||
: undefined
|
||||
: HA_HEAT_ONLY_MODES.includes(messageAsMode)
|
||||
? messageAsMode
|
||||
: undefined;
|
||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, mode);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'power_command_topic':
|
||||
this.mysaApiClient.setDeviceState(
|
||||
this.mysaDevice.Id,
|
||||
undefined,
|
||||
message === 'OFF' ? 'off' : message === 'ON' ? 'heat' : undefined
|
||||
message === 'OFF' ? 'off' : message === 'ON' && !isAC ? 'heat' : undefined
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -134,6 +186,13 @@ export class Thermostat {
|
||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, temperature, undefined);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fan_mode_command_topic': {
|
||||
const messageAsMode = message as MysaFanSpeedMode;
|
||||
const mode = FAN_SPEED_MODES.includes(messageAsMode) ? messageAsMode : undefined;
|
||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined, mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -200,13 +259,16 @@ export class Thermostat {
|
||||
try {
|
||||
const deviceStates = await this.mysaApiClient.getDeviceStates();
|
||||
const state = deviceStates.DeviceStatesObj[this.mysaDevice.Id];
|
||||
const tstatMode = state.TstatMode?.v;
|
||||
|
||||
this.mqttClimate.currentTemperature = state.CorrectedTemp?.v;
|
||||
this.mqttClimate.currentHumidity = state.Humidity?.v;
|
||||
this.mqttClimate.currentMode = tstatMode === 1 ? 'off' : tstatMode === 3 ? 'heat' : undefined;
|
||||
this.mqttClimate.currentMode =
|
||||
MYSA_RAW_MODE_TO_DEVICE_MODE[state.TstatMode?.v as number] ?? this.mqttClimate.currentMode;
|
||||
this.mqttClimate.currentFanMode =
|
||||
MYSA_RAW_FAN_SPEED_TO_FAN_SPEED_MODE[state.FanSpeed?.v as number] ?? this.mqttClimate.currentFanMode;
|
||||
this.mqttClimate.currentAction = this.computeCurrentAction(undefined, state.Duty?.v);
|
||||
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? state.SetPoint?.v : undefined;
|
||||
|
||||
await this.mqttClimate.writeConfig();
|
||||
|
||||
await this.mqttTemperature.setState(
|
||||
@@ -280,29 +342,52 @@ export class Thermostat {
|
||||
this.mqttClimate.currentMode = 'off';
|
||||
this.mqttClimate.currentAction = 'off';
|
||||
this.mqttClimate.targetTemperature = undefined;
|
||||
this.mqttClimate.currentFanMode = undefined;
|
||||
break;
|
||||
|
||||
case 'heat':
|
||||
this.mqttClimate.currentMode = 'heat';
|
||||
case 'cool':
|
||||
case 'auto':
|
||||
this.mqttClimate.currentMode = state.mode;
|
||||
if (this.deviceType === 'AC') {
|
||||
this.mqttClimate.currentAction = this.computeCurrentAction();
|
||||
}
|
||||
this.mqttClimate.targetTemperature = state.setPoint;
|
||||
this.mqttClimate.currentFanMode = state.fanSpeed;
|
||||
break;
|
||||
|
||||
case 'dry':
|
||||
case 'fan_only':
|
||||
this.mqttClimate.currentMode = state.mode;
|
||||
this.mqttClimate.currentAction = this.computeCurrentAction();
|
||||
this.mqttClimate.currentFanMode = state.fanSpeed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private computeCurrentAction(current?: number, dutyCycle?: number): ClimateAction {
|
||||
const mode: MysaDeviceMode | undefined =
|
||||
this.mqttClimate.currentMode === 'heat' ? 'heat' : this.mqttClimate.currentMode === 'off' ? 'off' : undefined;
|
||||
const currentModeAsMode = this.mqttClimate.currentMode as MysaDeviceMode;
|
||||
const mode = HA_AC_MODES.includes(currentModeAsMode) ? currentModeAsMode : undefined;
|
||||
|
||||
switch (mode) {
|
||||
case 'off':
|
||||
return 'off';
|
||||
|
||||
case 'heat':
|
||||
if (current != null) {
|
||||
return current > 0 ? 'heating' : 'idle';
|
||||
switch (this.deviceType) {
|
||||
case 'BB':
|
||||
if (current != null) {
|
||||
return current > 0 ? 'heating' : 'idle';
|
||||
}
|
||||
return (dutyCycle ?? 0) > 0 ? 'heating' : 'idle';
|
||||
default:
|
||||
return 'heating';
|
||||
}
|
||||
return (dutyCycle ?? 0) > 0 ? 'heating' : 'idle';
|
||||
|
||||
case 'cool':
|
||||
return 'cooling';
|
||||
case 'fan_only':
|
||||
return 'fan';
|
||||
case 'dry':
|
||||
return 'drying';
|
||||
default:
|
||||
return 'idle';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user