mirror of
https://github.com/bourquep/mysa2mqtt.git
synced 2025-12-17 05:32:38 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
750a5c4ebf | ||
|
|
60b1df23b2 | ||
|
|
02ab0863ba | ||
|
|
2e93ec8f3d | ||
|
|
c24a0da4d1 | ||
|
|
2ff6d00999 | ||
|
|
da90344ffc | ||
|
|
4069f24880 | ||
|
|
a47cdbb45e | ||
|
|
2e075cd40d | ||
|
|
f2d35a1ca5 | ||
|
|
122ffde2f1 | ||
|
|
c259557da0 | ||
|
|
57bd430c98 | ||
|
|
8ca80acb49 |
22
.all-contributorsrc
Normal file
22
.all-contributorsrc
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"files": ["README.md"],
|
||||||
|
"imageSize": 100,
|
||||||
|
"commit": false,
|
||||||
|
"commitType": "docs",
|
||||||
|
"commitConvention": "angular",
|
||||||
|
"contributors": [
|
||||||
|
{
|
||||||
|
"login": "remiolivier",
|
||||||
|
"name": "remiolivier",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1379047?v=4",
|
||||||
|
"profile": "https://github.com/remiolivier",
|
||||||
|
"contributions": ["code"]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributorsPerLine": 7,
|
||||||
|
"skipCi": true,
|
||||||
|
"repoType": "github",
|
||||||
|
"repoHost": "https://github.com",
|
||||||
|
"projectName": "mysa2mqtt",
|
||||||
|
"projectOwner": "bourquep"
|
||||||
|
}
|
||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -8,7 +8,7 @@ updates:
|
|||||||
- package-ecosystem: 'npm' # See documentation for possible values
|
- package-ecosystem: 'npm' # See documentation for possible values
|
||||||
directory: '/' # Location of package manifests
|
directory: '/' # Location of package manifests
|
||||||
schedule:
|
schedule:
|
||||||
interval: 'daily'
|
interval: 'weekly'
|
||||||
labels:
|
labels:
|
||||||
- 'dependencies'
|
- 'dependencies'
|
||||||
commit-message:
|
commit-message:
|
||||||
|
|||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
README.md
|
||||||
49
README.md
49
README.md
@@ -1,5 +1,11 @@
|
|||||||
# mysa2mqtt
|
# mysa2mqtt
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
|
||||||
|
[](#contributors-)
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
[](https://www.npmjs.com/package/mysa2mqtt)
|
[](https://www.npmjs.com/package/mysa2mqtt)
|
||||||
[](https://hub.docker.com/r/bourquep/mysa2mqtt)
|
[](https://hub.docker.com/r/bourquep/mysa2mqtt)
|
||||||
[](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql)
|
[](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql)
|
||||||
@@ -18,13 +24,13 @@ home automation platforms.
|
|||||||
|
|
||||||
## Supported hardware
|
## Supported hardware
|
||||||
|
|
||||||
| Model Number | Description | Supported |
|
| Model Number | Description | Supported |
|
||||||
| ------------ | --------------------------------------------------------- | -------------------------------------------------------------------- |
|
| ------------ | --------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||||
| `BB-V1-X` | Mysa Smart Thermostat for Electric Baseboard Heaters V1 | ✅ Tested and working |
|
| `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` | 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 |
|
| `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 |
|
| `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) |
|
| `AC-V1-X` | Mysa Smart Thermostat for Mini-Split Heat Pumps & AC | ⚠️ Partially working, in progress; missing swing and position functions |
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
@@ -118,6 +124,11 @@ For development or custom modifications:
|
|||||||
The application can be configured using either command-line arguments or environment variables. Environment variables
|
The application can be configured using either command-line arguments or environment variables. Environment variables
|
||||||
take precedence over command-line defaults.
|
take precedence over command-line defaults.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The `M2M_TEMPERATURE_UNIT` option must match Home Assistant's unit system (Settings → General → Unit System)
|
||||||
|
> so setpoints and readings are interpreted correctly. If mismatched, climate entities will show incorrect values (e.g.
|
||||||
|
> 21°C treated as 21°F) and commands may result in unexpected temperatures.
|
||||||
|
|
||||||
### Required Configuration
|
### Required Configuration
|
||||||
|
|
||||||
| CLI Option | Environment Variable | Description |
|
| CLI Option | Environment Variable | Description |
|
||||||
@@ -145,6 +156,7 @@ take precedence over command-line defaults.
|
|||||||
| `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` |
|
| `-l, --log-level` | `M2M_LOG_LEVEL` | `info` | Log level: `silent`, `fatal`, `error`, `warn`, `info`, `debug`, `trace` |
|
||||||
| `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` |
|
| `-f, --log-format` | `M2M_LOG_FORMAT` | `pretty` | Log format: `pretty`, `json` |
|
||||||
| `-s, --mysa-session-file` | `M2M_MYSA_SESSION_FILE` | `session.json` | Path to Mysa session file |
|
| `-s, --mysa-session-file` | `M2M_MYSA_SESSION_FILE` | `session.json` | Path to Mysa session file |
|
||||||
|
| `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) |
|
||||||
|
|
||||||
## Usage Examples
|
## Usage Examples
|
||||||
|
|
||||||
@@ -369,3 +381,26 @@ copyright notice and license text in any copy of the software or substantial por
|
|||||||
- [mqtt2ha](https://github.com/bourquep/mqtt2ha) - MQTT to Home Assistant bridge library
|
- [mqtt2ha](https://github.com/bourquep/mqtt2ha) - MQTT to Home Assistant bridge library
|
||||||
- [Commander.js](https://github.com/tj/commander.js) - Command-line argument parsing
|
- [Commander.js](https://github.com/tj/commander.js) - Command-line argument parsing
|
||||||
- [Pino](https://github.com/pinojs/pino) - Fast JSON logger
|
- [Pino](https://github.com/pinojs/pino) - Fast JSON logger
|
||||||
|
|
||||||
|
## Contributors ✨
|
||||||
|
|
||||||
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
|
<!-- prettier-ignore-start -->
|
||||||
|
<!-- markdownlint-disable -->
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/remiolivier"><img src="https://avatars.githubusercontent.com/u/1379047?v=4?s=100" width="100px;" alt="remiolivier"/><br /><sub><b>remiolivier</b></sub></a><br /><a href="https://github.com/bourquep/mysa2mqtt/commits?author=remiolivier" title="Code">💻</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- markdownlint-restore -->
|
||||||
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
|
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification.
|
||||||
|
Contributions of any kind welcome!
|
||||||
|
|||||||
3498
package-lock.json
generated
3498
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -46,29 +46,29 @@
|
|||||||
"build": "tsup"
|
"build": "tsup"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"commander": "14.0.1",
|
"commander": "14.0.2",
|
||||||
"dotenv": "17.2.3",
|
"dotenv": "17.2.3",
|
||||||
"mqtt2ha": "4.1.2",
|
"mqtt2ha": "4.1.2",
|
||||||
"mysa-js-sdk": "1.3.3",
|
"mysa-js-sdk": "2.0.1",
|
||||||
"pino": "10.0.0",
|
"pino": "10.1.0",
|
||||||
"pino-pretty": "13.1.1"
|
"pino-pretty": "13.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commander-js/extra-typings": "14.0.0",
|
"@commander-js/extra-typings": "14.0.0",
|
||||||
"@eslint/js": "9.37.0",
|
"@eslint/js": "9.39.0",
|
||||||
"@semantic-release/npm": "12.0.2",
|
"@semantic-release/npm": "13.1.1",
|
||||||
"@types/node": "24.6.2",
|
"@types/node": "24.10.0",
|
||||||
"conventional-changelog-conventionalcommits": "9.1.0",
|
"conventional-changelog-conventionalcommits": "9.1.0",
|
||||||
"eslint": "9.37.0",
|
"eslint": "9.39.0",
|
||||||
"eslint-plugin-jsdoc": "60.8.1",
|
"eslint-plugin-jsdoc": "61.1.11",
|
||||||
"eslint-plugin-tsdoc": "0.4.0",
|
"eslint-plugin-tsdoc": "0.4.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"prettier-plugin-jsdoc": "1.3.3",
|
"prettier-plugin-jsdoc": "1.5.0",
|
||||||
"prettier-plugin-organize-imports": "4.3.0",
|
"prettier-plugin-organize-imports": "4.3.0",
|
||||||
"semantic-release": "24.2.9",
|
"semantic-release": "25.0.1",
|
||||||
"tsup": "8.5.0",
|
"tsup": "8.5.0",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.20.6",
|
||||||
"typescript": "5.9.3",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.45.0"
|
"typescript-eslint": "8.46.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ async function main() {
|
|||||||
mqttSettings,
|
mqttSettings,
|
||||||
new PinoLogger(rootLogger.child({ module: 'thermostat', deviceId: device.Id })),
|
new PinoLogger(rootLogger.child({ module: 'thermostat', deviceId: device.Id })),
|
||||||
firmwares.Firmware[device.Id],
|
firmwares.Firmware[device.Id],
|
||||||
serialNumbers.get(device.Id)
|
serialNumbers.get(device.Id),
|
||||||
|
options.temperatureUnit
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -143,5 +143,12 @@ export const options = new Command('mysa2mqtt')
|
|||||||
.default('mysa2mqtt')
|
.default('mysa2mqtt')
|
||||||
.helpGroup('MQTT')
|
.helpGroup('MQTT')
|
||||||
)
|
)
|
||||||
|
.addOption(
|
||||||
|
new Option('--temperature-unit <temperatureUnit>', 'temperature unit (C or F)')
|
||||||
|
.env('M2M_TEMPERATURE_UNIT')
|
||||||
|
.choices(['C', 'F'])
|
||||||
|
.default('C')
|
||||||
|
.helpGroup('Configuration')
|
||||||
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.opts();
|
.opts();
|
||||||
|
|||||||
@@ -30,9 +30,39 @@ import {
|
|||||||
OriginConfiguration,
|
OriginConfiguration,
|
||||||
Sensor
|
Sensor
|
||||||
} from 'mqtt2ha';
|
} 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';
|
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 {
|
export class Thermostat {
|
||||||
private isStarted = false;
|
private isStarted = false;
|
||||||
private readonly mqttDevice: DeviceConfiguration;
|
private readonly mqttDevice: DeviceConfiguration;
|
||||||
@@ -45,14 +75,19 @@ export class Thermostat {
|
|||||||
private readonly mysaStatusUpdateHandler = this.handleMysaStatusUpdate.bind(this);
|
private readonly mysaStatusUpdateHandler = this.handleMysaStatusUpdate.bind(this);
|
||||||
private readonly mysaStateChangeHandler = this.handleMysaStateChange.bind(this);
|
private readonly mysaStateChangeHandler = this.handleMysaStateChange.bind(this);
|
||||||
|
|
||||||
|
private readonly deviceType: DeviceType;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly mysaApiClient: MysaApiClient,
|
public readonly mysaApiClient: MysaApiClient,
|
||||||
public readonly mysaDevice: DeviceBase,
|
public readonly mysaDevice: DeviceBase,
|
||||||
private readonly mqttSettings: MqttSettings,
|
private readonly mqttSettings: MqttSettings,
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
public readonly mysaDeviceFirmware?: FirmwareDevice,
|
public readonly mysaDeviceFirmware?: FirmwareDevice,
|
||||||
public readonly mysaDeviceSerialNumber?: string
|
public readonly mysaDeviceSerialNumber?: string,
|
||||||
|
public readonly temperatureUnit?: 'C' | 'F'
|
||||||
) {
|
) {
|
||||||
|
const is_celsius = (temperatureUnit ?? 'C') === 'C';
|
||||||
|
|
||||||
this.mqttDevice = {
|
this.mqttDevice = {
|
||||||
identifiers: mysaDevice.Id,
|
identifiers: mysaDevice.Id,
|
||||||
name: mysaDevice.Name,
|
name: mysaDevice.Name,
|
||||||
@@ -68,6 +103,9 @@ export class Thermostat {
|
|||||||
support_url: 'https://github.com/bourquep/mysa2mqtt'
|
support_url: 'https://github.com/bourquep/mysa2mqtt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isAC = mysaDevice.Model.startsWith('AC');
|
||||||
|
this.deviceType = isAC ? 'AC' : 'BB';
|
||||||
|
|
||||||
this.mqttClimate = new Climate(
|
this.mqttClimate = new Climate(
|
||||||
{
|
{
|
||||||
mqtt: this.mqttSettings,
|
mqtt: this.mqttSettings,
|
||||||
@@ -80,37 +118,54 @@ export class Thermostat {
|
|||||||
name: 'Thermostat',
|
name: 'Thermostat',
|
||||||
min_temp: mysaDevice.MinSetpoint,
|
min_temp: mysaDevice.MinSetpoint,
|
||||||
max_temp: mysaDevice.MaxSetpoint,
|
max_temp: mysaDevice.MaxSetpoint,
|
||||||
modes: ['off', 'heat'], // TODO: AC
|
modes: isAC ? HA_AC_MODES : HA_HEAT_ONLY_MODES,
|
||||||
precision: 0.1,
|
fan_modes: isAC ? FAN_SPEED_MODES : undefined,
|
||||||
temp_step: 0.5,
|
precision: is_celsius ? 0.1 : 1.0,
|
||||||
temperature_unit: 'C', // TODO: Confirm that Mysa always works in C
|
temp_step: is_celsius ? 0.5 : 1.0,
|
||||||
|
temperature_unit: 'C',
|
||||||
optimistic: true
|
optimistic: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
isAC
|
||||||
'action_topic',
|
? [
|
||||||
'current_humidity_topic',
|
'action_topic',
|
||||||
'current_temperature_topic',
|
'current_humidity_topic',
|
||||||
'mode_state_topic',
|
'current_temperature_topic',
|
||||||
'temperature_state_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 () => {},
|
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) => {
|
async (topic, message) => {
|
||||||
switch (topic) {
|
switch (topic) {
|
||||||
case 'mode_command_topic':
|
case 'mode_command_topic': {
|
||||||
this.mysaApiClient.setDeviceState(
|
const messageAsMode = message as MysaDeviceMode;
|
||||||
this.mysaDevice.Id,
|
const mode: MysaDeviceMode | undefined = isAC
|
||||||
undefined,
|
? HA_AC_MODES.includes(messageAsMode)
|
||||||
message === 'off' ? 'off' : message === 'heat' ? 'heat' : undefined
|
? messageAsMode
|
||||||
);
|
: undefined
|
||||||
|
: HA_HEAT_ONLY_MODES.includes(messageAsMode)
|
||||||
|
? messageAsMode
|
||||||
|
: undefined;
|
||||||
|
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, mode);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'power_command_topic':
|
case 'power_command_topic':
|
||||||
this.mysaApiClient.setDeviceState(
|
this.mysaApiClient.setDeviceState(
|
||||||
this.mysaDevice.Id,
|
this.mysaDevice.Id,
|
||||||
undefined,
|
undefined,
|
||||||
message === 'OFF' ? 'off' : message === 'ON' ? 'heat' : undefined
|
message === 'OFF' ? 'off' : message === 'ON' && !isAC ? 'heat' : undefined
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -118,9 +173,26 @@ export class Thermostat {
|
|||||||
if (message === '') {
|
if (message === '') {
|
||||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined);
|
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined);
|
||||||
} else {
|
} else {
|
||||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, parseFloat(message), undefined);
|
let temperature = parseFloat(message);
|
||||||
|
|
||||||
|
if (!is_celsius) {
|
||||||
|
const snapHalfC = (c: number) => Math.round(c * 2) / 2;
|
||||||
|
const clamp = (v: number, min: number, max: number) => Math.min(max, Math.max(min, v));
|
||||||
|
// Snap to 0.5 °C and clamp to device limits
|
||||||
|
const setC = snapHalfC(temperature);
|
||||||
|
temperature = clamp(setC, this.mysaDevice.MinSetpoint ?? 0, this.mysaDevice.MaxSetpoint ?? 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, temperature, undefined);
|
||||||
}
|
}
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -137,7 +209,7 @@ export class Thermostat {
|
|||||||
device_class: 'temperature',
|
device_class: 'temperature',
|
||||||
state_class: 'measurement',
|
state_class: 'measurement',
|
||||||
unit_of_measurement: '°C',
|
unit_of_measurement: '°C',
|
||||||
suggested_display_precision: 1,
|
suggested_display_precision: is_celsius ? 0.1 : 0.0,
|
||||||
force_update: true
|
force_update: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -188,17 +260,24 @@ export class Thermostat {
|
|||||||
const deviceStates = await this.mysaApiClient.getDeviceStates();
|
const deviceStates = await this.mysaApiClient.getDeviceStates();
|
||||||
const state = deviceStates.DeviceStatesObj[this.mysaDevice.Id];
|
const state = deviceStates.DeviceStatesObj[this.mysaDevice.Id];
|
||||||
|
|
||||||
this.mqttClimate.currentTemperature = state.CorrectedTemp.v;
|
this.mqttClimate.currentTemperature = state.CorrectedTemp?.v;
|
||||||
this.mqttClimate.currentHumidity = state.Humidity.v;
|
this.mqttClimate.currentHumidity = state.Humidity?.v;
|
||||||
this.mqttClimate.currentMode = state.TstatMode.v === 1 ? 'off' : state.TstatMode.v === 3 ? 'heat' : undefined;
|
this.mqttClimate.currentMode =
|
||||||
this.mqttClimate.currentAction = this.computeCurrentAction(undefined, state.Duty.v);
|
MYSA_RAW_MODE_TO_DEVICE_MODE[state.TstatMode?.v as number] ?? this.mqttClimate.currentMode;
|
||||||
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? state.SetPoint.v : undefined;
|
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.mqttClimate.writeConfig();
|
||||||
|
|
||||||
await this.mqttTemperature.setState('state_topic', state.CorrectedTemp.v.toFixed(2));
|
await this.mqttTemperature.setState(
|
||||||
|
'state_topic',
|
||||||
|
state.CorrectedTemp != null ? state.CorrectedTemp.v.toFixed(2) : 'None'
|
||||||
|
);
|
||||||
await this.mqttTemperature.writeConfig();
|
await this.mqttTemperature.writeConfig();
|
||||||
|
|
||||||
await this.mqttHumidity.setState('state_topic', state.Humidity.v.toFixed(2));
|
await this.mqttHumidity.setState('state_topic', state.Humidity != null ? state.Humidity.v.toFixed(2) : 'None');
|
||||||
await this.mqttHumidity.writeConfig();
|
await this.mqttHumidity.writeConfig();
|
||||||
|
|
||||||
// `state.Current.v` always has a non-zero value, even for thermostats that are off, so we can't use it to determine initial power state.
|
// `state.Current.v` always has a non-zero value, even for thermostats that are off, so we can't use it to determine initial power state.
|
||||||
@@ -242,7 +321,7 @@ export class Thermostat {
|
|||||||
this.mqttClimate.currentHumidity = status.humidity;
|
this.mqttClimate.currentHumidity = status.humidity;
|
||||||
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? status.setPoint : undefined;
|
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? status.setPoint : undefined;
|
||||||
|
|
||||||
if (status.current != null) {
|
if (this.mysaDevice.Voltage != null && status.current != null) {
|
||||||
const watts = this.mysaDevice.Voltage * status.current;
|
const watts = this.mysaDevice.Voltage * status.current;
|
||||||
await this.mqttPower.setState('state_topic', watts.toFixed(2));
|
await this.mqttPower.setState('state_topic', watts.toFixed(2));
|
||||||
} else {
|
} else {
|
||||||
@@ -263,29 +342,52 @@ export class Thermostat {
|
|||||||
this.mqttClimate.currentMode = 'off';
|
this.mqttClimate.currentMode = 'off';
|
||||||
this.mqttClimate.currentAction = 'off';
|
this.mqttClimate.currentAction = 'off';
|
||||||
this.mqttClimate.targetTemperature = undefined;
|
this.mqttClimate.targetTemperature = undefined;
|
||||||
|
this.mqttClimate.currentFanMode = undefined;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'heat':
|
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.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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private computeCurrentAction(current?: number, dutyCycle?: number): ClimateAction {
|
private computeCurrentAction(current?: number, dutyCycle?: number): ClimateAction {
|
||||||
const mode: MysaDeviceMode | undefined =
|
const currentModeAsMode = this.mqttClimate.currentMode as MysaDeviceMode;
|
||||||
this.mqttClimate.currentMode === 'heat' ? 'heat' : this.mqttClimate.currentMode === 'off' ? 'off' : undefined;
|
const mode = HA_AC_MODES.includes(currentModeAsMode) ? currentModeAsMode : undefined;
|
||||||
|
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'off':
|
case 'off':
|
||||||
return 'off';
|
return 'off';
|
||||||
|
|
||||||
case 'heat':
|
case 'heat':
|
||||||
if (current != null) {
|
switch (this.deviceType) {
|
||||||
return current > 0 ? 'heating' : 'idle';
|
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:
|
default:
|
||||||
return 'idle';
|
return 'idle';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user