From 9dfabbe9a732ec17686e9d5fca7877fc8a79ee69 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 11 Oct 2025 19:44:49 -0700 Subject: [PATCH 1/3] power on / off feature in cli --- README.rst | 23 ++- docs/DEVICE_STATUS_FIELDS.rst | 226 +++++++++++++++++++++++++++--- docs/MQTT_CLIENT.rst | 19 ++- docs/MQTT_MESSAGES.rst | 2 +- examples/power_control_example.py | 137 ++++++++++++++++++ src/nwp500/cli.py | 81 +++++++++++ src/nwp500/models.py | 25 +++- 7 files changed, 481 insertions(+), 32 deletions(-) create mode 100644 examples/power_control_example.py diff --git a/README.rst b/README.rst index 7841d12..fdbbf57 100644 --- a/README.rst +++ b/README.rst @@ -79,6 +79,15 @@ The library includes a command line interface for quick monitoring and device in # Get device feature/capability information python -m nwp500.cli --device-feature + # Turn device on + python -m nwp500.cli --power-on + + # Turn device off + python -m nwp500.cli --power-off + + # Turn device on and see updated status + python -m nwp500.cli --power-on --status + # Set operation mode and see response python -m nwp500.cli --set-mode energy-saver @@ -105,6 +114,8 @@ The library includes a command line interface for quick monitoring and device in * ``--status``: Print current device status as JSON. Can be combined with control commands to see updated status. * ``--device-info``: Print comprehensive device information (firmware, model, capabilities) via MQTT as JSON and exit * ``--device-feature``: Print device capabilities and feature settings via MQTT as JSON and exit +* ``--power-on``: Turn the device on and display response +* ``--power-off``: Turn the device off and display response * ``--set-mode MODE``: Set operation mode and display response. Valid modes: heat-pump, energy-saver, high-demand, electric, vacation, standby * ``--set-dhw-temp TEMP``: Set DHW (Domestic Hot Water) target temperature in Fahrenheit (115-150°F) and display response * ``--monitor``: Continuously monitor status every 30 seconds and log to CSV (default) @@ -156,19 +167,21 @@ Operation Modes * - Heat Pump Mode - 1 - Most energy-efficient mode using only the heat pump. Longest recovery time. - * - Energy Saver Mode + * - Electric Mode - 2 + - Fastest recovery using only electric heaters. Least energy-efficient. + * - Energy Saver Mode + - 3 - Default mode. Balances efficiency and recovery time using both heat pump and electric heater. * - High Demand Mode - - 3 - - Uses electric heater more frequently for faster recovery time. - * - Electric Mode - 4 - - Fastest recovery using only electric heaters. Least energy-efficient. + - Uses electric heater more frequently for faster recovery time. * - Vacation Mode - 5 - Suspends heating to save energy during extended absences. +**Important:** When you set a mode, you're configuring the ``dhwOperationSetting`` (what mode to use when heating). The device's current operational state is reported in ``operationMode`` (0=Standby, 32=Heat Pump active, 64=Energy Saver active, 96=High Demand active). See the `Device Status Fields documentation `_ for details on this distinction. + MQTT Protocol ============= diff --git a/docs/DEVICE_STATUS_FIELDS.rst b/docs/DEVICE_STATUS_FIELDS.rst index 20a15bb..f8380ac 100644 --- a/docs/DEVICE_STATUS_FIELDS.rst +++ b/docs/DEVICE_STATUS_FIELDS.rst @@ -44,9 +44,9 @@ This document lists the fields found in the ``status`` object of device status m - Sub error code providing additional error details. See ERROR_CODES.rst for details. - None * - ``operationMode`` - - integer + - OperationMode - None - - The current operation mode of the device. See Operation Modes section below. + - The current **actual operational state** of the device (what it's doing RIGHT NOW). Reports status values: 0=Standby, 32=Heat Pump active, 64=Energy Saver active, 96=High Demand active. See Operation Modes section below for the critical distinction between this and ``dhwOperationSetting``. - None * - ``operationBusy`` - integer @@ -229,9 +229,9 @@ This document lists the fields found in the ``status`` object of device status m - Type of program reservation. - None * - ``dhwOperationSetting`` - - integer + - OperationMode - None - - DHW operation setting. + - User's configured DHW operation mode preference. This field uses the same ``OperationMode`` enum as ``operationMode`` but contains command mode values (1=HEAT_PUMP, 2=ELECTRIC, 3=ENERGY_SAVER, 4=HIGH_DEMAND, 5=VACATION, 6=POWER_OFF). When the device is powered off via the power-off command, this field will show 6 (POWER_OFF). This is how to distinguish between "powered off" vs "on but in standby". See the Operation Modes section below for details. - None * - ``temperatureType`` - integer @@ -469,20 +469,20 @@ The ``operationMode`` field is an integer that maps to the following modes. Thes - High - Most energy-efficient mode, using only the heat pump. Recovery time varies with ambient temperature and humidity. Higher ambient temperature and humidity improve efficiency and reduce recovery time. * - 2 + - Electric + - Fast + - Very Low + - Uses only upper and lower electric heaters (not simultaneously). Least energy-efficient with shortest recovery time. Can operate continuously for up to 72 hours before automatically reverting to previous mode. + * - 3 - Energy Saver (Hybrid: Efficiency) - Fast - Very High - Default mode. Combines the heat pump and electric heater for balanced efficiency and recovery time. Heat pump is primarily used with electric heater for backup. Applied during initial shipment and factory reset. - * - 3 + * - 4 - High Demand (Hybrid: Boost) - Very Fast - Low - Combines heat pump and electric heater with more frequent use of electric heater for faster recovery. Suitable when higher hot water supply is needed. - * - 4 - - Electric - - Fast - - Very Low - - Uses only upper and lower electric heaters (not simultaneously). Least energy-efficient with shortest recovery time. Can operate continuously for up to 72 hours before automatically reverting to previous mode. * - 5 - Vacation - None @@ -490,10 +490,10 @@ The ``operationMode`` field is an integer that maps to the following modes. Thes - Suspends heating to save energy during absences (0-99 days). Only minimal operations like freeze protection and anti-seize are performed. Heating resumes 9 hours before the vacation period ends. -Observed Operation Modes (from network traffic analysis) +Operation Modes -------------------------------------------------------- -The following ``operationMode`` values have been observed in status messages from the device. These values appear to correspond to the commanded modes as follows: +The following ``operationMode`` values in status messages from the device. These values appear to correspond to the commanded modes as follows: .. list-table:: :header-rows: 1 @@ -509,13 +509,205 @@ The following ``operationMode`` values have been observed in status messages fro - Heat Pump - Corresponds to commanded mode ``HEAT_PUMP`` (1). * - 64 - - Energy Saver - - Corresponds to commanded mode ``ENERGY_SAVER`` (2). + - Energy Saver (Hybrid: Efficiency) + - Corresponds to commanded mode ``ENERGY_SAVER`` (3). * - 96 - - High Demand - - Corresponds to commanded mode ``HIGH_DEMAND`` (3). + - High Demand (Hybrid: Boost) + - Corresponds to commanded mode ``HIGH_DEMAND`` (4). + +The commanded mode ``ELECTRIC`` (2) has been observed to result in ``operationMode`` values of both 64 and 96 at different times. + +Understanding operationMode vs dhwOperationSetting +--------------------------------------------------- + +These two fields serve different purposes and it's critical to understand their relationship: + +Field Definitions +^^^^^^^^^^^^^^^^^ + +**dhwOperationSetting** (OperationMode enum with command values 1-5) + The user's **configured mode preference** - what heating mode the device should use when it needs to heat water. This is set via the ``dhw-mode`` command and persists until changed by the user or device. + + * Type: ``OperationMode`` enum + * Values: + + * 1 = ``HEAT_PUMP`` (Heat Pump Only) + * 2 = ``ELECTRIC`` (Electric Only) + * 3 = ``ENERGY_SAVER`` (Hybrid: Efficiency) + * 4 = ``HIGH_DEMAND`` (Hybrid: Boost) + * 5 = ``VACATION`` (Vacation mode) + * 6 = ``POWER_OFF`` (Device is powered off) + + * Set by: User via app, CLI, or MQTT command + * Changes: Only when user explicitly changes the mode or powers device off/on + * Meaning: "When heating is needed, use this mode" OR "I'm powered off" (if value is 6) + * **Critical**: Value 6 (``POWER_OFF``) indicates the device was powered off via the power-off command. This is how to distinguish between "powered off" and "on but idle". + +**operationMode** (OperationMode enum with status values 0, 32, 64, 96) + The device's **current actual operational state** - what the device is doing RIGHT NOW. This reflects real-time operation and changes automatically based on whether the device is idle or actively heating. + + * Type: ``OperationMode`` enum + * Values: + + * 0 = ``STANDBY`` (Idle, not heating) + * 32 = ``HEAT_PUMP_MODE`` (Heat Pump actively running) + * 64 = ``HYBRID_EFFICIENCY_MODE`` (Energy Saver actively heating) + * 96 = ``HYBRID_BOOST_MODE`` (High Demand actively heating) + + * Set by: Device automatically based on heating demand + * Changes: Dynamically as device starts/stops heating + * Meaning: "This is what I'm doing right now" + * **Note**: This field shows ``STANDBY`` (0) both when device is powered off AND when it's on but not heating. Check ``dhwOperationSetting`` to determine if device is actually powered off (value 6). + +Key Relationship +^^^^^^^^^^^^^^^^ + +The relationship between these fields can be summarized as: + +* ``dhwOperationSetting`` = "What mode to use when heating" +* ``operationMode`` = "Am I heating right now, and if so, how?" + +A device can be **idle** (``operationMode = STANDBY``) while still being **configured** for a specific heating mode (``dhwOperationSetting = ENERGY_SAVER``). When the tank temperature drops and heating begins, ``operationMode`` will change to reflect active heating (e.g., ``HYBRID_EFFICIENCY_MODE``), but ``dhwOperationSetting`` remains unchanged. + +Real-World Examples +^^^^^^^^^^^^^^^^^^^ + +**Example 1: Energy Saver Mode, Tank is Hot** + :: + + dhwOperationSetting = 3 (ENERGY_SAVER) # Configured mode + operationMode = 0 (STANDBY) # Currently idle + dhwChargePer = 100 # Tank is fully charged + + *Interpretation:* Device is configured for Energy Saver mode, but water is already at temperature so no heating is occurring. + +**Example 2: Energy Saver Mode, Actively Heating** + :: + + dhwOperationSetting = 3 (ENERGY_SAVER) # Configured mode + operationMode = 64 (HYBRID_EFFICIENCY_MODE) # Actively heating + operationBusy = true # Heating in progress + dhwChargePer = 75 # Tank at 75% + + *Interpretation:* Device is using Energy Saver mode to heat the tank, currently at 75% charge. + +**Example 3: High Demand Mode, Heat Pump Running** + :: + + dhwOperationSetting = 4 (HIGH_DEMAND) # Configured mode + operationMode = 32 (HEAT_PUMP_MODE) # Heat pump active + compUse = true # Compressor running + + *Interpretation:* Device is configured for High Demand but is currently running just the heat pump component (hybrid heating will engage electric elements as needed). + +**Example 4: Device Powered Off** + :: + + dhwOperationSetting = 6 (POWER_OFF) # Device powered off + operationMode = 0 (STANDBY) # Currently idle + operationBusy = false # No heating + + *Interpretation:* Device was powered off using the power-off command. Although ``operationMode`` shows ``STANDBY`` (same as an idle device), the ``dhwOperationSetting`` value of 6 indicates it's actually powered off, not just idle. + +Displaying Status in a User Interface +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For user-facing applications, follow these guidelines: + +**Primary Mode Display** +**Primary Mode Display** + Use ``dhwOperationSetting`` to show the user's configured mode preference. This is what users expect to see as "the current mode" because it represents their selection. + + **Important**: Check for value 6 (``POWER_OFF``) first to show "Off" or "Powered Off" status. + + Example display:: + + Mode: Energy Saver [when dhwOperationSetting = 1-5] + Mode: Off [when dhwOperationSetting = 6] + +**Status Indicator** + Use ``operationMode`` to show real-time operational status: + + * ``STANDBY`` (0): Show "Idle" or "Standby" indicator (but check ``dhwOperationSetting`` for power-off state) + * ``HEAT_PUMP_MODE`` (32): Show "Heating (Heat Pump)" or heating indicator + * ``HYBRID_EFFICIENCY_MODE`` (64): Show "Heating (Energy Saver)" or heating indicator + * ``HYBRID_BOOST_MODE`` (96): Show "Heating (High Demand)" or heating indicator + +**Combined Display Examples** + :: + + # Device on and idle + Mode: Energy Saver + Status: Idle ○ + Tank: 100% + + # Device on and heating + Mode: Energy Saver + Status: Heating ● + Tank: 75% + + # Device powered off + Mode: Off + Status: Powered Off + Tank: 100% + +**Code Example** + .. code-block:: python + + def format_mode_display(status: DeviceStatus) -> dict: + """Format mode and status for UI display.""" + + # Check if device is powered off first + if status.dhwOperationSetting == OperationMode.POWER_OFF: + return { + 'configured_mode': 'Off', + 'operational_state': 'Powered Off', + 'is_heating': False, + 'is_powered_on': False, + 'tank_charge': status.dhwChargePer, + } + + # User's configured mode (what they selected) + configured_mode = status.dhwOperationSetting.name.replace('_', ' ').title() + + # Current operational state + if status.operationMode == OperationMode.STANDBY: + operational_state = "Idle" + is_heating = False + elif status.operationMode == OperationMode.HEAT_PUMP_MODE: + operational_state = "Heating (Heat Pump)" + is_heating = True + elif status.operationMode == OperationMode.HYBRID_EFFICIENCY_MODE: + operational_state = "Heating (Energy Saver)" + is_heating = True + elif status.operationMode == OperationMode.HYBRID_BOOST_MODE: + operational_state = "Heating (High Demand)" + is_heating = True + else: + operational_state = "Unknown" + is_heating = False + + return { + 'configured_mode': configured_mode, # "Energy Saver" + 'operational_state': operational_state, # "Idle" or "Heating..." + 'is_heating': is_heating, # True/False + 'is_powered_on': True, # Device is on + 'tank_charge': status.dhwChargePer, # 0-100 + } + +**Display Notes** + +1. **Never display operationMode as "the mode"** - users don't care that the device is in "HYBRID_EFFICIENCY_MODE", they want to know it's set to "Energy Saver" + +2. **Do use operationMode for heating indicators** - it tells you whether the device is actively heating right now + +3. **Mode changes affect dhwOperationSetting** - when a user changes the mode, you're setting ``dhwOperationSetting`` + +4. **operationMode changes automatically** - you cannot directly set this; it changes based on device operation + +5. **Both fields use OperationMode enum** - but different value ranges (1-6 for dhwOperationSetting, 0/32/64/96 for operationMode) -The commanded mode ``ELECTRIC`` (4) has been observed to result in ``operationMode`` values of both 64 and 96 at different times. +6. **Power off detection** - Check if ``dhwOperationSetting == 6`` (``POWER_OFF``) to determine if device is powered off vs just idle Technical Notes --------------- diff --git a/docs/MQTT_CLIENT.rst b/docs/MQTT_CLIENT.rst index 6b82222..e7603a3 100644 --- a/docs/MQTT_CLIENT.rst +++ b/docs/MQTT_CLIENT.rst @@ -442,15 +442,21 @@ set_dhw_mode() await mqtt_client.set_dhw_mode(device: Device, mode_id: int) -> int -Set DHW (Domestic Hot Water) operation mode. +Set DHW (Domestic Hot Water) operation mode. This sets the ``dhwOperationSetting`` field, which determines what heating mode the device will use when it needs to heat water. **Command:** ``33554433`` **Mode:** ``dhw-mode`` -**Mode IDs:** - ``1``: Heat Pump (most efficient, longest recovery) - -``2``: Electric (least efficient, fastest recovery) - ``3``: Energy -Saver (default, balanced) - ``4``: High Demand (faster recovery) +**Mode IDs (command values):** + +* ``1``: Heat Pump Only (most efficient, longest recovery) +* ``2``: Electric Only (least efficient, fastest recovery) +* ``3``: Energy Saver (default, balanced - Hybrid: Efficiency) +* ``4``: High Demand (faster recovery - Hybrid: Boost) +* ``5``: Vacation (suspend heating for 0-99 days) + +**Important:** Setting the mode updates ``dhwOperationSetting`` but does not immediately change ``operationMode``. The ``operationMode`` field reflects the device's current operational state and changes automatically when the device starts/stops heating. See :doc:`DEVICE_STATUS_FIELDS` for details on the relationship between these fields. set_dhw_temperature() ''''''''''''''''''''' @@ -749,13 +755,16 @@ Response Message "dhwTemperature": 120, "tankUpperTemperature": 115, "tankLowerTemperature": 110, - "operationMode": 3, + "operationMode": 64, + "dhwOperationSetting": 3, "dhwUse": true, "compUse": false } } } +Note: ``operationMode`` shows the current operational state (64 = Energy Saver actively heating), while ``dhwOperationSetting`` shows the configured mode preference (3 = Energy Saver). See :doc:`DEVICE_STATUS_FIELDS` for the distinction between these fields. + Error Handling -------------- diff --git a/docs/MQTT_MESSAGES.rst b/docs/MQTT_MESSAGES.rst index f354cdc..ea1b7a4 100644 --- a/docs/MQTT_MESSAGES.rst +++ b/docs/MQTT_MESSAGES.rst @@ -133,7 +133,7 @@ The device sends a response to a control message on the ``responseTopic`` specif The ``sessionID`` in the response corresponds to the ``sessionID`` of the request. -The ``response`` object contains a ``status`` object that reflects the new state. For example, after a ``dhw-mode`` command with ``param`` ``[3]``\ , the ``dhwOperationSetting`` in the ``status`` object of the response will be ``3``. +The ``response`` object contains a ``status`` object that reflects the new state. For example, after a ``dhw-mode`` command with ``param`` ``[3]`` (Energy Saver), the ``dhwOperationSetting`` field in the ``status`` object will be ``3``. Note that ``operationMode`` may still show ``0`` (STANDBY) if the device is not currently heating. See :doc:`DEVICE_STATUS_FIELDS` for the important distinction between ``dhwOperationSetting`` (configured mode) and ``operationMode`` (current operational state). Device Status Messages ---------------------- diff --git a/examples/power_control_example.py b/examples/power_control_example.py new file mode 100644 index 0000000..4a0fdfd --- /dev/null +++ b/examples/power_control_example.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +""" +Example: Controlling device power via MQTT. + +This demonstrates how to programmatically turn the water heater on and off +and receive confirmation of the power state change. +""" + +import asyncio +import logging +from nwp500 import NavienAuthClient, NavienAPIClient, NavienMqttClient + +# Set up logging to see the power control process +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def power_control_example(): + """Example of controlling device power programmatically.""" + + # Use environment variables or replace with your credentials + email = "your_email@example.com" + password = "your_password" + + async with NavienAuthClient(email, password) as auth_client: + # Get device information + api_client = NavienAPIClient(auth_client) + devices = await api_client.list_devices() + + if not devices: + logger.error("No devices found") + return + + device = devices[0] + logger.info(f"Found device: {device.device_info.device_name}") + + # Connect MQTT client + mqtt_client = NavienMqttClient(auth_client) + await mqtt_client.connect() + logger.info("MQTT client connected") + + try: + # Get current status first + logger.info("Getting current device status...") + current_status = None + + def on_current_status(status): + nonlocal current_status + current_status = status + logger.info(f"Current operation mode: {status.operationMode.name}") + logger.info(f"Current DHW temperature: {status.dhwTemperature}°F") + + await mqtt_client.subscribe_device_status(device, on_current_status) + await mqtt_client.request_device_status(device) + await asyncio.sleep(3) # Wait for current status + + # Turn device off + logger.info("Turning device OFF...") + + power_off_complete = False + + def on_power_off_response(status): + nonlocal power_off_complete + logger.info("Power OFF response received!") + logger.info(f"Operation mode: {status.operationMode.name}") + logger.info(f"DHW Operation Setting: {status.dhwOperationSetting.name}") + power_off_complete = True + + await mqtt_client.subscribe_device_status(device, on_power_off_response) + await mqtt_client.set_power(device, power_on=False) + + # Wait for confirmation + for i in range(15): # Wait up to 15 seconds + if power_off_complete: + logger.info("Device turned OFF successfully!") + break + await asyncio.sleep(1) + else: + logger.warning("Timeout waiting for power OFF confirmation") + + # Wait a bit before turning back on + logger.info("Waiting 5 seconds before turning device back ON...") + await asyncio.sleep(5) + + # Turn device back on + logger.info("Turning device ON...") + + power_on_complete = False + + def on_power_on_response(status): + nonlocal power_on_complete + logger.info("Power ON response received!") + logger.info(f"Operation mode: {status.operationMode.name}") + logger.info(f"DHW Operation Setting: {status.dhwOperationSetting.name}") + logger.info(f"Tank charge: {status.dhwChargePer}%") + power_on_complete = True + + await mqtt_client.subscribe_device_status(device, on_power_on_response) + await mqtt_client.set_power(device, power_on=True) + + # Wait for confirmation + for i in range(15): # Wait up to 15 seconds + if power_on_complete: + logger.info("Device turned ON successfully!") + break + await asyncio.sleep(1) + else: + logger.warning("Timeout waiting for power ON confirmation") + + finally: + await mqtt_client.disconnect() + logger.info("Disconnected from MQTT") + + +if __name__ == "__main__": + print("=== Power Control Example ===") + print("This example demonstrates:") + print("1. Connecting to device via MQTT") + print("2. Getting current device status") + print("3. Turning device OFF") + print("4. Turning device ON") + print("5. Receiving and displaying the responses") + print() + + # Note: This requires valid credentials + print( + "Note: Update email/password or set NAVIEN_EMAIL/NAVIEN_PASSWORD environment variables" + ) + print() + + # Uncomment to run (requires valid credentials) + # asyncio.run(power_control_example()) + + print("CLI equivalent commands:") + print(" python -m nwp500.cli --power-off") + print(" python -m nwp500.cli --power-on") + print(" python -m nwp500.cli --power-on --status") diff --git a/src/nwp500/cli.py b/src/nwp500/cli.py index e9f4fba..9ff0159 100644 --- a/src/nwp500/cli.py +++ b/src/nwp500/cli.py @@ -373,6 +373,63 @@ def on_status_response(status): _logger.error(f"Error setting temperature: {e}") +async def handle_power_request(mqtt: NavienMqttClient, device: Device, power_on: bool): + """ + Set device power state and display the response. + + Args: + mqtt: MQTT client instance + device: Device to control + power_on: True to turn on, False to turn off + """ + action = "on" if power_on else "off" + _logger.info(f"Turning device {action}...") + + # Set up callback to capture status response after power change + future = asyncio.get_running_loop().create_future() + + def on_power_change_response(status: DeviceStatus): + if not future.done(): + future.set_result(status) + + try: + # Subscribe to status updates + await mqtt.subscribe_device_status(device, on_power_change_response) + + # Send power command + await mqtt.set_power(device, power_on) + + # Wait for response with timeout + status = await asyncio.wait_for(future, timeout=10.0) + + _logger.info(f"Device turned {action} successfully!") + + # Display relevant status information + print( + json.dumps( + { + "result": "success", + "action": action, + "status": { + "operationMode": status.operationMode.name, + "dhwOperationSetting": status.dhwOperationSetting.name, + "dhwTemperature": f"{status.dhwTemperature}°F", + "dhwChargePer": f"{status.dhwChargePer}%", + "tankUpperTemperature": f"{status.tankUpperTemperature:.1f}°F", + "tankLowerTemperature": f"{status.tankLowerTemperature:.1f}°F", + }, + }, + indent=2, + ) + ) + + except asyncio.TimeoutError: + _logger.error(f"Timed out waiting for power {action} confirmation") + + except Exception as e: + _logger.error(f"Error turning device {action}: {e}") + + async def handle_monitoring(mqtt: NavienMqttClient, device: Device, output_file: str): """Start periodic monitoring and write status to CSV.""" _logger.info(f"Starting periodic monitoring. Writing updates to {output_file}") @@ -419,6 +476,20 @@ async def async_main(args: argparse.Namespace): await handle_device_info_request(mqtt, device) elif args.device_feature: await handle_device_feature_request(mqtt, device) + elif args.power_on: + await handle_power_request(mqtt, device, power_on=True) + # If --status was also specified, get status after power change + if args.status: + _logger.info("Getting updated status after power on...") + await asyncio.sleep(2) # Brief pause for device to process + await handle_status_request(mqtt, device) + elif args.power_off: + await handle_power_request(mqtt, device, power_on=False) + # If --status was also specified, get status after power change + if args.status: + _logger.info("Getting updated status after power off...") + await asyncio.sleep(2) # Brief pause for device to process + await handle_status_request(mqtt, device) elif args.set_mode: await handle_set_mode_request(mqtt, device, args.set_mode) # If --status was also specified, get status after setting mode @@ -505,6 +576,16 @@ def parse_args(args): help="Set DHW (Domestic Hot Water) target temperature in Fahrenheit " "(115-150°F) and display response.", ) + group.add_argument( + "--power-on", + action="store_true", + help="Turn the device on and display response.", + ) + group.add_argument( + "--power-off", + action="store_true", + help="Turn the device off and display response.", + ) group.add_argument( "--monitor", action="store_true", diff --git a/src/nwp500/models.py b/src/nwp500/models.py index 6565005..dddcdab 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -16,8 +16,9 @@ class OperationMode(Enum): """Enumeration for the operation modes of the device. - The first set of modes (0-5) are used when commanding the device, while - the second set (32, 64, 96) are observed in status messages. + The first set of modes (0-6) are used when commanding the device or appear + in dhwOperationSetting, while the second set (32, 64, 96) are observed in + the operationMode status field. Command mode IDs (based on MQTT protocol): - 0: Standby (device in idle state) @@ -26,6 +27,7 @@ class OperationMode(Enum): - 3: Energy Saver (balanced, good default) - 4: High Demand (maximum heating capacity) - 5: Vacation mode + - 6: Power Off (device is powered off - appears in dhwOperationSetting only) """ # Commanded modes @@ -35,8 +37,9 @@ class OperationMode(Enum): ENERGY_SAVER = 3 # Energy Saver HIGH_DEMAND = 4 # High Demand VACATION = 5 + POWER_OFF = 6 # Power Off (appears in dhwOperationSetting when device is off) - # Observed status modes + # Status modes (operationMode field only) HEAT_PUMP_MODE = 32 HYBRID_EFFICIENCY_MODE = 64 HYBRID_BOOST_MODE = 96 @@ -242,7 +245,7 @@ class DeviceStatus: antiLegionellaPeriod: int antiLegionellaOperationBusy: bool programReservationType: int - dhwOperationSetting: int + dhwOperationSetting: OperationMode # User's configured mode preference (command modes: 1-5) temperatureType: TemperatureUnit tempFormulaType: str errorBuzzerUse: bool @@ -401,6 +404,20 @@ def from_dict(cls, data: dict): ) # Default to a safe enum value so callers can rely on .name converted_data["operationMode"] = OperationMode.STANDBY + + if "dhwOperationSetting" in converted_data: + try: + converted_data["dhwOperationSetting"] = OperationMode( + converted_data["dhwOperationSetting"] + ) + except ValueError: + _logger.warning( + "Unknown dhwOperationSetting: %s. Defaulting to ENERGY_SAVER.", + converted_data["dhwOperationSetting"], + ) + # Default to ENERGY_SAVER as a safe default + converted_data["dhwOperationSetting"] = OperationMode.ENERGY_SAVER + if "temperatureType" in converted_data: try: converted_data["temperatureType"] = TemperatureUnit( From 4dfc60456d254602a0559362b88c82b83147d0d2 Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 11 Oct 2025 20:29:30 -0700 Subject: [PATCH 2/3] Update docs/DEVICE_STATUS_FIELDS.rst Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/DEVICE_STATUS_FIELDS.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/DEVICE_STATUS_FIELDS.rst b/docs/DEVICE_STATUS_FIELDS.rst index f8380ac..daf50be 100644 --- a/docs/DEVICE_STATUS_FIELDS.rst +++ b/docs/DEVICE_STATUS_FIELDS.rst @@ -614,7 +614,6 @@ Displaying Status in a User Interface For user-facing applications, follow these guidelines: -**Primary Mode Display** **Primary Mode Display** Use ``dhwOperationSetting`` to show the user's configured mode preference. This is what users expect to see as "the current mode" because it represents their selection. From fc5cd60c16cd44b3f2b2b392590fea879afe47bc Mon Sep 17 00:00:00 2001 From: Emmanuel Levijarvi Date: Sat, 11 Oct 2025 20:29:39 -0700 Subject: [PATCH 3/3] Update src/nwp500/models.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/nwp500/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nwp500/models.py b/src/nwp500/models.py index dddcdab..5011be8 100644 --- a/src/nwp500/models.py +++ b/src/nwp500/models.py @@ -245,7 +245,7 @@ class DeviceStatus: antiLegionellaPeriod: int antiLegionellaOperationBusy: bool programReservationType: int - dhwOperationSetting: OperationMode # User's configured mode preference (command modes: 1-5) + dhwOperationSetting: OperationMode # User's configured mode preference (command modes: 1-6) temperatureType: TemperatureUnit tempFormulaType: str errorBuzzerUse: bool