mirror of
				https://github.com/bourquep/mysa2mqtt.git
				synced 2025-11-03 21:29:41 +00:00 
			
		
		
		
	fix: Unable to change the set point when Home Assistant is configured with Fahrenheit temperature unit (#73)
# Fix Temperature Handling in Fahrenheit Mode
## Problem
When operating in **Fahrenheit mode**, Mysa still expects temperature
values to be provided in **Celsius**.
However, Home Assistant sends integer Fahrenheit values (e.g.,
`72.02°F`), which convert to **non-aligned Celsius values** like
`22.22°C`.
Mysa’s API only accepts temperature values that are either **whole
numbers** or **increments of 0.5°C** (for example: `21.0`, `21.5`,
`22.0`).
As a result, values such as `22.22°C` or `21.72°C` are considered
invalid and are **rejected** by Mysa’s API.
---
## Root Cause
- The original code accepted **0.1°C precision** and **0.5°C step
size**.
- When Home Assistant runs in Fahrenheit, the conversion from °F to °C
produces fractional values that are not valid (e.g., 72°F → 22.22°C).
- Because Mysa enforces strict 0.5°C increments, these fractional
setpoints caused failed updates.
---
## Solution
This update ensures valid behavior when using Fahrenheit mode **while
keeping the current behavior for Celsius**:
- Adds a new environment variable:  
  **`M2M_TEMP_UNIT`** — accepts either:
  - `C` *(default)*
  - `F` *(for Fahrenheit operation)*
- When running in Fahrenheit mode (`M2M_TEMP_UNIT=F`):
  - Celsius values are **rounded and clamped to the nearest 0.5°C**.  
  - Temperature step size and precision are adjusted:
    - Precision → `1°F`
    - Step size → `1°F`
- When running in Celsius mode, existing logic remains unchanged (0.1
precision, 0.5 step).
---
## Technical Summary
| Mode | Env Variable | Precision | Step | Conversion Behavior |
|------|---------------|------------|------|----------------------|
| Celsius | `M2M_TEMP_UNIT=C` (default) | 0.1°C | 0.5°C | Direct
pass-through |
| Fahrenheit | `M2M_TEMP_UNIT=F` | 1°F | 1°F | Convert °F → °C, snap to
0.5°C |
The rounding logic ensures that when a Fahrenheit value (e.g., `72°F`)
is converted to Celsius (`21.72°C`), it is adjusted to the nearest valid
half-degree (`21.5°C` or `22.0°C`).
# Demo
https://github.com/user-attachments/assets/bbffe5fe-a3be-43cb-aed0-f63bdfacb1d4
---------
Co-authored-by: Pascal Bourque <pascal@cosmos.moi>
			
			
This commit is contained in:
		
							
								
								
									
										1
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.prettierignore
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					README.md
 | 
				
			||||||
@@ -124,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                      |
 | 
				
			||||||
@@ -151,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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -51,8 +51,11 @@ export class Thermostat {
 | 
				
			|||||||
    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,
 | 
				
			||||||
@@ -81,9 +84,9 @@ export class Thermostat {
 | 
				
			|||||||
          min_temp: mysaDevice.MinSetpoint,
 | 
					          min_temp: mysaDevice.MinSetpoint,
 | 
				
			||||||
          max_temp: mysaDevice.MaxSetpoint,
 | 
					          max_temp: mysaDevice.MaxSetpoint,
 | 
				
			||||||
          modes: ['off', 'heat'], // TODO: AC
 | 
					          modes: ['off', 'heat'], // TODO: AC
 | 
				
			||||||
          precision: 0.1,
 | 
					          precision: is_celsius ? 0.1 : 1.0,
 | 
				
			||||||
          temp_step: 0.5,
 | 
					          temp_step: is_celsius ? 0.5 : 1.0,
 | 
				
			||||||
          temperature_unit: 'C', // TODO: Confirm that Mysa always works in C
 | 
					          temperature_unit: 'C',
 | 
				
			||||||
          optimistic: true
 | 
					          optimistic: true
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@@ -118,7 +121,17 @@ 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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -137,7 +150,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
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user