From 8d534162fe31fb59f34266332811a55545b2b5c3 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Tue, 27 Jan 2026 15:12:42 -0800 Subject: [PATCH 1/2] Update changelog for version 7.3.4 --- CHANGELOG.rst | 19 +++++ docs/api/nwp500.rst | 8 ++ docs/guides/reservations.rst | 63 ++++++++-------- docs/protocol/data_conversions.rst | 42 ++++++----- docs/python_api/device_control.rst | 15 ++-- docs/python_api/mqtt_client.rst | 22 +++--- examples/advanced/combined_callbacks.py | 7 +- examples/advanced/demand_response.py | 3 +- examples/advanced/device_capabilities.py | 4 +- examples/advanced/device_status_debug.py | 3 +- examples/advanced/power_control.py | 3 +- examples/advanced/reconnection_demo.py | 3 +- examples/advanced/reservation_schedule.py | 2 +- examples/advanced/simple_auto_recovery.py | 3 +- examples/advanced/water_reservation.py | 3 +- examples/beginner/03_get_status.py | 3 +- examples/beginner/04_set_temperature.py | 23 +++--- .../intermediate/advanced_auth_patterns.py | 6 +- .../intermediate/device_status_callback.py | 22 ++++-- examples/intermediate/event_driven_control.py | 13 ++-- examples/intermediate/improved_auth.py | 5 +- .../intermediate/mqtt_realtime_monitoring.py | 7 +- examples/intermediate/periodic_requests.py | 3 +- examples/intermediate/set_mode.py | 3 +- examples/intermediate/vacation_mode.py | 3 +- examples/testing/periodic_device_info.py | 3 +- examples/testing/test_periodic_minimal.py | 3 +- src/nwp500/cli/output_formatters.py | 56 ++++++++++++++ src/nwp500/converters.py | 40 +++++++++- src/nwp500/models.py | 20 +++-- src/nwp500/temperature.py | 74 +++++++++++++++++++ 31 files changed, 361 insertions(+), 123 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e1fb06e..b974c27 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,25 @@ Changelog ========= +Version 7.3.4 (2026-01-27) +========================== + +Fixed +----- +- **Temperature Delta Conversions**: Fixed incorrect Fahrenheit conversion for differential temperature settings (heat pump and heater element on/off thresholds) + + - Created new ``DeciCelsiusDelta`` class for temperature deltas that apply scale factor (9/5) but NOT the +32 offset + - Heat pump and heater element differential settings now use ``DeciCelsiusDelta`` instead of ``DeciCelsius`` + - ``hp_upper_on_diff_temp_setting``, ``hp_lower_on_diff_temp_setting``, ``he_upper_on_diff_temp_setting``, ``he_lower_on_diff_temp_setting``, and related off settings now convert correctly to Fahrenheit + - Example: A device value of 5 (representing 0.5°C delta) now correctly converts to 0.9°F delta instead of 32.9°F + +- **CLI Output**: Added display of heat pump and heater element differential temperature settings in device status output + +Changed +------- +- **Internal API**: Added ``div_10_celsius_delta_to_preferred`` converter for temperature delta values in device models + + Version 7.3.3 (2026-01-27) ========================== diff --git a/docs/api/nwp500.rst b/docs/api/nwp500.rst index 46dc281..358f7cf 100644 --- a/docs/api/nwp500.rst +++ b/docs/api/nwp500.rst @@ -149,6 +149,14 @@ nwp500.topic\_builder module :show-inheritance: :undoc-members: +nwp500.unit\_system module +-------------------------- + +.. automodule:: nwp500.unit_system + :members: + :show-inheritance: + :undoc-members: + nwp500.utils module ------------------- diff --git a/docs/guides/reservations.rst b/docs/guides/reservations.rst index 404b93d..328b10a 100644 --- a/docs/guides/reservations.rst +++ b/docs/guides/reservations.rst @@ -44,14 +44,14 @@ Here's a simple example that sets up a weekday morning reservation: device = await api.get_first_device() # Build reservation entry - weekday_morning = NavienAPIClient.build_reservation_entry( + weekday_morning = build_reservation_entry( enabled=True, days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], hour=6, minute=30, mode_id=4, # High Demand - temperature_f=140.0 # Temperature in Fahrenheit + temperature=140.0 # Temperature in user's preferred unit ) # Send to device @@ -149,8 +149,8 @@ Field Descriptions (e.g., ``98`` for 120°F) for consistency. **Note:** When using ``build_reservation_entry()``, you don't need to - calculate the param value manually - just pass ``temperature_f`` in - Fahrenheit and the conversion is handled automatically. + calculate the param value manually - just pass ``temperature`` in + your user's preferred unit and the conversion is handled automatically. Helper Functions ================ @@ -161,33 +161,33 @@ Building Reservation Entries ----------------------------- Use ``build_reservation_entry()`` to create properly formatted entries. -The function accepts temperature in Fahrenheit and handles the conversion +The function accepts temperature in your user's preferred unit and handles the conversion to the device's internal format automatically: .. code-block:: python from nwp500 import build_reservation_entry - # Weekday morning - High Demand mode at 140°F + # Weekday morning - High Demand mode at 140 (°F or °C based on unit preference) entry = build_reservation_entry( enabled=True, days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], hour=6, minute=30, mode_id=4, # High Demand - temperature_f=140.0 # Temperature in Fahrenheit + temperature=140.0 # Temperature in user's preferred unit ) # Returns: {'enable': 1, 'week': 62, 'hour': 6, 'min': 30, # 'mode': 4, 'param': 120} - # Weekend - Energy Saver mode at 120°F + # Weekend - Energy Saver mode at 120 (°F or °C) entry2 = build_reservation_entry( enabled=True, days=["Saturday", "Sunday"], hour=8, minute=0, mode_id=3, # Energy Saver - temperature_f=120.0 + temperature=120.0 ) # You can also use day indices (0=Sunday, 6=Saturday) @@ -197,7 +197,7 @@ to the device's internal format automatically: hour=18, minute=0, mode_id=1, # Heat Pump Only - temperature_f=130.0 + temperature=130.0 ) Temperature Conversion Utility @@ -269,7 +269,7 @@ Send a new reservation schedule to the device: # Build multiple reservation entries reservations = [ - # Weekday morning: High Demand at 140°F + # Weekday morning: High Demand at 140 (user's preferred unit) build_reservation_entry( enabled=True, days=["Monday", "Tuesday", "Wednesday", "Thursday", @@ -277,9 +277,9 @@ Send a new reservation schedule to the device: hour=6, minute=30, mode_id=4, - temperature_f=140.0 + temperature=140.0 ), - # Weekday evening: Energy Saver at 130°F + # Weekday evening: Energy Saver at 130 (user's preferred unit) build_reservation_entry( enabled=True, days=["Monday", "Tuesday", "Wednesday", "Thursday", @@ -287,16 +287,16 @@ Send a new reservation schedule to the device: hour=18, minute=0, mode_id=3, - temperature_f=130.0 + temperature=130.0 ), - # Weekend: Heat Pump Only at 120°F + # Weekend: Heat Pump Only at 120 (user's preferred unit) build_reservation_entry( enabled=True, days=["Saturday", "Sunday"], hour=8, minute=0, mode_id=1, - temperature_f=120.0 + temperature=120.0 ), ] @@ -439,7 +439,7 @@ Different settings for work days and weekends: hour=5, minute=30, mode_id=4, # High Demand - temperature_f=140.0 + temperature=140.0 ), # Weekend morning: later start, energy saver build_reservation_entry( @@ -448,7 +448,7 @@ Different settings for work days and weekends: hour=8, minute=0, mode_id=3, # Energy Saver - temperature_f=130.0 + temperature=130.0 ), ] @@ -467,7 +467,7 @@ Minimize energy use during peak hours: hour=6, minute=0, mode_id=4, - temperature_f=140.0 + temperature=140.0 ), # Day: 9:00 AM - Switch to Energy Saver build_reservation_entry( @@ -476,7 +476,7 @@ Minimize energy use during peak hours: hour=9, minute=0, mode_id=3, - temperature_f=120.0 + temperature=120.0 ), # Evening: 5:00 PM - Heat Pump Only (before peak pricing) build_reservation_entry( @@ -485,7 +485,7 @@ Minimize energy use during peak hours: hour=17, minute=0, mode_id=1, - temperature_f=130.0 + temperature=130.0 ), # Night: 10:00 PM - Back to Energy Saver build_reservation_entry( @@ -494,7 +494,7 @@ Minimize energy use during peak hours: hour=22, minute=0, mode_id=3, - temperature_f=120.0 + temperature=120.0 ), ] @@ -512,7 +512,7 @@ Automatically enable vacation mode during a trip: hour=20, minute=0, mode_id=5, # Vacation Mode - temperature_f=120.0 # Temperature doesn't matter for vacation mode + temperature=120.0 # Temperature doesn't matter for vacation mode ) # Return to normal operation when you get back @@ -522,7 +522,7 @@ Automatically enable vacation mode during a trip: hour=14, minute=0, mode_id=3, # Energy Saver - temperature_f=130.0 + temperature=130.0 ) reservations = [start_vacation, end_vacation] @@ -533,11 +533,12 @@ Important Notes Temperature Conversion ----------------------- -When using ``build_reservation_entry()``, pass temperatures in Fahrenheit -using the ``temperature_f`` parameter. The function automatically converts -to the device's internal format (half-degrees Celsius). +When using ``build_reservation_entry()``, pass temperatures in your user's +preferred unit (Celsius or Fahrenheit) using the ``temperature`` parameter. +The function automatically converts to the device's internal format +(half-degrees Celsius). -The valid temperature range is 95°F to 150°F. +The valid temperature range is 35°C to 65.5°C (95°F to 150°F). For reading reservation responses from the device, the ``param`` field contains the raw half-degrees Celsius value. Convert to Fahrenheit with: @@ -619,7 +620,7 @@ Full working example with error handling and response monitoring: hour=6, minute=30, mode_id=4, # High Demand - temperature_f=140.0 + temperature=140.0 ), # Weekday day build_reservation_entry( @@ -629,7 +630,7 @@ Full working example with error handling and response monitoring: hour=9, minute=0, mode_id=3, # Energy Saver - temperature_f=120.0 + temperature=120.0 ), # Weekend morning build_reservation_entry( @@ -638,7 +639,7 @@ Full working example with error handling and response monitoring: hour=8, minute=0, mode_id=3, # Energy Saver - temperature_f=130.0 + temperature=130.0 ), ] diff --git a/docs/protocol/data_conversions.rst b/docs/protocol/data_conversions.rst index cd73564..baf0017 100644 --- a/docs/protocol/data_conversions.rst +++ b/docs/protocol/data_conversions.rst @@ -34,9 +34,17 @@ The device uses several encoding schemes to minimize transmission overhead: - Applied to decimal precision values - Formula: ``displayed_value = raw_value / 10.0`` - Purpose: Preserve decimal precision in integer storage - - Common for flow rates and differential temperatures + - Common for flow rates - Example: Raw 125 → 12.5 GPM +3.5. **Temperature Delta (Decicelsius)** (div_10_celsius_delta) + - Applied to differential temperature settings (heat pump and element hysteresis) + - Formula: ``displayed_value_celsius = raw_value / 10.0``, ``displayed_value_fahrenheit = (raw_value / 10.0) * 9/5`` (NO +32 offset) + - Purpose: Represents temperature DIFFERENCES/DELTAS, not absolute temperatures + - Key difference from absolute temperature conversion: No +32 offset applied when converting to Fahrenheit + - Example: Raw 5 → 0.5°C delta → 0.9°F delta (NOT 32.9°F) + - Used for: Heat pump on/off differential, heating element on/off differential + 4. **Boolean Encoding** (device_bool) - Applied to all status flags - Formula: ``displayed_value = (raw_value == 2)`` @@ -181,21 +189,21 @@ Electric heating elements are controlled via thermostat ranges. Two sensors (upp - °F - **Lower element OFF threshold**. Lower tank temp rises above this to deactivate lower element. * - ``heUpperOnDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Upper element differential** (ON-OFF difference). Hysteresis width to prevent rapid cycling. Typically 2-5°F. + - **Upper element differential** (ON-OFF hysteresis width). Temperature delta to prevent rapid cycling. Typically 2-5°F. This is a DELTA value, not an absolute temperature (0 delta = 0°F difference). * - ``heUpperOffDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Upper element differential** variation (advanced tuning). May vary based on mode. + - **Upper element OFF differential** (advanced tuning). May vary based on mode. DELTA value. * - ``heLowerOnDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Lower element differential** (ON-OFF difference). + - **Lower element differential** (ON-OFF hysteresis width). DELTA value. * - ``heLowerOffDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Lower element differential** variation. + - **Lower element OFF differential** variation. DELTA value. * - ``heatMinOpTemperature`` - HalfCelsiusToF - °F @@ -231,21 +239,21 @@ Heat pump stages are similarly controlled via thermostat ranges: - °F - **Lower heat pump OFF**. Lower tank rises above this to stop lower tank heat pump operation. * - ``hpUpperOnDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Heat pump upper differential** (ON-OFF hysteresis). Prevents rapid cycling. + - **Heat pump upper differential** (ON-OFF hysteresis). Temperature delta to prevent rapid cycling. DELTA value. * - ``hpUpperOffDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Heat pump upper differential** variation. + - **Heat pump upper OFF differential** variation. DELTA value. * - ``hpLowerOnDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Heat pump lower differential**. + - **Heat pump lower ON differential**. DELTA value. * - ``hpLowerOffDiffTempSetting`` - - div_10 + - div_10_celsius_delta - °F - - **Heat pump lower differential** variation. + - **Heat pump lower OFF differential** variation. DELTA value. Freeze Protection Temperatures ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/python_api/device_control.rst b/docs/python_api/device_control.rst index 71354e6..ab3ae16 100644 --- a/docs/python_api/device_control.rst +++ b/docs/python_api/device_control.rst @@ -220,7 +220,7 @@ Temperature Control set_dhw_temperature() ^^^^^^^^^^^^^^^^^^^^^ -.. py:method:: set_dhw_temperature(device, temperature_f) +.. py:method:: set_dhw_temperature(device, temperature) Set DHW target temperature. @@ -228,24 +228,25 @@ set_dhw_temperature() :param device: Device object :type device: Device - :param temperature_f: Target temperature in Fahrenheit (95-150°F) - :type temperature_f: float + :param temperature: Target temperature in user's preferred unit (Celsius or Fahrenheit) + :type temperature: float :return: Publish packet ID :rtype: int - :raises RangeValidationError: If temperature is outside 95-150°F range + :raises RangeValidationError: If temperature is outside valid range :raises DeviceCapabilityError: If device doesn't support temperature control The temperature is automatically converted to the device's internal format - (half-degrees Celsius). + (half-degrees Celsius). The valid range depends on the device's + temperature preference and configuration. **Example:** .. code-block:: python - # Set temperature to 140°F + # Set temperature (interpreted in device's preferred unit) await mqtt.control.set_dhw_temperature(device, 140.0) - # Common temperatures + # Common temperatures (device-dependent units) await mqtt.control.set_dhw_temperature(device, 120.0) # Standard await mqtt.control.set_dhw_temperature(device, 130.0) # Medium await mqtt.control.set_dhw_temperature(device, 140.0) # Hot diff --git a/docs/python_api/mqtt_client.rst b/docs/python_api/mqtt_client.rst index 4e9474d..b1069ec 100644 --- a/docs/python_api/mqtt_client.rst +++ b/docs/python_api/mqtt_client.rst @@ -49,8 +49,9 @@ Basic Monitoring # Subscribe to status updates def on_status(status): - print(f"Water Temp: {status.dhw_temperature}°F") - print(f"Target: {status.dhw_temperature_setting}°F") + unit = status.get_field_unit('dhw_temperature') + print(f"Water Temp: {status.dhw_temperature}{unit}") + print(f"Target: {status.dhw_temperature_setting}{unit}") print(f"Power: {status.current_inst_power}W") print(f"Mode: {status.dhw_operation_setting.name}") @@ -87,7 +88,7 @@ control method reference, capability checking, and advanced features. # Control operations (with automatic capability checking) await mqtt.control.set_power(device, power_on=True) await mqtt.control.set_dhw_mode(device, mode_id=3) # Energy Saver - await mqtt.control.set_dhw_temperature(device, 140.0) + await mqtt.control.set_dhw_temperature(device, 140.0) # Temperature in user's preferred unit await mqtt.disconnect() @@ -442,29 +443,30 @@ set_dhw_mode() set_dhw_temperature() ^^^^^^^^^^^^^^^^^^^^^ -.. py:method:: set_dhw_temperature(device, temperature_f) +.. py:method:: set_dhw_temperature(device, temperature) Set target DHW temperature. :param device: Device object :type device: Device - :param temperature_f: Temperature in Fahrenheit (95-150°F) - :type temperature_f: float + :param temperature: Temperature in user's preferred unit (Celsius or Fahrenheit) + :type temperature: float :return: Publish packet ID :rtype: int - :raises RangeValidationError: If temperature is outside 95-150°F range + :raises RangeValidationError: If temperature is outside valid range The temperature is automatically converted to the device's internal - format (half-degrees Celsius). + format (half-degrees Celsius). The actual valid range depends on the + device's temperature preference and configuration. **Example:** .. code-block:: python - # Set temperature to 140°F + # Set temperature (value interpreted in device's preferred unit) await mqtt.control.set_dhw_temperature(device, 140.0) - # Common temperatures + # Common temperatures (device-dependent units) await mqtt.control.set_dhw_temperature(device, 120.0) # Standard await mqtt.control.set_dhw_temperature(device, 130.0) # Medium await mqtt.control.set_dhw_temperature(device, 140.0) # Hot diff --git a/examples/advanced/combined_callbacks.py b/examples/advanced/combined_callbacks.py index bd6fd4f..b4e8c9d 100644 --- a/examples/advanced/combined_callbacks.py +++ b/examples/advanced/combined_callbacks.py @@ -86,9 +86,10 @@ async def main(): # Callback for status updates def on_status(status: DeviceStatus): counts["status"] += 1 + unit = status.get_field_unit("dhw_temperature") print(f"\n📊 Status Update #{counts['status']}") print(f" Mode: {status.operation_mode.name}") - print(f" DHW Temp: {status.dhw_temperature:.1f}°F") + print(f" DHW Temp: {status.dhw_temperature:.1f}{unit}") print(f" DHW Charge: {status.dhw_charge_per:.1f}%") print(f" Compressor: {'On' if status.comp_use else 'Off'}") @@ -98,9 +99,7 @@ def on_feature(feature: DeviceFeature): print(f"\n📋 Feature Info #{counts['feature']}") print(f" Serial: {feature.controller_serial_number}") print(f" FW Version: {feature.controller_sw_version}") - unit_suffix = ( - "°C" if feature.temperature_type.name == "CELSIUS" else "°F" - ) + unit_suffix = feature.get_field_unit("dhw_temperature_min") print( f" Temp Range: {feature.dhw_temperature_min}-{feature.dhw_temperature_max}{unit_suffix}" ) diff --git a/examples/advanced/demand_response.py b/examples/advanced/demand_response.py index ec2264a..d2153d0 100644 --- a/examples/advanced/demand_response.py +++ b/examples/advanced/demand_response.py @@ -48,8 +48,9 @@ def on_current_status(status): nonlocal current_status current_status = status logger.info(f"Current operation mode: {status.operation_mode.name}") + unit = status.get_field_unit("dhw_target_temperature_setting") logger.info( - f"Current DHW temperature setting: {status.dhw_target_temperature_setting}°F" + f"Current DHW temperature setting: {status.dhw_target_temperature_setting}{unit}" ) await mqtt_client.subscribe_device_status(device, on_current_status) diff --git a/examples/advanced/device_capabilities.py b/examples/advanced/device_capabilities.py index e5f1e18..1cd9a75 100644 --- a/examples/advanced/device_capabilities.py +++ b/examples/advanced/device_capabilities.py @@ -138,9 +138,7 @@ def on_device_feature(feature: DeviceFeature): ) print("\nConfiguration:") - unit_suffix = ( - "°C" if feature.temperature_type.name == "CELSIUS" else "°F" - ) + unit_suffix = feature.get_field_unit("dhw_temperature_min") print(f" Temperature Unit: {feature.temperature_type.name}") print(f" Temp Formula Type: {feature.temp_formula_type}") print( diff --git a/examples/advanced/device_status_debug.py b/examples/advanced/device_status_debug.py index 6ff6012..eb59f45 100644 --- a/examples/advanced/device_status_debug.py +++ b/examples/advanced/device_status_debug.py @@ -129,10 +129,11 @@ def raw_message_handler(topic: str, message: dict): def on_device_status(status: DeviceStatus): """Parsed status callback.""" message_count["status"] += 1 + unit = status.get_field_unit("dhw_temperature") print( f"\n[SUCCESS] PARSED Status Update #{message_count['status']}" ) - print(f" DHW Temperature: {status.dhw_temperature:.1f}°F") + print(f" DHW Temperature: {status.dhw_temperature:.1f}{unit}") print(f" Operation Mode: {status.operation_mode.name}") print(f" Compressor: {status.comp_use}") diff --git a/examples/advanced/power_control.py b/examples/advanced/power_control.py index a425ed3..e041406 100644 --- a/examples/advanced/power_control.py +++ b/examples/advanced/power_control.py @@ -48,7 +48,8 @@ def on_current_status(status): nonlocal current_status current_status = status logger.info(f"Current operation mode: {status.operation_mode.name}") - logger.info(f"Current DHW temperature: {status.dhw_temperature}°F") + unit = status.get_field_unit("dhw_temperature") + logger.info(f"Current DHW temperature: {status.dhw_temperature}{unit}") await mqtt_client.subscribe_device_status(device, on_current_status) await mqtt_client.control.request_device_status(device) diff --git a/examples/advanced/reconnection_demo.py b/examples/advanced/reconnection_demo.py index 7eecdfe..0393317 100644 --- a/examples/advanced/reconnection_demo.py +++ b/examples/advanced/reconnection_demo.py @@ -89,8 +89,9 @@ def on_resumed(return_code, session_present): def on_status(status): nonlocal status_count status_count += 1 + unit = status.get_field_unit("dhw_temperature") print(f"\n📊 Status update #{status_count}:") - print(f" Temperature: {status.dhw_temperature}°F") + print(f" Temperature: {status.dhw_temperature}{unit}") print(f" Connected: {mqtt_client.is_connected}") if mqtt_client.is_reconnecting: print(f" Reconnecting: attempt {mqtt_client.reconnect_attempts}...") diff --git a/examples/advanced/reservation_schedule.py b/examples/advanced/reservation_schedule.py index fcc67ec..f522d5e 100644 --- a/examples/advanced/reservation_schedule.py +++ b/examples/advanced/reservation_schedule.py @@ -25,7 +25,7 @@ async def main() -> None: print("No devices found for this account") return - # Build a weekday morning reservation for High Demand mode at 140°F + # Build a weekday morning reservation for High Demand mode at 140 (user's preferred unit) weekday_reservation = build_reservation_entry( enabled=True, days=["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], diff --git a/examples/advanced/simple_auto_recovery.py b/examples/advanced/simple_auto_recovery.py index 86ad0cc..2fff604 100644 --- a/examples/advanced/simple_auto_recovery.py +++ b/examples/advanced/simple_auto_recovery.py @@ -200,8 +200,9 @@ async def main(): def on_status(status): nonlocal status_count status_count += 1 + unit = status.get_field_unit("dhw_temperature") logger.info( - f"Status #{status_count}: Temp={status.dhw_temperature}°F, " + f"Status #{status_count}: Temp={status.dhw_temperature}{unit}, " f"Mode={status.operation_mode}" ) diff --git a/examples/advanced/water_reservation.py b/examples/advanced/water_reservation.py index 703efd1..ff03b26 100644 --- a/examples/advanced/water_reservation.py +++ b/examples/advanced/water_reservation.py @@ -48,8 +48,9 @@ def on_current_status(status): nonlocal current_status current_status = status logger.info(f"Current operation mode: {status.operation_mode.name}") + unit = status.get_field_unit("dhw_target_temperature_setting") logger.info( - f"Current DHW temperature setting: {status.dhw_target_temperature_setting}°F" + f"Current DHW temperature setting: {status.dhw_target_temperature_setting}{unit}" ) await mqtt_client.subscribe_device_status(device, on_current_status) diff --git a/examples/beginner/03_get_status.py b/examples/beginner/03_get_status.py index 2b7cb16..ff4380c 100755 --- a/examples/beginner/03_get_status.py +++ b/examples/beginner/03_get_status.py @@ -40,8 +40,9 @@ async def main(): # Typed callback def on_status(status: DeviceStatus): + unit = status.get_field_unit("dhw_temperature") print( - f"Status: {status.dhw_temperature:.1f}°F, {status.current_inst_power:.1f}W" + f"Status: {status.dhw_temperature:.1f}{unit}, {status.current_inst_power:.1f}W" ) # Subscribe with typed parsing diff --git a/examples/beginner/04_set_temperature.py b/examples/beginner/04_set_temperature.py index 4958fa2..993441a 100644 --- a/examples/beginner/04_set_temperature.py +++ b/examples/beginner/04_set_temperature.py @@ -47,18 +47,22 @@ async def set_dhw_temperature_example(): def on_current_status(status): nonlocal current_status current_status = status + unit = status.get_field_unit("dhw_target_temperature_setting") logger.info( - f"Current DHW target temperature: {status.dhw_target_temperature_setting}°F" + f"Current DHW target temperature: {status.dhw_target_temperature_setting}{unit}" ) - logger.info(f"Current DHW temperature: {status.dhw_temperature}°F") + logger.info(f"Current DHW temperature: {status.dhw_temperature}{unit}") await mqtt_client.subscribe_device_status(device, on_current_status) await mqtt_client.control.request_device_status(device) await asyncio.sleep(3) # Wait for current status - # Set new target temperature to 140°F + # Set new target temperature to 140 (in user's preferred unit) target_temperature = 140 - logger.info(f"Setting DHW target temperature to {target_temperature}°F...") + unit = "°C" if current_status.temperature_type.name == "CELSIUS" else "°F" + logger.info( + f"Setting DHW target temperature to {target_temperature}{unit}..." + ) # Set up callback to capture temperature change response temp_changed = False @@ -66,10 +70,11 @@ def on_current_status(status): def on_temp_change_response(status): nonlocal temp_changed logger.info("Temperature change response received!") + unit = status.get_field_unit("dhw_target_temperature_setting") logger.info( - f"New target temperature: {status.dhw_target_temperature_setting}°F" + f"New target temperature: {status.dhw_target_temperature_setting}{unit}" ) - logger.info(f"Current DHW temperature: {status.dhw_temperature}°F") + logger.info(f"Current DHW temperature: {status.dhw_temperature}{unit}") logger.info(f"Operation mode: {status.operation_mode.name}") logger.info(f"Tank charge: {status.dhw_charge_per}%") temp_changed = True @@ -98,7 +103,7 @@ def on_temp_change_response(status): print("This example demonstrates:") print("1. Connecting to device via MQTT") print("2. Getting current DHW target temperature") - print("3. Setting new DHW target temperature to 140°F") + print("3. Setting new DHW target temperature (in device's preferred unit)") print("4. Receiving and displaying the response") print() @@ -116,5 +121,5 @@ def on_temp_change_response(status): print(" python -m nwp500.cli temp 130") print(" python -m nwp500.cli temp 150") print() - print("Valid temperature range: 115-150°F") - print("Note: The device may cap temperatures at 150°F maximum") + print("Valid temperature range depends on device configuration") + print("Note: Temperature values are interpreted in your device's preferred unit") diff --git a/examples/intermediate/advanced_auth_patterns.py b/examples/intermediate/advanced_auth_patterns.py index f042b51..b55c21d 100644 --- a/examples/intermediate/advanced_auth_patterns.py +++ b/examples/intermediate/advanced_auth_patterns.py @@ -48,7 +48,8 @@ async def example_basic_pattern(): if devices: device = devices[0] print(f" Device: {device.device_info.device_name}") - print(f" Temperature: {device.status.dhw_temperature}°F") + unit = device.status.get_field_unit("dhw_temperature") + print(f" Temperature: {device.status.dhw_temperature}{unit}") print("✓ Context exited, session closed") @@ -90,8 +91,9 @@ async def example_with_mqtt(): # Subscribe to status updates def on_status(status): + unit = status.get_field_unit("dhw_temperature") print( - f" 📊 Status: Temp={status.dhw_temperature}°F, " + f" 📊 Status: Temp={status.dhw_temperature}{unit}, " f"Mode={status.operation_mode}, " f"Power={status.current_inst_power}W" ) diff --git a/examples/intermediate/device_status_callback.py b/examples/intermediate/device_status_callback.py index 289baf0..0c3b86b 100755 --- a/examples/intermediate/device_status_callback.py +++ b/examples/intermediate/device_status_callback.py @@ -137,22 +137,28 @@ def on_device_status(status: DeviceStatus): print("=" * 60) # Access typed status fields directly + unit = status.get_field_unit("dhw_temperature") + flow_unit = status.get_field_unit("current_dhw_flow_rate") + superheat_unit = status.get_field_unit("current_super_heat") + print("Temperatures:") - print(f" DHW Temperature: {status.dhw_temperature:.1f}°F") print( - f" DHW Target Setting: {status.dhw_target_temperature_setting:.1f}°F" + f" DHW Temperature: {status.dhw_temperature:.1f}{unit}" + ) + print( + f" DHW Target Setting: {status.dhw_target_temperature_setting:.1f}{unit}" ) print( - f" Tank Upper: {status.tank_upper_temperature:.1f}°F" + f" Tank Upper: {status.tank_upper_temperature:.1f}{unit}" ) print( - f" Tank Lower: {status.tank_lower_temperature:.1f}°F" + f" Tank Lower: {status.tank_lower_temperature:.1f}{unit}" ) print( - f" Discharge: {status.discharge_temperature:.1f}°F" + f" Discharge: {status.discharge_temperature:.1f}{unit}" ) print( - f" Ambient: {status.ambient_temperature:.1f}°F" + f" Ambient: {status.ambient_temperature:.1f}{unit}" ) print("\nOperation:") @@ -176,10 +182,10 @@ def on_device_status(status: DeviceStatus): ) print(f" EEV Step: {status.eev_step}") print( - f" Super Heat: {status.current_super_heat:.1f}°F" + f" Super Heat: {status.current_super_heat:.1f}{superheat_unit}" ) print( - f" Flow Rate: {status.current_dhw_flow_rate:.1f} GPM" + f" Flow Rate: {status.current_dhw_flow_rate:.1f}{flow_unit}" ) print(f" Temperature Unit: {status.temperature_type.name}") diff --git a/examples/intermediate/event_driven_control.py b/examples/intermediate/event_driven_control.py index 2adb879..280374c 100644 --- a/examples/intermediate/event_driven_control.py +++ b/examples/intermediate/event_driven_control.py @@ -41,20 +41,20 @@ # Example 1: Multiple listeners for the same event def log_temperature(old_temp: float, new_temp: float): """Logger for temperature changes.""" - print(f"📊 [Logger] Temperature: {old_temp}°F → {new_temp}°F") + print(f"📊 [Logger] Temperature: {old_temp} → {new_temp}") def alert_on_high_temp(old_temp: float, new_temp: float): """Alert handler for high temperatures.""" if new_temp > 145: - print(f"[WARNING] [Alert] HIGH TEMPERATURE: {new_temp}°F!") + print(f"[WARNING] [Alert] HIGH TEMPERATURE: {new_temp}!") async def save_temperature_to_db(old_temp: float, new_temp: float): """Async database saver (simulated).""" # Simulate async database operation await asyncio.sleep(0.1) - print(f"💾 [Database] Saved temperature change: {new_temp}°F") + print(f"💾 [Database] Saved temperature change: {new_temp}") # Example 2: Mode change handlers @@ -90,7 +90,8 @@ def on_heating_stopped(status: DeviceStatus): def on_error_detected(error_code: str, status: DeviceStatus): """Handler for error detection.""" print(f"[ERROR] [Error] ERROR DETECTED: {error_code}") - print(f" Temperature: {status.dhw_temperature}°F") + unit = status.get_field_unit("dhw_temperature") + print(f" Temperature: {status.dhw_temperature}{unit}") print(f" Mode: {status.operation_mode}") @@ -190,7 +191,9 @@ async def main(): # One-time listener example mqtt_client.once( MqttClientEvents.STATUS_RECEIVED, - lambda s: print(f" 🎉 First status received: {s.dhw_temperature}°F"), + lambda s: print( + f" 🎉 First status received: {s.dhw_temperature}{s.get_field_unit('dhw_temperature')}" + ), ) print(" [SUCCESS] Registered one-time status handler") print() diff --git a/examples/intermediate/improved_auth.py b/examples/intermediate/improved_auth.py index 0bf0aef..f59e5ad 100644 --- a/examples/intermediate/improved_auth.py +++ b/examples/intermediate/improved_auth.py @@ -44,9 +44,10 @@ async def main(): # Step 4: Monitor device status def on_status(status): + unit = status.get_field_unit("dhw_temperature") print("\n📊 Device Status:") - print(f" Temperature: {status.dhw_temperature}°F") - print(f" Target: {status.dhw_temperature_setting}°F") + print(f" Temperature: {status.dhw_temperature}{unit}") + print(f" Target: {status.dhw_temperature_setting}{unit}") print(f" Power: {status.current_inst_power}W") await mqtt.subscribe_device_status(device, on_status) diff --git a/examples/intermediate/mqtt_realtime_monitoring.py b/examples/intermediate/mqtt_realtime_monitoring.py index fdd53c2..21abe0f 100755 --- a/examples/intermediate/mqtt_realtime_monitoring.py +++ b/examples/intermediate/mqtt_realtime_monitoring.py @@ -147,12 +147,13 @@ def on_device_status(status: DeviceStatus): """Typed callback for device status.""" message_count["count"] += 1 message_count["status"] += 1 + unit = status.get_field_unit("dhw_temperature") print( f"\n📊 Status Update #{message_count['status']} (Message #{message_count['count']})" ) - print(f" - DHW Temperature: {status.dhw_temperature:.1f}°F") - print(f" - Tank Upper: {status.tank_upper_temperature:.1f}°F") - print(f" - Tank Lower: {status.tank_lower_temperature:.1f}°F") + print(f" - DHW Temperature: {status.dhw_temperature:.1f}{unit}") + print(f" - Tank Upper: {status.tank_upper_temperature:.1f}{unit}") + print(f" - Tank Lower: {status.tank_lower_temperature:.1f}{unit}") print(f" - Operation Mode: {status.operation_mode}") print(f" - DHW Active: {status.dhw_use}") print(f" - Compressor: {status.comp_use}") diff --git a/examples/intermediate/periodic_requests.py b/examples/intermediate/periodic_requests.py index 79b6708..cc807de 100755 --- a/examples/intermediate/periodic_requests.py +++ b/examples/intermediate/periodic_requests.py @@ -73,8 +73,9 @@ def on_device_status(status: DeviceStatus): nonlocal status_count status_count += 1 + unit = status.get_field_unit("dhw_temperature") print(f"\n--- Status Response #{status_count} ---") - print(f" Temperature: {status.dhw_temperature:.1f}°F") + print(f" Temperature: {status.dhw_temperature:.1f}{unit}") print(f" Power: {status.current_inst_power:.1f}W") print(f" Available Energy: {status.available_energy_capacity:.0f} Wh") diff --git a/examples/intermediate/set_mode.py b/examples/intermediate/set_mode.py index 094eb11..ffae437 100644 --- a/examples/intermediate/set_mode.py +++ b/examples/intermediate/set_mode.py @@ -63,7 +63,8 @@ def on_mode_change_response(status): nonlocal mode_changed logger.info("Mode change response received!") logger.info(f"New mode: {status.operation_mode.name}") - logger.info(f"DHW Temperature: {status.dhw_temperature}°F") + unit = status.get_field_unit("dhw_temperature") + logger.info(f"DHW Temperature: {status.dhw_temperature}{unit}") logger.info(f"Tank Charge: {status.dhw_charge_per}%") mode_changed = True diff --git a/examples/intermediate/vacation_mode.py b/examples/intermediate/vacation_mode.py index 1e2487a..2bae187 100644 --- a/examples/intermediate/vacation_mode.py +++ b/examples/intermediate/vacation_mode.py @@ -51,8 +51,9 @@ def on_current_status(status): nonlocal current_status current_status = status logger.info(f"Current operation mode: {status.operation_mode.name}") + unit = status.get_field_unit("dhw_target_temperature_setting") logger.info( - f"Current DHW temperature setting: {status.dhw_target_temperature_setting}°F" + f"Current DHW temperature setting: {status.dhw_target_temperature_setting}{unit}" ) await mqtt_client.subscribe_device_status(device, on_current_status) diff --git a/examples/testing/periodic_device_info.py b/examples/testing/periodic_device_info.py index ee4b6df..1ace6b0 100755 --- a/examples/testing/periodic_device_info.py +++ b/examples/testing/periodic_device_info.py @@ -81,8 +81,9 @@ def on_device_feature(feature: DeviceFeature): print(f"Controller Serial: {feature.controller_serial_number}") print(f"Controller SW Version: {feature.controller_sw_version}") print(f"Heat Pump Use: {feature.heatpump_use}") + unit = feature.get_field_unit("dhw_temperature_min") print( - f"DHW Temp Min/Max: {feature.dhw_temperature_min}/{feature.dhw_temperature_max}°F" + f"DHW Temp Min/Max: {feature.dhw_temperature_min}/{feature.dhw_temperature_max}{unit}" ) # Subscribe with typed parsing diff --git a/examples/testing/test_periodic_minimal.py b/examples/testing/test_periodic_minimal.py index 6b6ce88..0d87128 100755 --- a/examples/testing/test_periodic_minimal.py +++ b/examples/testing/test_periodic_minimal.py @@ -54,8 +54,9 @@ def on_device_status(status: DeviceStatus): nonlocal message_count message_count += 1 timestamp = datetime.now().strftime("%H:%M:%S") + unit = status.get_field_unit("dhw_temperature") print(f"[{timestamp}] Status #{message_count}") - print(f" Temperature: {status.dhw_temperature:.1f}°F") + print(f" Temperature: {status.dhw_temperature:.1f}{unit}") print(f" Power: {status.current_inst_power:.1f}W") # Subscribe with typed parsing diff --git a/src/nwp500/cli/output_formatters.py b/src/nwp500/cli/output_formatters.py index 1a4b89b..ee28df0 100644 --- a/src/nwp500/cli/output_formatters.py +++ b/src/nwp500/cli/output_formatters.py @@ -562,6 +562,13 @@ def print_device_status(device_status: Any) -> None: "HEAT PUMP SETTINGS", "Upper On", ) + _add_numeric_item( + all_items, + device_status, + "hp_upper_on_diff_temp_setting", + "HEAT PUMP SETTINGS", + "Upper On Diff", + ) _add_numeric_item( all_items, device_status, @@ -569,6 +576,13 @@ def print_device_status(device_status: Any) -> None: "HEAT PUMP SETTINGS", "Upper Off", ) + _add_numeric_item( + all_items, + device_status, + "hp_upper_off_diff_temp_setting", + "HEAT PUMP SETTINGS", + "Upper Off Diff", + ) _add_numeric_item( all_items, device_status, @@ -576,6 +590,13 @@ def print_device_status(device_status: Any) -> None: "HEAT PUMP SETTINGS", "Lower On", ) + _add_numeric_item( + all_items, + device_status, + "hp_lower_on_diff_temp_setting", + "HEAT PUMP SETTINGS", + "Lower On Diff", + ) _add_numeric_item( all_items, device_status, @@ -583,6 +604,13 @@ def print_device_status(device_status: Any) -> None: "HEAT PUMP SETTINGS", "Lower Off", ) + _add_numeric_item( + all_items, + device_status, + "hp_lower_off_diff_temp_setting", + "HEAT PUMP SETTINGS", + "Lower Off Diff", + ) # Heat Element Settings _add_numeric_item( @@ -592,6 +620,13 @@ def print_device_status(device_status: Any) -> None: "HEAT ELEMENT SETTINGS", "Upper On", ) + _add_numeric_item( + all_items, + device_status, + "he_upper_on_diff_temp_setting", + "HEAT ELEMENT SETTINGS", + "Upper On Diff", + ) _add_numeric_item( all_items, device_status, @@ -599,6 +634,13 @@ def print_device_status(device_status: Any) -> None: "HEAT ELEMENT SETTINGS", "Upper Off", ) + _add_numeric_item( + all_items, + device_status, + "he_upper_off_diff_temp_setting", + "HEAT ELEMENT SETTINGS", + "Upper Off Diff", + ) _add_numeric_item( all_items, device_status, @@ -606,6 +648,13 @@ def print_device_status(device_status: Any) -> None: "HEAT ELEMENT SETTINGS", "Lower On", ) + _add_numeric_item( + all_items, + device_status, + "he_lower_on_diff_temp_setting", + "HEAT ELEMENT SETTINGS", + "Lower On Diff", + ) _add_numeric_item( all_items, device_status, @@ -613,6 +662,13 @@ def print_device_status(device_status: Any) -> None: "HEAT ELEMENT SETTINGS", "Lower Off", ) + _add_numeric_item( + all_items, + device_status, + "he_lower_off_diff_temp_setting", + "HEAT ELEMENT SETTINGS", + "Lower Off Diff", + ) # Power & Energy if hasattr(device_status, "wh_total_power_consumption"): diff --git a/src/nwp500/converters.py b/src/nwp500/converters.py index d2a88e8..03e3711 100644 --- a/src/nwp500/converters.py +++ b/src/nwp500/converters.py @@ -17,7 +17,7 @@ from pydantic import ValidationInfo, ValidatorFunctionWrapHandler from .enums import TemperatureType, TempFormulaType -from .temperature import DeciCelsius, HalfCelsius, RawCelsius +from .temperature import DeciCelsius, DeciCelsiusDelta, HalfCelsius, RawCelsius from .unit_system import get_unit_system _logger = logging.getLogger(__name__) @@ -35,6 +35,7 @@ "flow_rate_to_preferred", "volume_to_preferred", "div_10_celsius_to_preferred", + "div_10_celsius_delta_to_preferred", ] @@ -479,3 +480,40 @@ def div_10_celsius_to_preferred( # Convert Celsius to Fahrenheit return round(celsius * 9 / 5 + 32, 1) + + +def div_10_celsius_delta_to_preferred( + value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo +) -> float: + """Convert decicelsius delta value (raw / 10) to preferred unit (C or F). + + Raw device values are in tenths of Celsius (0.1°C per unit). + This represents a temperature DELTA (difference), not an absolute + temperature. + + Key difference from div_10_celsius_to_preferred: For deltas, we apply the + scale factor but NOT the +32 offset. + + - If Metric (Celsius) mode: Return Celsius delta (value / 10.0) + - If Imperial (Fahrenheit) mode: Convert to Fahrenheit delta (no +32) + + Uses WrapValidator instead of BeforeValidator to access ValidationInfo.data, + which contains sibling fields needed to determine the device's temperature + preference (Celsius or Fahrenheit). + + Args: + value: Raw device value (tenths of Celsius delta). + handler: Pydantic next validator handler. Not invoked as we bypass the + validation chain to directly convert using the device's temperature + preference. WrapValidator is required for access to ValidationInfo. + info: Pydantic validation context containing sibling fields, used to + retrieve the device's temperature_type preference. + + Returns: + Temperature delta in preferred unit (Celsius or Fahrenheit). + """ + is_celsius = _get_temperature_preference(info) + + if isinstance(value, (int, float)): + return DeciCelsiusDelta(value).to_preferred(is_celsius) + return float(value) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index db63edd..35d61d9 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -25,6 +25,7 @@ deci_celsius_to_preferred, device_bool_to_python, div_10, + div_10_celsius_delta_to_preferred, div_10_celsius_to_preferred, enum_validator, flow_rate_to_preferred, @@ -80,6 +81,9 @@ Div10CelsiusToPreferred = Annotated[ float, WrapValidator(div_10_celsius_to_preferred) ] +Div10CelsiusDeltaToPreferred = Annotated[ + float, WrapValidator(div_10_celsius_delta_to_preferred) +] FlowRate = Annotated[float, WrapValidator(flow_rate_to_preferred)] Volume = Annotated[float, WrapValidator(volume_to_preferred)] TouStatus = Annotated[bool, BeforeValidator(bool)] @@ -709,49 +713,49 @@ class DeviceStatus(NavienBaseModel): "device_class": "flow_rate", }, ) - hp_upper_on_diff_temp_setting: Div10CelsiusToPreferred = Field( + hp_upper_on_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heat pump upper on differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - hp_upper_off_diff_temp_setting: Div10CelsiusToPreferred = Field( + hp_upper_off_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heat pump upper off differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - hp_lower_on_diff_temp_setting: Div10CelsiusToPreferred = Field( + hp_lower_on_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heat pump lower on differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - hp_lower_off_diff_temp_setting: Div10CelsiusToPreferred = Field( + hp_lower_off_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heat pump lower off differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - he_upper_on_diff_temp_setting: Div10CelsiusToPreferred = Field( + he_upper_on_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heater element upper on differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - he_upper_off_diff_temp_setting: Div10CelsiusToPreferred = Field( + he_upper_off_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heater element upper off differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", "device_class": "temperature", }, ) - he_lower_on_diff_temp_setting: Div10CelsiusToPreferred = Field( + he_lower_on_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( alias="heLowerOnTDiffempSetting", description="Heater element lower on differential temperature setting", json_schema_extra={ @@ -759,7 +763,7 @@ class DeviceStatus(NavienBaseModel): "device_class": "temperature", }, ) # Handle API typo: heLowerOnTDiffempSetting -> heLowerOnDiffTempSetting - he_lower_off_diff_temp_setting: Div10CelsiusToPreferred = Field( + he_lower_off_diff_temp_setting: Div10CelsiusDeltaToPreferred = Field( description="Heater element lower off differential temperature setting", json_schema_extra={ "unit_of_measurement": "°F", diff --git a/src/nwp500/temperature.py b/src/nwp500/temperature.py index 8e50e45..3d76a4f 100644 --- a/src/nwp500/temperature.py +++ b/src/nwp500/temperature.py @@ -371,3 +371,77 @@ def deci_celsius_to_fahrenheit(value: Any) -> float: if isinstance(value, (int, float)): return DeciCelsius(value).to_fahrenheit() return float(value) + + +class DeciCelsiusDelta(Temperature): + """Temperature delta in decicelsius (0.1°C precision). + + Represents a temperature difference/delta, NOT an absolute temperature. + Used for differential temperature settings (e.g., heat pump on/off Diff). + Formula: raw_value / 10.0 converts to Celsius delta. + + Key difference from DeciCelsius: When converting to Fahrenheit, we apply + the scale factor (9/5) but NOT the offset (+32), since this is a delta not + an absolute temperature. + + Example: + >>> temp = DeciCelsiusDelta(5) # Raw device value 5 + >>> temp.to_celsius() + 0.5 + >>> temp.to_fahrenheit() + 0.9 # 0.5°C * 9/5 = 0.9°F, no +32 offset + """ + + def to_celsius(self) -> float: + """Convert to Celsius delta. + + Returns: + Temperature delta in Celsius. + """ + return self.raw_value / 10.0 + + def to_fahrenheit(self) -> float: + """Convert to Fahrenheit delta (without +32 offset). + + Returns: + Temperature delta in Fahrenheit. + """ + celsius = self.to_celsius() + return round(celsius * 9 / 5, 1) + + @classmethod + def from_fahrenheit(cls, fahrenheit: float) -> DeciCelsiusDelta: + """Create DeciCelsiusDelta from Fahrenheit delta (for device commands). + + Args: + fahrenheit: Temperature delta in Fahrenheit. + + Returns: + DeciCelsiusDelta instance with raw value for device. + + Example: + >>> temp = DeciCelsiusDelta.from_fahrenheit(0.9) + >>> temp.raw_value + 5 + """ + celsius = fahrenheit * 5 / 9 + raw_value = round(celsius * 10) + return cls(raw_value) + + @classmethod + def from_celsius(cls, celsius: float) -> DeciCelsiusDelta: + """Create DeciCelsiusDelta from Celsius delta (for device commands). + + Args: + celsius: Temperature delta in Celsius. + + Returns: + DeciCelsiusDelta instance with raw value for device. + + Example: + >>> temp = DeciCelsiusDelta.from_celsius(0.5) + >>> temp.raw_value + 5 + """ + raw_value = round(celsius * 10) + return cls(raw_value) From 9c28dccca13221ba654e76e336a9f1bebbb8c709 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Tue, 27 Jan 2026 15:30:55 -0800 Subject: [PATCH 2/2] Remove round() from DeciCelsiusDelta.to_fahrenheit() for consistency Address PR review comment: DeciCelsiusDelta.to_fahrenheit() now matches the parent DeciCelsius.to_fahrenheit() by not applying rounding. This ensures consistency across temperature conversion methods. --- src/nwp500/temperature.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nwp500/temperature.py b/src/nwp500/temperature.py index 3d76a4f..ef43501 100644 --- a/src/nwp500/temperature.py +++ b/src/nwp500/temperature.py @@ -407,7 +407,7 @@ def to_fahrenheit(self) -> float: Temperature delta in Fahrenheit. """ celsius = self.to_celsius() - return round(celsius * 9 / 5, 1) + return celsius * 9 / 5 @classmethod def from_fahrenheit(cls, fahrenheit: float) -> DeciCelsiusDelta: