Skip to content

Split washer and dryer class #96

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
import aiohttp

from cli_ac_menu import show_aircon_menu
from cli_dryer_menu import show_dryer_menu
from cli_oven_menu import show_oven_menu
from cli_refrigerator_menu import show_refrigerator_menu
from cli_washerdryer_menu import show_washerdryer_menu
from cli_washer_menu import show_washer_menu
from whirlpool.appliancesmanager import AppliancesManager
from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector, Brand, Region
Expand Down Expand Up @@ -69,8 +70,11 @@ async def start():
if appliance_manager.aircons:
print("\n".join(map(str, appliance_manager.aircons)))

if appliance_manager.washer_dryers:
print("\n".join(map(str, appliance_manager.washer_dryers)))
if appliance_manager.dryers:
print("\n".join(map(str, appliance_manager.dryers)))

if appliance_manager.washers:
print("\n".join(map(str, appliance_manager.washers)))

if appliance_manager.ovens:
print("\n".join(map(str, appliance_manager.ovens)))
Expand Down Expand Up @@ -99,9 +103,14 @@ async def __aexit__(self, *args) -> None:
await show_aircon_menu(ac_data)
return

for wd_data in appliance_manager.washer_dryers:
if wd_data.said == args.said:
await show_washerdryer_menu(wd_data)
for dr_data in appliance_manager.dryers:
if dr_data.said == args.said:
await show_dryer_menu(dr_data)
return

for wr_data in appliance_manager.washers:
if wr_data.said == args.said:
await show_washer_menu(wr_data)
return

for mo_data in appliance_manager.ovens:
Expand Down
78 changes: 78 additions & 0 deletions cli_dryer_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import json

import aioconsole

from whirlpool.dryer import Dryer


async def show_dryer_menu(dr: Dryer) -> None:
def print_menu():
print("\n")
print(30 * "-", "MENU", 30 * "-")
print("u. Update status from server")
print("p. Print status")
print("v. Print raw status")
print("c. Custom command")
print("q. Exit")
print(67 * "-")

def print_status(dr: Dryer):
print(f"online: {dr.get_online()}")
print(f"state: {dr.get_machine_state()}")
print(f"door open: {dr.get_door_open()}")
print(f"est time remaining: {dr.get_est_time_remaining()}")
print(f"extra power changeable: {dr.get_status_extra_power_changeable()}")
print(f"steam changeable: {dr.get_status_extra_steam_changeable()}")
print(f"cycle select: {dr.get_status_cycle_select()}")
print(f"dryness: {dr.get_status_dryness()}")
print(f"manual dry time: {dr.get_status_manual_dry_time()}")
print(f"static guard: {dr.get_status_static_guard()}")
print(f"temperature: {dr.get_status_temperature()}")
print(f"wrinkle shield: {dr.get_status_wrinkle_shield()}")
print(f"airflow status: {dr.get_airflow_status()}")
print(f"cool down: {dr.get_cool_down()}")
print(f"damp: {dr.get_damp()}")
print(f"drying: {dr.get_drying()}")
print(f"limited cycle: {dr.get_limited_cycle()}")
print(f"sensing: {dr.get_sensing()}")
print(f"static reduce: {dr.get_static_reduce()}")
print(f"steaming: {dr.get_steaming()}")
print(f"wet: {dr.get_wet()}")
print(f"cycle count: {dr.get_cycle_count()}")
print(f"running hours: {dr.get_running_hours()}")
print(f"total hours: {dr.get_total_hours()}")
print(f"isp check: {dr.get_isp_check()}")
print(f"rssi antenna diversity: {dr.get_rssi_antenna_diversity()}")

print(f"set dryness: {dr.get_dryness()}")
print(f"set manual dry time: {dr.get_manual_dry_time()}")
print(f"set cycle select: {dr.get_cycle_select()}")
print(f"set temperature: {dr.get_temperature()}")
print(f"set wrinkle shield: {dr.get_wrinkle_shield()}")

def attr_upd():
print("Attributes updated")

dr.register_attr_callback(attr_upd)

loop = True
while loop:
print_menu()
choice = await aioconsole.ainput("Enter your choice: ")

if choice == "p":
print_status(dr)
elif choice == "u":
await dr.fetch_data()
print_status(dr)
elif choice == "v":
print(json.dumps(dr._data_dict, indent=4))
elif choice == "c":
cmd = await aioconsole.ainput("Command: ")
val = await aioconsole.ainput("Value: ")
await dr.send_attributes({cmd: val})
elif choice == "q":
print("Bye")
loop = False
else:
print("Wrong option selection. Enter any key to try again..")
34 changes: 17 additions & 17 deletions cli_washerdryer_menu.py → cli_washer_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import aioconsole

from whirlpool.washerdryer import WasherDryer
from whirlpool.washer import Washer


async def show_washerdryer_menu(wd: WasherDryer) -> None:
async def show_washer_menu(wr: Washer) -> None:
def print_menu():
print("\n")
print(30 * "-", "MENU", 30 * "-")
Expand All @@ -16,37 +16,37 @@ def print_menu():
print("q. Exit")
print(67 * "-")

def print_status(wd: WasherDryer):
print("online: " + str(wd.get_online()))
print("state: " + str(wd.get_machine_state()))
print("sensing: " + str(wd.get_cycle_status_sensing()))
print("filling: " + str(wd.get_cycle_status_filling()))
print("soaking: " + str(wd.get_cycle_status_soaking()))
print("washing: " + str(wd.get_cycle_status_washing()))
print("rinsing: " + str(wd.get_cycle_status_rinsing()))
print("spinning: " + str(wd.get_cycle_status_spinning()))
def print_status(wr: Washer):
print("online: " + str(wr.get_online()))
print("state: " + str(wr.get_machine_state()))
print("sensing: " + str(wr.get_sensing()))
print("filling: " + str(wr.get_filling()))
print("soaking: " + str(wr.get_soaking()))
print("washing: " + str(wr.get_washing()))
print("rinsing: " + str(wr.get_rinsing()))
print("spinning: " + str(wr.get_spinning()))

def attr_upd():
print("Attributes updated")

wd.register_attr_callback(attr_upd)
wr.register_attr_callback(attr_upd)

loop = True
while loop:
print_menu()
choice = await aioconsole.ainput("Enter your choice: ")

if choice == "p":
print_status(wd)
print_status(wr)
elif choice == "u":
await wd.fetch_data()
print_status(wd)
await wr.fetch_data()
print_status(wr)
elif choice == "v":
print(json.dumps(wd._data_dict, indent=4))
print(json.dumps(wr._data_dict, indent=4))
elif choice == "c":
cmd = await aioconsole.ainput("Command: ")
val = await aioconsole.ainput("Value: ")
await wd.send_attributes({cmd: val})
await wr.send_attributes({cmd: val})
elif choice == "q":
print("Bye")
loop = False
Expand Down
33 changes: 33 additions & 0 deletions tests/data/owned_appliances.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,39 @@
"CATEGORY_NAME": "Climate",
"MODEL_NO": "CoolMyBeer",
"SERIAL": "FR12345678"
},
{
"DATA_MODEL_KEY": "DDM_LAUNDRY_VMAX20_MAYTAG_DRYER_6_V1",
"CATEGORY_NAME": "FabricCare",
"MODEL_NO": "MGD6230HW3",
"REPLENISHMENT_DEVICE_MODEL": null,
"IMAGE_PATH": null,
"APPLIANCE_ID": 999,
"APPLIANCE_MASTER_ID": 3703,
"MODEL_SKU_ID": null,
"CREATED_AT": 1693943005000,
"UPDATED_AT": 1693943005000,
"APPLIANCE_NAME": "Dryer",
"SAID": "SAIDDRYER1",
"NEST_AWAY": 0,
"CYCLE_HANDOFF": 0,
"NEST_THERMOSTAT_ID": 0,
"THERMOSTAT_INFLUENCE_THRESHOLD": null,
"THERMOSTAT_DESIRED_OFFSET": null,
"THERMOSTAT_OFFSET_NEEDED": null,
"DELETE_FLAG": 0,
"DISPLAY_POSITION": null,
"SERIAL": "MC1234567",
"LOCATION_ID": 999,
"MACHINE_ID": null,
"MACHINE_POSITION": 0,
"ISVOICEDEFAULT": 1,
"DEVICE_ID": "9d83427a-b994-4859-a2b9-2472c5dcbc26",
"IS_ENROLLED": null,
"STATUS": "CLAIMED",
"APPLIANCE_TYPE_ID": null,
"MACHINE_STATUS": null,
"APPLIANCE_MODE": 2
}
]
}
94 changes: 94 additions & 0 deletions tests/test_dryer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from collections.abc import Callable
from typing import Any

import pytest
from aioresponses import aioresponses
from yarl import URL

from whirlpool.appliancesmanager import AppliancesManager
from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector
from whirlpool.dryer import (
CycleSelect,
Dryness,
MachineState,
Temperature,
WrinkleShield,
)


async def test_attributes(appliances_manager: AppliancesManager):
dryer = appliances_manager.dryers[0]
assert dryer.get_machine_state() == MachineState.Standby
assert dryer.get_door_open() is False
assert dryer.get_est_time_remaining() == 1800
assert dryer.get_drum_light_on() == 0
assert dryer.get_status_extra_steam_changeable() == 1
assert dryer.get_status_cycle_select() == 0
assert dryer.get_status_dryness() == 1
assert dryer.get_status_manual_dry_time() == 1
assert dryer.get_status_temperature() == 1
assert dryer.get_status_wrinkle_shield() == 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets remove the status_ from these, since it does not seem to add much context. maybe add _enabled as a suffix? at least for the ones that are setting toggles

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really sure about these. It "seems" that they are telling the caller whether that specific attribute can be changed or not. Maybe they should be more like:

assert dryer.get_extra_steam_changeable() == 1
assert dryer.get_cycle_select_changeable() == 0
assert dryer.get_dryness_changeable() == 1
assert dryer.get_manual_dry_time_changeable() == 1
assert dryer.get_temperature_changeable() == 1
assert dryer.get_wrinkle_shield_changeable() == 1

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah! they inform the app if it should allow the user to change those settings trough the app?
if so, lets rename it like you suggest then.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these where not updated.

btw, if I got it right, these tell if the user can change the respective settings on the app?
so, if get_status_wrinkle_shield() == False, then ATTR_WRINKLE_SHIELD is read only?

assert dryer.get_dryness() == Dryness.High
assert dryer.get_manual_dry_time() == 1800
assert dryer.get_cycle_select() == CycleSelect.Timed_Dry
assert dryer.get_airflow_status() == 0
assert dryer.get_cool_down() == 0
assert dryer.get_damp() == 0
assert dryer.get_drying() == 0
assert dryer.get_limited_cycle() == 0
assert dryer.get_sensing() == 0
assert dryer.get_static_reduce() == 0
assert dryer.get_steaming() == 0
assert dryer.get_wet() == 0
assert dryer.get_cycle_count() == 195
assert dryer.get_running_hours() == 148
assert dryer.get_total_hours() == 6302
assert dryer.get_rssi_antenna_diversity() == -51
assert dryer.get_damp_notification_tone_volume() == 0
assert dryer.get_alert_tone_volume() == 0
assert dryer.get_temperature() == Temperature.Cool
assert dryer.get_wrinkle_shield() == WrinkleShield.Off


@pytest.mark.parametrize(
["method", "argument", "expected_json"],
[
],
)
async def test_setters(
appliances_manager: AppliancesManager,
auth: Auth,
backend_selector: BackendSelector,
aioresponses_mock: aioresponses,
method: Callable,
argument: Any,
expected_json: dict,
):
dryer = appliances_manager.dryers[0]

expected_payload = {
"json": {
"body": expected_json,
"header": {"said": dryer.said, "command": "setAttributes"},
}
}

post_request_call_kwargs = {
"url": backend_selector.appliance_command_url,
"method": "POST",
"data": None,
"json": expected_payload["json"],
"allow_redirects": True,
"headers": auth.create_headers(),
}

url = backend_selector.appliance_command_url

# add call, call method
aioresponses_mock.post(url, payload=expected_payload)
await method(dryer, argument)

# assert args and length
aioresponses_mock.assert_called_with(**post_request_call_kwargs)
assert len(aioresponses_mock.requests[("POST", URL(url))]) == 1
68 changes: 68 additions & 0 deletions tests/test_washer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
from collections.abc import Callable
from typing import Any

import pytest
from aioresponses import aioresponses
from yarl import URL

from whirlpool.appliancesmanager import AppliancesManager
from whirlpool.auth import Auth
from whirlpool.backendselector import BackendSelector
from whirlpool.washer import MachineState


async def test_attributes(appliances_manager: AppliancesManager):
washer = appliances_manager.washers[0]

assert washer.get_machine_state() == MachineState.Standby
assert washer.get_sensing() is False
assert washer.get_filling() is False
assert washer.get_soaking() is False
assert washer.get_washing() is False
assert washer.get_rinsing() is False
assert washer.get_spinning() is False
assert washer.get_dispense_1_level() == 4
assert washer.get_door_open() is True
assert washer.get_time_remaining() == 4080


@pytest.mark.parametrize(
["method", "argument", "expected_json"],
[
],
)
async def test_setters(
appliances_manager: AppliancesManager,
auth: Auth,
backend_selector: BackendSelector,
aioresponses_mock: aioresponses,
method: Callable,
argument: Any,
expected_json: dict,
):
washer = appliances_manager.washers[0]
expected_payload = {
"json": {
"body": expected_json,
"header": {"said": washer.said, "command": "setAttributes"},
}
}

post_request_call_kwargs = {
"url": backend_selector.appliance_command_url,
"method": "POST",
"data": None,
"json": expected_payload["json"],
"allow_redirects": True,
"headers": auth.create_headers(),
}

url = backend_selector.appliance_command_url

# add call, call method
aioresponses_mock.post(url, payload=expected_payload)
await method(washer, argument)

# assert args and length
aioresponses_mock.assert_called_with(**post_request_call_kwargs)
assert len(aioresponses_mock.requests[("POST", URL(url))]) == 1
Loading
Loading