mirror of
https://github.com/bourquep/mysa2mqtt.git
synced 2025-11-03 05:10:28 +00:00
Compare commits
12 Commits
v1.1.2
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34f9b574a3 | ||
|
|
2ff6d00999 | ||
|
|
da90344ffc | ||
|
|
4069f24880 | ||
|
|
a47cdbb45e | ||
|
|
2e075cd40d | ||
|
|
f2d35a1ca5 | ||
|
|
122ffde2f1 | ||
|
|
c259557da0 | ||
|
|
57bd430c98 | ||
|
|
8ca80acb49 | ||
|
|
3b5dafeda9 |
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
|
||||
directory: '/' # Location of package manifests
|
||||
schedule:
|
||||
interval: 'daily'
|
||||
interval: 'weekly'
|
||||
labels:
|
||||
- 'dependencies'
|
||||
commit-message:
|
||||
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
README.md
|
||||
35
README.md
35
README.md
@@ -1,5 +1,11 @@
|
||||
# mysa2mqtt
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
|
||||
[](#contributors-)
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://www.npmjs.com/package/mysa2mqtt)
|
||||
[](https://hub.docker.com/r/bourquep/mysa2mqtt)
|
||||
[](https://github.com/bourquep/mysa2mqtt/actions/workflows/github-code-scanning/codeql)
|
||||
@@ -118,6 +124,11 @@ For development or custom modifications:
|
||||
The application can be configured using either command-line arguments or environment variables. Environment variables
|
||||
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
|
||||
|
||||
| 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` |
|
||||
| `-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 |
|
||||
| `-t, --temperature-unit` | `M2M_TEMPERATURE_UNIT` | `C` | Temperature unit (`C` = Celsius, `F` = Fahrenheit) |
|
||||
|
||||
## 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
|
||||
- [Commander.js](https://github.com/tj/commander.js) - Command-line argument parsing
|
||||
- [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!
|
||||
|
||||
2881
package-lock.json
generated
2881
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -49,26 +49,26 @@
|
||||
"commander": "14.0.1",
|
||||
"dotenv": "17.2.3",
|
||||
"mqtt2ha": "4.1.2",
|
||||
"mysa-js-sdk": "1.3.2",
|
||||
"pino": "10.0.0",
|
||||
"pino-pretty": "13.1.1"
|
||||
"mysa-js-sdk": "2.0.0",
|
||||
"pino": "10.1.0",
|
||||
"pino-pretty": "13.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commander-js/extra-typings": "14.0.0",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@semantic-release/npm": "12.0.2",
|
||||
"@types/node": "24.6.2",
|
||||
"@eslint/js": "9.39.0",
|
||||
"@semantic-release/npm": "13.1.1",
|
||||
"@types/node": "24.9.2",
|
||||
"conventional-changelog-conventionalcommits": "9.1.0",
|
||||
"eslint": "9.37.0",
|
||||
"eslint-plugin-jsdoc": "60.8.1",
|
||||
"eslint": "9.39.0",
|
||||
"eslint-plugin-jsdoc": "61.1.11",
|
||||
"eslint-plugin-tsdoc": "0.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"prettier-plugin-jsdoc": "1.3.3",
|
||||
"prettier-plugin-jsdoc": "1.5.0",
|
||||
"prettier-plugin-organize-imports": "4.3.0",
|
||||
"semantic-release": "24.2.9",
|
||||
"semantic-release": "25.0.1",
|
||||
"tsup": "8.5.0",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.45.0"
|
||||
"typescript-eslint": "8.46.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +99,8 @@ async function main() {
|
||||
mqttSettings,
|
||||
new PinoLogger(rootLogger.child({ module: 'thermostat', deviceId: 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')
|
||||
.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()
|
||||
.opts();
|
||||
|
||||
@@ -51,8 +51,11 @@ export class Thermostat {
|
||||
private readonly mqttSettings: MqttSettings,
|
||||
private readonly logger: Logger,
|
||||
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 = {
|
||||
identifiers: mysaDevice.Id,
|
||||
name: mysaDevice.Name,
|
||||
@@ -81,9 +84,9 @@ export class Thermostat {
|
||||
min_temp: mysaDevice.MinSetpoint,
|
||||
max_temp: mysaDevice.MaxSetpoint,
|
||||
modes: ['off', 'heat'], // TODO: AC
|
||||
precision: 0.1,
|
||||
temp_step: 0.5,
|
||||
temperature_unit: 'C', // TODO: Confirm that Mysa always works in C
|
||||
precision: is_celsius ? 0.1 : 1.0,
|
||||
temp_step: is_celsius ? 0.5 : 1.0,
|
||||
temperature_unit: 'C',
|
||||
optimistic: true
|
||||
}
|
||||
},
|
||||
@@ -118,7 +121,17 @@ export class Thermostat {
|
||||
if (message === '') {
|
||||
this.mysaApiClient.setDeviceState(this.mysaDevice.Id, undefined, undefined);
|
||||
} 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;
|
||||
}
|
||||
@@ -137,7 +150,7 @@ export class Thermostat {
|
||||
device_class: 'temperature',
|
||||
state_class: 'measurement',
|
||||
unit_of_measurement: '°C',
|
||||
suggested_display_precision: 1,
|
||||
suggested_display_precision: is_celsius ? 0.1 : 0.0,
|
||||
force_update: true
|
||||
}
|
||||
});
|
||||
@@ -187,18 +200,22 @@ 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 = state.TstatMode.v === 1 ? 'off' : state.TstatMode.v === 3 ? 'heat' : undefined;
|
||||
this.mqttClimate.currentAction = this.computeCurrentAction(undefined, state.Duty.v);
|
||||
this.mqttClimate.targetTemperature = this.mqttClimate.currentMode !== 'off' ? state.SetPoint.v : undefined;
|
||||
this.mqttClimate.currentTemperature = state.CorrectedTemp?.v;
|
||||
this.mqttClimate.currentHumidity = state.Humidity?.v;
|
||||
this.mqttClimate.currentMode = tstatMode === 1 ? 'off' : tstatMode === 3 ? 'heat' : undefined;
|
||||
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('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.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();
|
||||
|
||||
// `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 +259,7 @@ export class Thermostat {
|
||||
this.mqttClimate.currentHumidity = status.humidity;
|
||||
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;
|
||||
await this.mqttPower.setState('state_topic', watts.toFixed(2));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user