diff --git a/docs/DEVICE_STATUS_FIELDS.rst b/docs/DEVICE_STATUS_FIELDS.rst index 57607ef..4f9c5ec 100644 --- a/docs/DEVICE_STATUS_FIELDS.rst +++ b/docs/DEVICE_STATUS_FIELDS.rst @@ -32,7 +32,7 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Indicates if the device has recently reloaded or restarted. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``errorCode`` - integer - None @@ -52,22 +52,22 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Indicates if the device is currently performing heating operations (True=busy, False=idle). - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``freezeProtectionUse`` - bool - None - Whether freeze protection is active. When tank water temperature falls below 43°F (6°C), the electric heater activates to prevent freezing. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``dhwUse`` - bool - None - Domestic Hot Water (DHW) usage status - indicates if hot water is currently being drawn from the tank. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``dhwUseSustained`` - bool - None - Sustained DHW usage status - indicates prolonged hot water usage. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``dhwTemperature`` - integer - °F @@ -82,7 +82,7 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Whether a program reservation (scheduled operation) is in use. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``smartDiagnostic`` - integer - None @@ -107,7 +107,7 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Whether ECO (Energy Cut Off) safety feature has been triggered. The ECO switch is a high-temperature safety limit. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``dhwTargetTemperatureSetting`` - integer - °F @@ -127,22 +127,22 @@ This document lists the fields found in the ``status`` object of device status m - integer - °F - Compressor discharge temperature - temperature of refrigerant leaving the compressor. - - ``raw / 10.0`` + - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit) * - ``suctionTemperature`` - integer - °F - Compressor suction temperature - temperature of refrigerant entering the compressor. - - ``raw / 10.0`` + - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit) * - ``evaporatorTemperature`` - integer - °F - Evaporator temperature - temperature where heat is absorbed from ambient air. - - ``raw / 10.0`` + - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit) * - ``ambientTemperature`` - integer - °F - Ambient air temperature measured at the heat pump air intake. - - ``(raw / 22.4) * 9/5 + 32`` + - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit) * - ``targetSuperHeat`` - integer - °F @@ -152,17 +152,17 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Compressor usage status (True=On, False=Off). The compressor is the main component of the heat pump. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``eevUse`` - bool - None - Electronic Expansion Valve (EEV) usage status (True=active, False=inactive). The EEV controls refrigerant flow. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``evaFanUse`` - bool - None - Evaporator fan usage status (True=On, False=Off). The fan pulls ambient air through the evaporator coil. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``currentInstPower`` - integer - W @@ -172,17 +172,17 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Shut-off valve usage status. The valve controls refrigerant flow in the system. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``conOvrSensorUse`` - bool - None - Condensate overflow sensor usage status. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``wtrOvrSensorUse`` - bool - None - Water overflow/leak sensor usage status. Triggers error E799 if leak detected. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``dhwChargePer`` - integer - % @@ -212,7 +212,7 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Whether anti-legionella function is enabled. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``antiLegionellaPeriod`` - integer - days @@ -222,7 +222,7 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Whether the anti-legionella function is busy. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``programReservationType`` - integer - None @@ -247,12 +247,12 @@ This document lists the fields found in the ``status`` object of device status m - bool - None - Whether the error buzzer is enabled. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``currentHeatUse`` - bool - None - Current heat usage. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``currentInletTemperature`` - float - °F @@ -302,27 +302,27 @@ This document lists the fields found in the ``status`` object of device status m - integer - °F - Current superheat value - actual temperature difference between suction and evaporator temperatures. - - ``raw / 10.0`` + - ``(raw / 10) * 9/5 + 32`` (decicelsius to Fahrenheit) * - ``heatUpperUse`` - bool - None - Upper electric heating element usage status (True=On, False=Off). Power: 3,755W @ 208V or 5,000W @ 240V. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``heatLowerUse`` - bool - None - Lower electric heating element usage status (True=On, False=Off). Power: 3,755W @ 208V or 5,000W @ 240V. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``scaldUse`` - bool - None - Scald protection active status. Displays warning when water temperature reaches levels that could cause scalding. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``airFilterAlarmUse`` - bool - None - Air filter alarm usage - indicates if air filter maintenance reminder is enabled. - - Converted from integer (0 or 1) to bool + - Converted from integer (1=OFF, 2=ON) to bool * - ``airFilterAlarmPeriod`` - integer - hours diff --git a/src/nwp500/cli.py b/src/nwp500/cli.py index 9ff0159..43d0d71 100644 --- a/src/nwp500/cli.py +++ b/src/nwp500/cli.py @@ -205,6 +205,37 @@ def on_status(status: DeviceStatus): _logger.error("Timed out waiting for device status response.") +async def handle_status_raw_request(mqtt: NavienMqttClient, device: Device): + """Request device status once and print raw MQTT data (no conversions).""" + future = asyncio.get_running_loop().create_future() + + # Subscribe to the raw MQTT topic to capture data before conversion + def raw_callback(topic: str, message: dict): + if not future.done(): + # Extract and print the raw status portion + if "response" in message and "status" in message["response"]: + print( + json.dumps( + message["response"]["status"], indent=2, default=_json_default_serializer + ) + ) + future.set_result(None) + elif "status" in message: + print(json.dumps(message["status"], indent=2, default=_json_default_serializer)) + future.set_result(None) + + # Subscribe to all device messages + await mqtt.subscribe_device(device, raw_callback) + + _logger.info("Requesting device status (raw)...") + await mqtt.request_device_status(device) + + try: + await asyncio.wait_for(future, timeout=10) + except asyncio.TimeoutError: + _logger.error("Timed out waiting for device status response.") + + async def handle_device_info_request(mqtt: NavienMqttClient, device: Device): """ Request comprehensive device information via MQTT and print it. @@ -504,6 +535,9 @@ async def async_main(args: argparse.Namespace): _logger.info("Getting updated status after temperature change...") await asyncio.sleep(2) # Brief pause for device to process await handle_status_request(mqtt, device) + elif args.status_raw: + # Raw status request (no conversions) + await handle_status_raw_request(mqtt, device) elif args.status: # Status-only request await handle_status_request(mqtt, device) @@ -549,6 +583,12 @@ def parse_args(args): action="store_true", help="Fetch and print the current device status. Can be combined with control commands.", ) + parser.add_argument( + "--status-raw", + action="store_true", + help="Fetch and print the raw device status as received from MQTT " + "(no conversions applied).", + ) # Primary action modes (mutually exclusive) group = parser.add_mutually_exclusive_group() @@ -635,6 +675,12 @@ def setup_logging(loglevel): def main(args): """Wrapper for the asynchronous main function.""" args = parse_args(args) + + # Validate that --status and --status-raw are not used together + if args.status and args.status_raw: + print("Error: --status and --status-raw cannot be used together.", file=sys.stderr) + return 1 + # Set default log level for libraries setup_logging(logging.WARNING) # Set user-defined log level for this script diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 5011be8..55855dd 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -13,6 +13,24 @@ _logger = logging.getLogger(__name__) +def _decicelsius_to_fahrenheit(raw_value: float) -> float: + """ + Convert a raw decicelsius value to Fahrenheit. + + Args: + raw_value: Raw value in decicelsius (tenths of degrees Celsius) + + Returns: + Temperature in Fahrenheit + + Example: + >>> _decicelsius_to_fahrenheit(250) # 25.0°C + 77.0 + """ + celsius = raw_value / 10.0 + return (celsius * 9 / 5) + 32 + + class OperationMode(Enum): """Enumeration for the operation modes of the device. @@ -306,6 +324,11 @@ def from_dict(cls, data: dict): ) # Convert integer-based booleans + # The device uses a non-standard encoding for boolean values: + # 0 = Not applicable/disabled (rarely used) + # 1 = OFF/Inactive/False + # 2 = ON/Active/True + # This applies to ALL boolean fields in the device status bool_fields = [ "didReload", "operationBusy", @@ -329,9 +352,11 @@ def from_dict(cls, data: dict): "scaldUse", "airFilterAlarmUse", ] + + # Convert using the device's encoding: 0 or 1=false, 2=true for field_name in bool_fields: if field_name in converted_data: - converted_data[field_name] = bool(converted_data[field_name]) + converted_data[field_name] = converted_data[field_name] == 2 # Convert temperatures with 'raw + 20' formula add_20_fields = [ @@ -353,15 +378,11 @@ def from_dict(cls, data: dict): if field_name in converted_data: converted_data[field_name] += 20 - # Convert fields with 'raw / 10.0' formula + # Convert fields with 'raw / 10.0' formula (non-temperature fields) div_10_fields = [ - "dischargeTemperature", - "suctionTemperature", - "evaporatorTemperature", "targetSuperHeat", "currentInletTemperature", "currentDhwFlowRate", - "currentSuperHeat", "hpUpperOnDiffTempSetting", "hpUpperOffDiffTempSetting", "hpLowerOnDiffTempSetting", @@ -375,23 +396,28 @@ def from_dict(cls, data: dict): if field_name in converted_data: converted_data[field_name] /= 10.0 - # Special conversion for ambientTemperature - if "ambientTemperature" in converted_data: - raw_temp = converted_data["ambientTemperature"] - # Based on observed data: raw 503.6 corresponds to 72.5°F - # Convert raw value to Celsius first (raw / 22.4 ≈ Celsius) - # Then convert Celsius to Fahrenheit - celsius = raw_temp / 22.4 - converted_data["ambientTemperature"] = (celsius * 9 / 5) + 32 - # Special conversion for tank temperatures (decicelsius to Fahrenheit) tank_temp_fields = ["tankUpperTemperature", "tankLowerTemperature"] for field_name in tank_temp_fields: if field_name in converted_data: - raw_temp = converted_data[field_name] - # Convert from decicelsius (raw / 10.0) to Celsius, then to Fahrenheit - celsius = raw_temp / 10.0 - converted_data[field_name] = (celsius * 9 / 5) + 32 + converted_data[field_name] = _decicelsius_to_fahrenheit(converted_data[field_name]) + + # Special conversion for dischargeTemperature (decicelsius to Fahrenheit) + if "dischargeTemperature" in converted_data: + converted_data["dischargeTemperature"] = _decicelsius_to_fahrenheit( + converted_data["dischargeTemperature"] + ) + + # Special conversion for heat pump temperatures (decicelsius to Fahrenheit) + heat_pump_temp_fields = [ + "suctionTemperature", + "evaporatorTemperature", + "ambientTemperature", + "currentSuperHeat", + ] + for field_name in heat_pump_temp_fields: + if field_name in converted_data: + converted_data[field_name] = _decicelsius_to_fahrenheit(converted_data[field_name]) # Convert enum fields with error handling for unknown values if "operationMode" in converted_data: