Skip to content
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

Added support to washer-dryers to override some of the settings defined when a course is selected. Partial implementation of feature request #596 #716

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
30 changes: 29 additions & 1 deletion custom_components/smartthinq_sensors/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,41 @@ class ThinQSelectEntityDescription(
WASH_DEV_SELECT: tuple[ThinQSelectEntityDescription, ...] = (
ThinQSelectEntityDescription(
key="course_selection",
name="Course selection",
# Changed the name so that course controls are grouped together on the UI. name="Course selection",
name="Set Course",
icon="mdi:tune-vertical-variant",
options_fn=lambda x: x.device.course_list,
select_option_fn=lambda x, option: x.device.select_start_course(option),
available_fn=lambda x: x.device.select_course_enabled,
value_fn=lambda x: x.device.selected_course,
),
ThinQSelectEntityDescription(
key="temp_selection",
name="Set Water Temp",
icon="mdi:tune-vertical-variant",
options_fn=lambda x: x.device.temps_list,
select_option_fn=lambda x, option: x.device.select_start_temp(option),
available_fn=lambda x: x.device.select_temp_enabled,
value_fn=lambda x: x.device.selected_temp,
),
ThinQSelectEntityDescription(
key="rinse_selection",
name="Set Rinse Option",
icon="mdi:tune-vertical-variant",
options_fn=lambda x: x.device.rinses_list,
select_option_fn=lambda x, option: x.device.select_start_rinse(option),
available_fn=lambda x: x.device.select_rinse_enabled,
value_fn=lambda x: x.device.selected_rinse,
),
ThinQSelectEntityDescription(
key="spin_selection",
name="Set Spin Speed",
icon="mdi:tune-vertical-variant",
options_fn=lambda x: x.device.spins_list,
select_option_fn=lambda x, option: x.device.select_start_spin(option),
available_fn=lambda x: x.device.select_spin_enabled,
value_fn=lambda x: x.device.selected_spin,
),
Copy link
Owner

Choose a reason for hiding this comment

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

As in my previous comment, I prefer to not use a select but just provide additional parameter to be used in the remote_start service.

Copy link
Author

Choose a reason for hiding this comment

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

Please see my comments above.

)
MICROWAVE_SELECT: tuple[ThinQSelectEntityDescription, ...] = (
ThinQSelectEntityDescription(
Expand Down
110 changes: 109 additions & 1 deletion custom_components/smartthinq_sensors/wideq/devices/washerDryer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""------------------for Washer and Dryer"""

from __future__ import annotations
#jl
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError

import base64
from copy import deepcopy
Expand Down Expand Up @@ -124,6 +126,13 @@ def __init__(
self._course_keys: dict[CourseType, str | None] | None = None
self._course_infos: dict[str, str] | None = None
self._selected_course: str | None = None
self._course_overrides: dict | None = {}
self._course_overrides_lists: dict | None = {
'temp': ['TEMP_COLD', 'TEMP_20', 'TEMP_30', 'TEMP_40', 'TEMP_60', 'TEMP_95'],
'spin': ['NO_SPIN', 'SPIN_400', 'SPIN_800', 'SPIN_1000', 'SPIN_Max'],
'rinse': ['RINSE_NORMAL', 'RINSE_PLUS']}
# Need to initialise this list so that the UI can setup the select drop down.
# A better solution would be to generate this information from the data returned ThinQ API.
lancasterJ marked this conversation as resolved.
Show resolved Hide resolved
self._is_cycle_finishing = False
self._stand_by = False
self._remote_start_status: dict | None = None
Expand Down Expand Up @@ -167,6 +176,36 @@ def selected_course(self) -> str:
"""Return current selected course."""
return self._selected_course or _CURRENT_COURSE

@property
def temps_list(self) -> list:
"""Return a list of available water temperatures for the selected course."""
return self._course_overrides_lists.get('temp')

@property
def selected_temp(self) -> str:
"""Return current selected water temperature."""
return self._course_overrides.get('temp')

@property
def rinses_list(self) -> list:
"""Return a list of available rinse options for the selected course."""
return self._course_overrides_lists.get('rinse')

@property
def selected_rinse(self) -> str:
"""Return current selected rinse option."""
return self._course_overrides.get('rinse')

@property
def spins_list(self) -> list:
"""Return a list of available spin speeds for the selected course."""
return self._course_overrides_lists.get('spin')

@property
def selected_spin(self) -> str:
"""Return current selected spin speed."""
return self._course_overrides.get('spin')

@property
def run_state(self) -> str:
"""Return calculated pre state."""
Expand Down Expand Up @@ -365,7 +404,6 @@ def _prepare_course_info(
s_course_key: str | None,
) -> dict:
"""Prepare the course info used to run the command."""

ret_data = deepcopy(data)

# Prepare the course data initializing option for infoV1 device
Expand Down Expand Up @@ -418,6 +456,11 @@ def _prepare_course_info(
continue
ret_data[ckey] = cdata

# If an override is defeined then apply it
if override_value := self._course_overrides.get(ckey):
ret_data[ckey] = override_value
_LOGGER.debug("_prepare_course_info, course data override: %s: %s", ckey, ret_data[ckey])

if not course_set:
ret_data[VT_CTRL_COURSE_INFO] = course_info

Expand Down Expand Up @@ -708,6 +751,71 @@ async def select_start_course(self, course_name: str) -> None:
raise ValueError(f"Invalid course: {course_name}")
self._selected_course = course_name

# For the selected course save the permitted values for water temperature
course_id = self._get_course_infos().get(self._selected_course)
n_course_key = self.get_course_key(CourseType.COURSE)
course_info = self._get_course_details(n_course_key, course_id)
if not course_info:
raise ValueError("Course info not available")
# _LOGGER.debug("select_start_course, course_info: %s", course_info)

self._course_overrides.clear()
self._course_overrides_lists.clear()
for func_key in course_info["function"]:
value = func_key.get("value")
default = func_key.get("default")
selectable = func_key.get("selectable")

if selectable is None:
continue

_LOGGER.debug("select_start_course(%s), set overrides for %s - default: %s, selectable: %s", course_name, value, default, selectable)
self._course_overrides[value] = default
self._course_overrides_lists[value] = selectable


def _select_enabled(self, select_name: str) -> bool:
"""Return if specified select is enabled."""
enabled = self.select_course_enabled and self._selected_course and self._course_overrides_lists.get(select_name)
if (not enabled) and (select_name in self._course_overrides):
del self._course_overrides[select_name]
return enabled

def _select_start_option(self, option: str, option_friendly_name: str, option_selected: str) -> None:
"""Select a secific option for remote start."""
permitted_options = self._course_overrides_lists.get(option)
if permitted_options and option_selected in permitted_options:
self._course_overrides[option] = option_selected
else:
raise ServiceValidationError(f"{option_selected} is invalid and will be ignored. {option_friendly_name} must be one of {permitted_options}")

@property
def select_temp_enabled(self) -> bool:
"""Return if select temp is enabled."""
return self._select_enabled('temp')

async def select_start_temp(self, temp_name: str) -> None:
"""Select a secific water temperature for remote start."""
self._select_start_option('temp', 'Water Temp', temp_name)

@property
def select_rinse_enabled(self) -> bool:
"""Return if select rinse is enabled."""
return self._select_enabled('rinse')

async def select_start_rinse(self, rinse_name: str) -> None:
"""Select a secific rinse option for remote start."""
self._select_start_option('rinse', 'Rinse Option', rinse_name)

@property
def select_spin_enabled(self) -> bool:
"""Return if select spin is enabled."""
return self._select_enabled('spin')

async def select_start_spin(self, spin_name: str) -> None:
"""Select a secific spin for remote start."""
self._select_start_option('spin', 'Spin Speed', spin_name)

Copy link
Owner

Choose a reason for hiding this comment

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

This will make all the logic very complex, because there are different combination that can vary depending on selected course. I would prefer to just add some parameter to the service call and allow to change this value calling the HA service with prober parameters, that will be ignored if not valid.

Copy link
Author

Choose a reason for hiding this comment

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

@ollo69 ,

This will make all the logic very complex, because there are different combination that can vary depending on selected course

You are correct the ability to override a temp, spin or rinse depends on the WM model and the course selected. The PR handles this by extracting the necessary data for course info and storing it in _course_overrides_lists. The code should not required updating for different or future VM models unless a new setting is introduced (e.g. amount washing liquid to use) and we want to provide the user with a GUI selector to set it. Background: In the GUI and API the temp, spin and rinse select elements are not enabled until a course is selected that permits them to be overridden. For example, if the course Cotton is selected temp, spin and rinse are enabled, while for the course Allergy Care only spin and rinse are enabled. For each enabled override, a list of allowed values is extracted from from the course description. When a user attempts to set a value it is checked against this list. If the value is not in the list it is ignored and the user is informed of the permitted values by raising ServiceValidationError.

I would prefer to just add some parameter to the service call and allow to change this value calling the HA service with prober parameters, that will be ignored if not valid

When I started this work you had not publish the change to remote_start that allowed a course name string to be passed as a parameter. I have since studied your change and learnt how an integration can define a service that can be used by an automation or script.

I feel it is very easy to add another parameter to remote_start that accepts a JSON dictionary of overrides. The method can check _course_overrides_lists to confirm if the setting (e.g. spin speed) can be overridden and that the value is correct. An example call would be remote_start('Eco 40-60', '{"temp": "TEMP_60", "spin": "SPIN_400"}')

If you are happy with the above approach I am happy to modify the PR. I feel it will be a simple change. If you are thinking of something different please let me know.

I feel there is one problem we should agree on. For course names users can discover the valid course names that can be passed to remote_start using the course selection entity on the integration GUI. There may be another method that I have not found. How will a user, for a given course, discover setting that can be overridden and their permitted values that can be passed to remote_start. The selectors for temp, spin and rinse in the integration GUI will provide this information to the user. If you don't wish to have these, how do you think we can expose the information in _course_overrides_lists to the user?

In summary:

  1. I like the idea of adding overrides to remote_start. It gives a cleaner HA script.
  2. I believe the code will handle all current and future course settings and WM models.
  3. Without a discovery mechanism for the overrides that is simple for the user I don't feel we have usable solution. I am therefore reluctant to remove the temp, spin and rinse selectors if we don't provide an alternative. Nevertheless, this is your call.

Regards
John

Copy link
Author

@lancasterJ lancasterJ Mar 15, 2024

Choose a reason for hiding this comment

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

@ollo69 I had all the logic in place to add overrides to remote_start so I did it and update the PR. Most of the needed code was to help the user understand what to do when the remote_start service call fails by displaying meaning full messages.

I have not removed the selectors yet. I am waiting your feed back, see above.

async def power_off(self):
"""Power off the device."""
keys = self._get_cmd_keys(CMD_POWER_OFF)
Expand Down
13 changes: 12 additions & 1 deletion info.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@ If during configuration you receive the message "No SmartThinQ devices found", p

**Important 2**: If you receive an "Invalid Credential" error during component configuration/startup, check in the LG mobile app if is requested to accept new Term Of Service.

**Note**: some device status may not be correctly detected, this depends on the model. I'm working to map all possible status developing the component in a way to allow to configure model option in the simplest possible way and provide update using Pull Requests. I will provide a guide on how update this information.
**Note**: some device status may not be correctly detected, this depend on the model. I'm working to map all possible status developing the component in a way to allow to configure model option in the simplest possible way and provide update using Pull Requests. I will provide a guide on how update this information.

**Washer-Dryer remote start**: The component provides entities to select a course and override some of the course settings, before remotely starting the machine. The overrides available and their permitted values depend on the selected course. Attempts to set an invalid value for an override are ignored and result in an error message pop-up on the lovelace UI. To remotely start the washer perform the following steps in order:

- Turn on the washer and enable remote start using its front panel. This is an LG safety feature that is also required for the LG app.
- Select a course.
- Optionally, select a value for the course setting (e.g. water temperature) you would like to override.
- "Press" the Washer Remote Start button.

Nothing will happen/change on the washer and the component sensor entities will not show your selected course or overrides, until you press Remote Start. This is the same behaviour as the LG app.

Please note, remote start feature override was developed for use in scripts and automations. If you use the locelace UI and select an invalid override value, it will incorrectly be shown as selected. In fact, it has been ignored and you must refresh the page to see the currently selected value. Pull requests that fix this issue are welcome.

## Component configuration

Expand Down