From 4c5ff9227a8e39cea6315fc5687eafc11b45935d Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Wed, 6 Mar 2024 19:46:19 +0000 Subject: [PATCH 01/12] Devlopment and testing of course override feature complete --- .../smartthinq_sensors/select.py | 30 ++- .../smartthinq_sensors/wideq/core_async.py | 2 + .../wideq/devices/washerDryer.py | 183 +++++++++++++++++- 3 files changed, 212 insertions(+), 3 deletions(-) diff --git a/custom_components/smartthinq_sensors/select.py b/custom_components/smartthinq_sensors/select.py index 068056b4..4cdf41f0 100644 --- a/custom_components/smartthinq_sensors/select.py +++ b/custom_components/smartthinq_sensors/select.py @@ -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, + ), ) MICROWAVE_SELECT: tuple[ThinQSelectEntityDescription, ...] = ( ThinQSelectEntityDescription( diff --git a/custom_components/smartthinq_sensors/wideq/core_async.py b/custom_components/smartthinq_sensors/wideq/core_async.py index 4b684b31..a3416556 100644 --- a/custom_components/smartthinq_sensors/wideq/core_async.py +++ b/custom_components/smartthinq_sensors/wideq/core_async.py @@ -1705,6 +1705,8 @@ async def model_url_info(self, url, device=None): if not (model_url_info := await self._load_json_info(url)): return None self._model_url_info[url] = model_url_info + #jl + # _LOGGER.debug("model_url_info, self._model_url_info[url], %s", self._model_url_info[url]) return self._model_url_info[url] def dump(self) -> dict[str, Any]: diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index bc7f37a9..a586b36f 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -124,6 +124,11 @@ 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']} self._is_cycle_finishing = False self._stand_by = False self._remote_start_status: dict | None = None @@ -159,7 +164,11 @@ def subkey_device(self) -> Device | None: @cached_property def course_list(self) -> list: """Return a list of available course.""" + #jl + _LOGGER.debug("course_list()") course_infos = self._get_course_infos() + #jl + _LOGGER.debug("course_list->: %s",[_CURRENT_COURSE, *course_infos.keys()]) return [_CURRENT_COURSE, *course_infos.keys()] @property @@ -167,6 +176,48 @@ 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.""" + #jl + _LOGGER.debug("temps_list->: %s", self._course_overrides_lists.get('temp')) + return self._course_overrides_lists.get('temp') + + @property + def selected_temp(self) -> str: + """Return current selected water temperature.""" + #jl + _LOGGER.debug("selected_temp->: %s", self._course_overrides.get('temp')) + return self._course_overrides.get('temp') + + @property + def rinses_list(self) -> list: + """Return a list of available rinse options for the selected course.""" + #jl + _LOGGER.debug("rinses_list->: %s", self._course_overrides_lists.get('rinse')) + return self._course_overrides_lists.get('rinse') + + @property + def selected_rinse(self) -> str: + """Return current selected rinse option.""" + #jl + _LOGGER.debug("selected_rinse->: %s", self._course_overrides.get('rinse')) + return self._course_overrides.get('rinse') + + @property + def spins_list(self) -> list: + """Return a list of available spin speeds for the selected course.""" + #jl + _LOGGER.debug("spins_list->: %s", self._course_overrides_lists.get('spin')) + return self._course_overrides_lists.get('spin') + + @property + def selected_spin(self) -> str: + """Return current selected spin speed.""" + #jl + _LOGGER.debug("selected_spin->: %s", self._course_overrides.get('spin')) + return self._course_overrides.get('spin') + @property def run_state(self) -> str: """Return calculated pre state.""" @@ -278,6 +329,8 @@ def _getcmdkey(self, key: str | None) -> str | None: return f"{self._sub_key.capitalize()}{key}" def _update_status(self, key, value): + #jl + _LOGGER.debug("_update_status(key, value): %s:%s", key, value) if self._status and value: self._status.update_status(key, value) @@ -297,6 +350,8 @@ def _update_opt_bit(self, opt_name: str, opt_val: str, bit_name: str, bit_val: i return None def _get_course_key(self, course_type: CourseType) -> str | None: + #jl + # _LOGGER.debug("_get_course_key(course_type): %s", course_type) """Return the course key for specific device.""" if self.model_info.is_info_v2: course_type_keys = _COURSE_KEYS[course_type][1] @@ -314,6 +369,8 @@ def _get_course_key(self, course_type: CourseType) -> str | None: return None def get_course_key(self, course_type: CourseType) -> str | None: + #jl + # _LOGGER.debug("get_course_key(course_type): %s", course_type) """Return the course key for specific device.""" if self._course_keys is None: if not self.model_info: @@ -322,6 +379,8 @@ def get_course_key(self, course_type: CourseType) -> str | None: return self._course_keys[course_type] def _get_course_infos(self) -> dict: + #jl + # _LOGGER.debug("_get_course_infos()") """Return a dict with available courses.""" if self._course_infos is not None: return self._course_infos @@ -347,10 +406,15 @@ def _get_course_infos(self) -> dict: return ret_val def _get_course_details(self, course_key, course_id): + #jl + # _LOGGER.debug("_get_course_details(course_key): %s", course_key) + # _LOGGER.debug("_get_course_details(course_id): %s", course_id) """Get definition for a specific course ID.""" if course_key is None: return None if courses := self.model_info.reference_values(course_key): + #jl + # _LOGGER.debug("_get_course_details->: %s", courses.get(course_id)) return courses.get(course_id) return None @@ -365,7 +429,7 @@ 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 @@ -399,12 +463,18 @@ def _prepare_course_info( ret_data.pop(op_course_key, None) for func_key in course_info["function"]: + #jl + # _LOGGER.debug("_prepare_course_info,func_key): %s", func_key) ckey = func_key.get("value") cdata = func_key.get("default") if not ckey or cdata is None: continue opt_set = False + #jl + # _LOGGER.debug("_prepare_course_info,option_keys): %s", option_keys) for opt_name in option_keys: + #jl + _LOGGER.debug("_prepare_course_info,opt_name): %s", opt_name) if opt_name not in ret_data: continue opt_val = ret_data[opt_name] @@ -418,6 +488,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 @@ -429,6 +504,8 @@ def _update_course_info(self) -> dict: Save information in the data payload for a specific course or default course if not already available. """ + #jl + _LOGGER.debug("_update_course_info()") data = None if self._initial_bit_start: data = self._remote_start_status @@ -490,6 +567,8 @@ def _update_course_info(self) -> dict: def _prepare_vtctrl_course_info(self) -> list: """Prepare course info for vtctrl command.""" + #jl + _LOGGER.debug("_prepare_vtctrl_course_info") vt_cmd_data = [] course_data = self._update_course_info() if course_info := course_data.get(VT_CTRL_COURSE_INFO): @@ -502,11 +581,14 @@ def _prepare_vtctrl_course_info(self) -> list: vt_cmd_data.append( {"cmd": ckey, "type": "ABSOLUTE", "value": str(cdata)} ) - + #jl + _LOGGER.debug("_prepare_vtctrl_course_info->: %s", vt_cmd_data) return vt_cmd_data def _prepare_command_v1(self, cmd, key): """Prepare command for specific ThinQ1 device.""" + #jl + _LOGGER.debug("_prepare_command_v1(cmd): %s", cmd) encode = cmd.pop("encode", False) str_data = "" @@ -538,8 +620,13 @@ def _prepare_command_v1(self, cmd, key): def _prepare_command_v2(self, cmd, key: str): """Prepare command for specific ThinQ2 device.""" + #jl + _LOGGER.debug("_prepare_command_v2(cmd): %s", cmd) + _LOGGER.debug("_prepare_command_v2(key): %s", key) data_set = cmd.pop("data", None) if not data_set: + #jl + _LOGGER.debug("_prepare_command_v2.1->: %s", cmd) return cmd res_data_set = None @@ -580,7 +667,13 @@ def _prepare_command_v2(self, cmd, key: str): else: cmd_data_set[cmd_key] = status_data.get(cmd_key, cmd_value) res_data_set = {WM_ROOT_DATA: cmd_data_set} + #jl + _LOGGER.debug("_prepare_command_v2.2, cmd: %s", cmd) + #jl + _LOGGER.debug("_prepare_command_v2.3->, cmd: %s", cmd) + _LOGGER.debug("_prepare_command_v2.3->, res_data_set: %s", res_data_set) + _LOGGER.debug("_prepare_command_v2.3->, data_set: %s", data_set) return { **cmd, "dataKey": None, @@ -591,6 +684,9 @@ def _prepare_command_v2(self, cmd, key: str): def _prepare_command_vtctrl(self, cmd: dict, command: str): """Prepare vtCtrl command for specific ThinQ2 device.""" + #jl + _LOGGER.debug("_prepare_command_vtctrl(command): %s", command) + _LOGGER.debug("_prepare_command_vtctrl(cmd): %s", cmd) data_set: dict = cmd.pop("data", None) if not data_set: return cmd @@ -627,6 +723,9 @@ def _prepare_command_vtctrl(self, cmd: dict, command: str): def _prepare_command(self, ctrl_key, command, key, value): """Prepare command for specific device.""" + #jl + _LOGGER.debug("_prepare_command(ctrl_key): %s", ctrl_key) + #_LOGGER.debug("_prepare_command, self.model_info: %s", self.model_info) cmd = None vt_ctrl = True if command in VT_CTRL_CMD: @@ -698,6 +797,9 @@ def select_course_enabled(self) -> bool: async def select_start_course(self, course_name: str) -> None: """Select a secific course for remote start.""" + #jl + _LOGGER.debug("select_start_course(course_name): %s", course_name) + if not self.select_course_enabled: raise InvalidDeviceStatus() @@ -708,6 +810,75 @@ 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, set overrides for %s - default: %s, selectable: %s", 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_name: str) -> None: + """Select a secific option for remote start.""" + #jl + _LOGGER.debug("_select_start_option(option, option_name): %s: %s", option, option_name) + # list = self._course_overrides_lists.get(option) + _LOGGER.debug("_select_start_option(list): %s", list) + # if option_name: + if list and option_name in list: ##self._course_overrides_lists[option]: + self._course_overrides[option] = option_name + #jl + # _LOGGER.debug("_select_start_option, self._course_overrides: %s", self._course_overrides) + + @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',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_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_name) + async def power_off(self): """Power off the device.""" keys = self._get_cmd_keys(CMD_POWER_OFF) @@ -727,13 +898,19 @@ async def wake_up(self): async def remote_start(self, course_name: str | None = None) -> None: """Remote start the device.""" + #jl + _LOGGER.debug("async remote_start") if not self.remote_start_enabled: raise InvalidDeviceStatus() if course_name and self._initial_bit_start: + #jl + _LOGGER.debug("async remote_start, await self.select_start_course(%s)", course_name) await self.select_start_course(course_name) keys = self._get_cmd_keys(CMD_REMOTE_START) + #jl + _LOGGER.debug("async remote_start, keys: %s", keys) await self.set(keys[0], keys[1], key=keys[2]) self._remote_start_pressed = True @@ -753,6 +930,8 @@ async def set( self, ctrl_key, command, *, key=None, value=None, data=None, ctrl_path=None ): """Set a device's control for `key` to `value`.""" + #jl + _LOGGER.debug("async washerDryer.set") await super().set( self._getcmdkey(ctrl_key), self._getcmdkey(command), From f526c66425c4877b07ddff08467591438a68511c Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Thu, 7 Mar 2024 10:00:40 +0000 Subject: [PATCH 02/12] removed debug logging --- .../smartthinq_sensors/wideq/core_async.py | 2 - .../wideq/devices/washerDryer.py | 92 +++---------------- 2 files changed, 13 insertions(+), 81 deletions(-) diff --git a/custom_components/smartthinq_sensors/wideq/core_async.py b/custom_components/smartthinq_sensors/wideq/core_async.py index a3416556..4b684b31 100644 --- a/custom_components/smartthinq_sensors/wideq/core_async.py +++ b/custom_components/smartthinq_sensors/wideq/core_async.py @@ -1705,8 +1705,6 @@ async def model_url_info(self, url, device=None): if not (model_url_info := await self._load_json_info(url)): return None self._model_url_info[url] = model_url_info - #jl - # _LOGGER.debug("model_url_info, self._model_url_info[url], %s", self._model_url_info[url]) return self._model_url_info[url] def dump(self) -> dict[str, Any]: diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index a586b36f..cc15b87b 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -164,11 +164,7 @@ def subkey_device(self) -> Device | None: @cached_property def course_list(self) -> list: """Return a list of available course.""" - #jl - _LOGGER.debug("course_list()") course_infos = self._get_course_infos() - #jl - _LOGGER.debug("course_list->: %s",[_CURRENT_COURSE, *course_infos.keys()]) return [_CURRENT_COURSE, *course_infos.keys()] @property @@ -179,43 +175,31 @@ def selected_course(self) -> str: @property def temps_list(self) -> list: """Return a list of available water temperatures for the selected course.""" - #jl - _LOGGER.debug("temps_list->: %s", self._course_overrides_lists.get('temp')) return self._course_overrides_lists.get('temp') @property def selected_temp(self) -> str: """Return current selected water temperature.""" - #jl - _LOGGER.debug("selected_temp->: %s", self._course_overrides.get('temp')) return self._course_overrides.get('temp') @property def rinses_list(self) -> list: """Return a list of available rinse options for the selected course.""" - #jl - _LOGGER.debug("rinses_list->: %s", self._course_overrides_lists.get('rinse')) return self._course_overrides_lists.get('rinse') @property def selected_rinse(self) -> str: """Return current selected rinse option.""" - #jl - _LOGGER.debug("selected_rinse->: %s", self._course_overrides.get('rinse')) return self._course_overrides.get('rinse') @property def spins_list(self) -> list: """Return a list of available spin speeds for the selected course.""" - #jl - _LOGGER.debug("spins_list->: %s", self._course_overrides_lists.get('spin')) return self._course_overrides_lists.get('spin') @property def selected_spin(self) -> str: """Return current selected spin speed.""" - #jl - _LOGGER.debug("selected_spin->: %s", self._course_overrides.get('spin')) return self._course_overrides.get('spin') @property @@ -329,8 +313,6 @@ def _getcmdkey(self, key: str | None) -> str | None: return f"{self._sub_key.capitalize()}{key}" def _update_status(self, key, value): - #jl - _LOGGER.debug("_update_status(key, value): %s:%s", key, value) if self._status and value: self._status.update_status(key, value) @@ -350,8 +332,6 @@ def _update_opt_bit(self, opt_name: str, opt_val: str, bit_name: str, bit_val: i return None def _get_course_key(self, course_type: CourseType) -> str | None: - #jl - # _LOGGER.debug("_get_course_key(course_type): %s", course_type) """Return the course key for specific device.""" if self.model_info.is_info_v2: course_type_keys = _COURSE_KEYS[course_type][1] @@ -369,8 +349,6 @@ def _get_course_key(self, course_type: CourseType) -> str | None: return None def get_course_key(self, course_type: CourseType) -> str | None: - #jl - # _LOGGER.debug("get_course_key(course_type): %s", course_type) """Return the course key for specific device.""" if self._course_keys is None: if not self.model_info: @@ -379,8 +357,6 @@ def get_course_key(self, course_type: CourseType) -> str | None: return self._course_keys[course_type] def _get_course_infos(self) -> dict: - #jl - # _LOGGER.debug("_get_course_infos()") """Return a dict with available courses.""" if self._course_infos is not None: return self._course_infos @@ -406,15 +382,10 @@ def _get_course_infos(self) -> dict: return ret_val def _get_course_details(self, course_key, course_id): - #jl - # _LOGGER.debug("_get_course_details(course_key): %s", course_key) - # _LOGGER.debug("_get_course_details(course_id): %s", course_id) """Get definition for a specific course ID.""" if course_key is None: return None if courses := self.model_info.reference_values(course_key): - #jl - # _LOGGER.debug("_get_course_details->: %s", courses.get(course_id)) return courses.get(course_id) return None @@ -463,18 +434,12 @@ def _prepare_course_info( ret_data.pop(op_course_key, None) for func_key in course_info["function"]: - #jl - # _LOGGER.debug("_prepare_course_info,func_key): %s", func_key) ckey = func_key.get("value") cdata = func_key.get("default") if not ckey or cdata is None: continue opt_set = False - #jl - # _LOGGER.debug("_prepare_course_info,option_keys): %s", option_keys) for opt_name in option_keys: - #jl - _LOGGER.debug("_prepare_course_info,opt_name): %s", opt_name) if opt_name not in ret_data: continue opt_val = ret_data[opt_name] @@ -504,8 +469,7 @@ def _update_course_info(self) -> dict: Save information in the data payload for a specific course or default course if not already available. """ - #jl - _LOGGER.debug("_update_course_info()") + data = None if self._initial_bit_start: data = self._remote_start_status @@ -567,8 +531,7 @@ def _update_course_info(self) -> dict: def _prepare_vtctrl_course_info(self) -> list: """Prepare course info for vtctrl command.""" - #jl - _LOGGER.debug("_prepare_vtctrl_course_info") + vt_cmd_data = [] course_data = self._update_course_info() if course_info := course_data.get(VT_CTRL_COURSE_INFO): @@ -581,14 +544,11 @@ def _prepare_vtctrl_course_info(self) -> list: vt_cmd_data.append( {"cmd": ckey, "type": "ABSOLUTE", "value": str(cdata)} ) - #jl - _LOGGER.debug("_prepare_vtctrl_course_info->: %s", vt_cmd_data) + return vt_cmd_data def _prepare_command_v1(self, cmd, key): """Prepare command for specific ThinQ1 device.""" - #jl - _LOGGER.debug("_prepare_command_v1(cmd): %s", cmd) encode = cmd.pop("encode", False) str_data = "" @@ -620,13 +580,8 @@ def _prepare_command_v1(self, cmd, key): def _prepare_command_v2(self, cmd, key: str): """Prepare command for specific ThinQ2 device.""" - #jl - _LOGGER.debug("_prepare_command_v2(cmd): %s", cmd) - _LOGGER.debug("_prepare_command_v2(key): %s", key) data_set = cmd.pop("data", None) if not data_set: - #jl - _LOGGER.debug("_prepare_command_v2.1->: %s", cmd) return cmd res_data_set = None @@ -667,13 +622,7 @@ def _prepare_command_v2(self, cmd, key: str): else: cmd_data_set[cmd_key] = status_data.get(cmd_key, cmd_value) res_data_set = {WM_ROOT_DATA: cmd_data_set} - #jl - _LOGGER.debug("_prepare_command_v2.2, cmd: %s", cmd) - #jl - _LOGGER.debug("_prepare_command_v2.3->, cmd: %s", cmd) - _LOGGER.debug("_prepare_command_v2.3->, res_data_set: %s", res_data_set) - _LOGGER.debug("_prepare_command_v2.3->, data_set: %s", data_set) return { **cmd, "dataKey": None, @@ -684,9 +633,6 @@ def _prepare_command_v2(self, cmd, key: str): def _prepare_command_vtctrl(self, cmd: dict, command: str): """Prepare vtCtrl command for specific ThinQ2 device.""" - #jl - _LOGGER.debug("_prepare_command_vtctrl(command): %s", command) - _LOGGER.debug("_prepare_command_vtctrl(cmd): %s", cmd) data_set: dict = cmd.pop("data", None) if not data_set: return cmd @@ -723,9 +669,6 @@ def _prepare_command_vtctrl(self, cmd: dict, command: str): def _prepare_command(self, ctrl_key, command, key, value): """Prepare command for specific device.""" - #jl - _LOGGER.debug("_prepare_command(ctrl_key): %s", ctrl_key) - #_LOGGER.debug("_prepare_command, self.model_info: %s", self.model_info) cmd = None vt_ctrl = True if command in VT_CTRL_CMD: @@ -797,8 +740,6 @@ def select_course_enabled(self) -> bool: async def select_start_course(self, course_name: str) -> None: """Select a secific course for remote start.""" - #jl - _LOGGER.debug("select_start_course(course_name): %s", course_name) if not self.select_course_enabled: raise InvalidDeviceStatus() @@ -828,7 +769,7 @@ async def select_start_course(self, course_name: str) -> None: if selectable is None: continue - _LOGGER.debug("select_start_course, set overrides for %s - default: %s, selectable: %s", value, default, selectable) + _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 @@ -842,15 +783,16 @@ def _select_enabled(self, select_name: str) -> bool: def _select_start_option(self, option: str, option_name: str) -> None: """Select a secific option for remote start.""" - #jl - _LOGGER.debug("_select_start_option(option, option_name): %s: %s", option, option_name) - # list = self._course_overrides_lists.get(option) - _LOGGER.debug("_select_start_option(list): %s", list) - # if option_name: - if list and option_name in list: ##self._course_overrides_lists[option]: + permitted_options = self._course_overrides_lists.get(option) + if permitted_options and option_name in permitted_options: self._course_overrides[option] = option_name - #jl - # _LOGGER.debug("_select_start_option, self._course_overrides: %s", self._course_overrides) + else: + # self._raise_error( + # "Invalid option", + # not_logged=True, + # exc=exc, + # ) + super().__init__("not a permitted option") @property def select_temp_enabled(self) -> bool: @@ -898,19 +840,13 @@ async def wake_up(self): async def remote_start(self, course_name: str | None = None) -> None: """Remote start the device.""" - #jl - _LOGGER.debug("async remote_start") if not self.remote_start_enabled: raise InvalidDeviceStatus() if course_name and self._initial_bit_start: - #jl - _LOGGER.debug("async remote_start, await self.select_start_course(%s)", course_name) await self.select_start_course(course_name) keys = self._get_cmd_keys(CMD_REMOTE_START) - #jl - _LOGGER.debug("async remote_start, keys: %s", keys) await self.set(keys[0], keys[1], key=keys[2]) self._remote_start_pressed = True @@ -930,8 +866,6 @@ async def set( self, ctrl_key, command, *, key=None, value=None, data=None, ctrl_path=None ): """Set a device's control for `key` to `value`.""" - #jl - _LOGGER.debug("async washerDryer.set") await super().set( self._getcmdkey(ctrl_key), self._getcmdkey(command), From 8d07178a383fd4f162b4a4489a6e31cb445bdb2b Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Thu, 7 Mar 2024 11:23:11 +0000 Subject: [PATCH 03/12] Added documentation and cleaned up code --- .../wideq/devices/washerDryer.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index cc15b87b..b9652c5d 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -129,6 +129,8 @@ def __init__( '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. self._is_cycle_finishing = False self._stand_by = False self._remote_start_status: dict | None = None @@ -400,7 +402,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 @@ -469,7 +470,6 @@ def _update_course_info(self) -> dict: Save information in the data payload for a specific course or default course if not already available. """ - data = None if self._initial_bit_start: data = self._remote_start_status @@ -531,7 +531,6 @@ def _update_course_info(self) -> dict: def _prepare_vtctrl_course_info(self) -> list: """Prepare course info for vtctrl command.""" - vt_cmd_data = [] course_data = self._update_course_info() if course_info := course_data.get(VT_CTRL_COURSE_INFO): @@ -740,7 +739,6 @@ def select_course_enabled(self) -> bool: async def select_start_course(self, course_name: str) -> None: """Select a secific course for remote start.""" - if not self.select_course_enabled: raise InvalidDeviceStatus() @@ -786,13 +784,7 @@ def _select_start_option(self, option: str, option_name: str) -> None: permitted_options = self._course_overrides_lists.get(option) if permitted_options and option_name in permitted_options: self._course_overrides[option] = option_name - else: - # self._raise_error( - # "Invalid option", - # not_logged=True, - # exc=exc, - # ) - super().__init__("not a permitted option") + # TO DO - display a pop up message if option_name is not valid. Did try self._raise_error, but the message poor. @property def select_temp_enabled(self) -> bool: From 05b8bac8f2ecbc852166e69767fd2d83a409a21e Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Thu, 7 Mar 2024 11:29:17 +0000 Subject: [PATCH 04/12] Added missing documentation --- info.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/info.md b/info.md index c5f75349..a6981f3d 100644 --- a/info.md +++ b/info.md @@ -30,6 +30,17 @@ If during configuration you receive the message "No SmartThinQ devices found", p **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. +**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 permitted values for each override depends on the selected course. Attempts to set an invalid value for an override are ignored. 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, the remote start feature overrides were developed for use in scripts and automations. If you are using the 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 that issue are welcome. + ## Component configuration Once the component has been installed, you need to configure it using the web interface in order to make it work. From 13059141c1c0e52ae333a0fea3b05e29b6e12119 Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Thu, 7 Mar 2024 16:08:00 +0000 Subject: [PATCH 05/12] Added error pop-up when an invalid override selection is made. --- .../wideq/devices/washerDryer.py | 17 ++++++++++------- info.md | 8 ++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index b9652c5d..079ad0fe 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -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 @@ -779,12 +781,13 @@ def _select_enabled(self, select_name: str) -> bool: del self._course_overrides[select_name] return enabled - def _select_start_option(self, option: str, option_name: str) -> None: + 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_name in permitted_options: - self._course_overrides[option] = option_name - # TO DO - display a pop up message if option_name is not valid. Did try self._raise_error, but the message poor. + 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: @@ -793,7 +796,7 @@ def select_temp_enabled(self) -> bool: async def select_start_temp(self, temp_name: str) -> None: """Select a secific water temperature for remote start.""" - self._select_start_option('temp',temp_name) + self._select_start_option('temp', 'Water Temp', temp_name) @property def select_rinse_enabled(self) -> bool: @@ -802,7 +805,7 @@ def select_rinse_enabled(self) -> bool: async def select_start_rinse(self, rinse_name: str) -> None: """Select a secific rinse option for remote start.""" - self._select_start_option('rinse',rinse_name) + self._select_start_option('rinse', 'Rinse Option', rinse_name) @property def select_spin_enabled(self) -> bool: @@ -811,7 +814,7 @@ def select_spin_enabled(self) -> bool: async def select_start_spin(self, spin_name: str) -> None: """Select a secific spin for remote start.""" - self._select_start_option('spin',spin_name) + self._select_start_option('spin', 'Spin Speed', spin_name) async def power_off(self): """Power off the device.""" diff --git a/info.md b/info.md index a6981f3d..fbc81529 100644 --- a/info.md +++ b/info.md @@ -28,18 +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 permitted values for each override depends on the selected course. Attempts to set an invalid value for an override are ignored. To remotely start the washer perform the following steps in order: +**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 +- 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, the remote start feature overrides were developed for use in scripts and automations. If you are using the 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 that issue are welcome. +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 From b59b000114b492a1d93918dd6b36b06171fdc701 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 Mar 2024 15:55:17 +0000 Subject: [PATCH 06/12] remote start service accepts overrides. Improved error messages. --- .../smartthinq_sensors/sensor.py | 13 +++++++--- .../smartthinq_sensors/services.yaml | 8 ++++++- .../wideq/devices/washerDryer.py | 24 +++++++++++++++---- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/custom_components/smartthinq_sensors/sensor.py b/custom_components/smartthinq_sensors/sensor.py index e52738be..ffbc5d67 100644 --- a/custom_components/smartthinq_sensors/sensor.py +++ b/custom_components/smartthinq_sensors/sensor.py @@ -584,7 +584,10 @@ def _async_discover_device(lge_devices: dict) -> None: platform = current_platform.get() platform.async_register_entity_service( SERVICE_REMOTE_START, - {vol.Optional("course"): str}, + { + vol.Optional("course"): str, + vol.Optional("overrides"): str + }, "async_remote_start", [SUPPORT_WM_SERVICES], ) @@ -696,11 +699,15 @@ def _get_sensor_state(self): return None - async def async_remote_start(self, course: str | None = None): + async def async_remote_start( + self, + course: str | None = None, + overrides: str | None = None + ): """Call the remote start command for WM devices.""" if self._api.type not in WM_DEVICE_TYPES: raise NotImplementedError() - await self._api.device.remote_start(course) + await self._api.device.remote_start(course, overrides) async def async_wake_up(self): """Call the wakeup command for WM devices.""" diff --git a/custom_components/smartthinq_sensors/services.yaml b/custom_components/smartthinq_sensors/services.yaml index f5c10224..40d94f3c 100644 --- a/custom_components/smartthinq_sensors/services.yaml +++ b/custom_components/smartthinq_sensors/services.yaml @@ -12,7 +12,13 @@ remote_start: required: false selector: text: - + overrides: + name: overrides + description: Overrides (JSON dictionary containing the setting to override and their new value) + required: false + selector: + text: + wake_up: name: WakeUp description: Send to ThinQ device the wakeup command. diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index 079ad0fe..7658c97f 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -748,7 +748,7 @@ async def select_start_course(self, course_name: str) -> None: self._selected_course = None return if course_name not in self.course_list: - raise ValueError(f"Invalid course: {course_name}") + raise ServiceValidationError(f"The course name '{course_name}' is invalid. Permitted names are: {self.course_list}.") self._selected_course = course_name # For the selected course save the permitted values for water temperature @@ -787,7 +787,7 @@ def _select_start_option(self, option: str, option_friendly_name: str, option_se 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}") + 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: @@ -833,14 +833,30 @@ async def wake_up(self): self._stand_by = False self._update_status(POWER_STATUS_KEY, self._state_power_on_init) - async def remote_start(self, course_name: str | None = None) -> None: + async def remote_start(self, course_name: str | None = None, overrides_json: str | None = None) -> None: + #jl + _LOGGER.debug("async def remote_start(%s, %s)", course_name, overrides_json) + """Remote start the device.""" if not self.remote_start_enabled: - raise InvalidDeviceStatus() + raise ServiceValidationError("Cannot remote start. Please wake machine or enable remote start at the machine.") if course_name and self._initial_bit_start: await self.select_start_course(course_name) + if overrides_json: + if not course_name: raise ServiceValidationError("Course name required if overrides are specified.") + try: overrides = json.loads(overrides_json) + except: raise ServiceValidationError("'overrides' contains invalid JSON.") + allowed_overrides = self._course_overrides_lists.keys() + if not overrides: raise ServiceValidationError(f"'overrides' must contain one key from {str(list(allowed_overrides))}.") + for key in overrides: + if key in allowed_overrides: + value = overrides.get(key) + self._select_start_option(key, key, value) + else: + raise ServiceValidationError(f"Course '{course_name}' does not allow the setting '{key}' to be overridden. Permitted overrides are: {str(list(allowed_overrides))}.") + keys = self._get_cmd_keys(CMD_REMOTE_START) await self.set(keys[0], keys[1], key=keys[2]) self._remote_start_pressed = True From cac87a0af026949c984bacd419ccdb3f3fc2bf0c Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 Mar 2024 16:05:38 +0000 Subject: [PATCH 07/12] Removed debug code --- .../smartthinq_sensors/wideq/devices/washerDryer.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index 7658c97f..41d7fb1c 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -1,7 +1,6 @@ """------------------for Washer and Dryer""" from __future__ import annotations -#jl from homeassistant.exceptions import HomeAssistantError, ServiceValidationError import base64 @@ -751,13 +750,12 @@ async def select_start_course(self, course_name: str) -> None: raise ServiceValidationError(f"The course name '{course_name}' is invalid. Permitted names are: {self.course_list}.") self._selected_course = course_name - # For the selected course save the permitted values for water temperature + # For the selected course save the permitted values for setting that can be overridden 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() @@ -834,9 +832,6 @@ async def wake_up(self): self._update_status(POWER_STATUS_KEY, self._state_power_on_init) async def remote_start(self, course_name: str | None = None, overrides_json: str | None = None) -> None: - #jl - _LOGGER.debug("async def remote_start(%s, %s)", course_name, overrides_json) - """Remote start the device.""" if not self.remote_start_enabled: raise ServiceValidationError("Cannot remote start. Please wake machine or enable remote start at the machine.") From 39db484cad7ce578bc38727df5ebb50ef23b5d5e Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Tue, 19 Mar 2024 10:43:55 +0000 Subject: [PATCH 08/12] Option overrides built from machine info. Friendly names from machine info language pack used. --- .../smartthinq_sensors/services.yaml | 2 +- .../wideq/devices/washerDryer.py | 183 ++++++++++++++---- 2 files changed, 143 insertions(+), 42 deletions(-) diff --git a/custom_components/smartthinq_sensors/services.yaml b/custom_components/smartthinq_sensors/services.yaml index 40d94f3c..31deafd8 100644 --- a/custom_components/smartthinq_sensors/services.yaml +++ b/custom_components/smartthinq_sensors/services.yaml @@ -14,7 +14,7 @@ remote_start: text: overrides: name: overrides - description: Overrides (JSON dictionary containing the setting to override and their new value) + description: Overrides (JSON dictionary containing the options to override and their new value) required: false selector: text: diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index 41d7fb1c..ad137214 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -6,6 +6,7 @@ import base64 from copy import deepcopy from enum import IntEnum +from collections import namedtuple import json import logging @@ -96,6 +97,8 @@ class CourseType(IntEnum): _COURSE_TYPE = "courseType" _CURRENT_COURSE = "Current course" +FriendlyName = namedtuple("FriendlyName", ["internal", "friendly"]) + class WMDevice(Device): """A higher-level interface for washer and dryer.""" @@ -125,20 +128,103 @@ 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. + # For the selected course, the options that can be overridden and the override value. + # Uses internal names. {'option internal name': value internal name, ...} + # Initialised by select_start_course and _select_start_option. read in selected_*, _select_start_option and _prepare_course_info + self._course_overrides: dict | None = {} + # For the selected course, the permitted values for each option that can be overridden. + # Uses internal names. {'option internal name': [value internal name, ...], ...} + # Initialised by select_start_course, _select_start_option. read in _select_option_enabled, _select_start_option and remote_start + self._course_overrides_lists: dict | None = {} + # For some options and their values, map the internal name to and from a friendly name. + # The friendly name is extracted from the machine info language pack + # {'option internal name': {'friendly_name': option friendly name, + # 'enum_values': {'value internal name': 'value friendly name', ...} + self._option_friendly_names: dict | None = {} self._is_cycle_finishing = False self._stand_by = False self._remote_start_status: dict | None = None self._remote_start_pressed = False self._power_on_available: bool = None self._initial_bit_start: bool = False + + def _build_friendly_enum_value(self, option: str): + """Add to friendly names dictionary the enum friendly values.""" + if not self.model_info.is_enum_type(option): + return + # Some emums (e.g. spin) are not incrementally indexed + friendly_enum_values = {} + for index in range(256): + if encoded_value := self.model_info.enum_index(option, index): + internal_value = self.model_info.enum_value(option, encoded_value) + friendly_value = self.get_enum_text(encoded_value) + friendly_enum_values[internal_value] = friendly_value + + friendly_enum = {} + friendly_enum['friendly_name'] = self.get_enum_text( + self.model_info._data_root(option).get('label', option)) + friendly_enum['enum_values'] = friendly_enum_values + self._option_friendly_names[option] = friendly_enum + + def _convert_option_name(self, option_str: str) -> FriendlyName | None: + """Convert an option name to its internal and friendly forms.""" + # If option matches a key, it is the internal name + internal_name = option_str + if friendly_name := self._option_friendly_names.get(internal_name): + return FriendlyName(internal_name, + friendly_name.get('friendly_name', internal_name)) + + # Search for a matching friendly name + friendly_name = option_str + for internal_name in self._option_friendly_names: + if self._option_friendly_names[internal_name].get('friendly_name','') == friendly_name: + return FriendlyName(internal_name, friendly_name) + + # option does not match an internal or friendly option name + return None + + def _convert_option_value(self, option_str: str, value_str: str) -> FriendlyName | None: + """Convert an option value to its internal and friendly forms.""" + if not(option := self._convert_option_name(option_str)): + return None + + if friendly_names := self._option_friendly_names.get(option.internal): + enum_values = friendly_names['enum_values'] + + #If value matches a key, it is the internal value + internal_value = value_str + if internal_value in enum_values: + return FriendlyName(internal_value, + enum_values.get(internal_value, '')) + + # Search for a matching friendly name + friendly_value = value_str + for key in enum_values: + if enum_values[key] == friendly_value: + return FriendlyName(key, friendly_value) + + # value does not match an internal or friendly option name + return None + + def _get_friendly_names_list(self, internal_names: list) -> list | None: + """Convert a list of internal enum names to list of friendly names.""" + friendly_names = [] + for internal_name in internal_names: + if option := self._convert_option_name(internal_name): + friendly_names.append(option.friendly) + return friendly_names + + def _get_friendly_values_list(self, option_str: str, internal_values: list) -> list | None: + """Convert a list of enum internal values to list of friendly values.""" + if not(option := self._convert_option_name(option_str)): + return None + friendly_values = [] + for value_str in internal_values: + if value := self._convert_option_value(option.internal, value_str): + friendly_values.append(value.friendly) + return friendly_values + @cached_property def _state_power_off(self): """Return native value for power off state.""" @@ -175,35 +261,41 @@ def selected_course(self) -> str: """Return current selected course.""" return self._selected_course or _CURRENT_COURSE - @property + @cached_property def temps_list(self) -> list: """Return a list of available water temperatures for the selected course.""" - return self._course_overrides_lists.get('temp') + self._build_friendly_enum_value('temp') + return list(self._option_friendly_names['temp']['enum_values'].values()) @property def selected_temp(self) -> str: """Return current selected water temperature.""" - return self._course_overrides.get('temp') + value = self._convert_option_value('temp', self._course_overrides.get('temp')) + return value.friendly - @property + @cached_property def rinses_list(self) -> list: """Return a list of available rinse options for the selected course.""" - return self._course_overrides_lists.get('rinse') + self._build_friendly_enum_value('rinse') + return list(self._option_friendly_names['rinse']['enum_values'].values()) @property def selected_rinse(self) -> str: """Return current selected rinse option.""" - return self._course_overrides.get('rinse') + value = self._convert_option_value('rinse', self._course_overrides.get('rinse')) + return value.friendly - @property + @cached_property def spins_list(self) -> list: """Return a list of available spin speeds for the selected course.""" - return self._course_overrides_lists.get('spin') + self._build_friendly_enum_value('spin') + return list(self._option_friendly_names['spin']['enum_values'].values()) @property def selected_spin(self) -> str: """Return current selected spin speed.""" - return self._course_overrides.get('spin') + value = self._convert_option_value('spin', self._course_overrides.get('spin')) + return value.friendly @property def run_state(self) -> str: @@ -747,7 +839,7 @@ async def select_start_course(self, course_name: str) -> None: self._selected_course = None return if course_name not in self.course_list: - raise ServiceValidationError(f"The course name '{course_name}' is invalid. Permitted names are: {self.course_list}.") + raise ServiceValidationError(f"The programme (course name) '{course_name}' is invalid. Permitted names are: {list(self.course_list)}.") self._selected_course = course_name # For the selected course save the permitted values for setting that can be overridden @@ -771,48 +863,55 @@ async def select_start_course(self, course_name: str) -> None: 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.""" + def _select_option_enabled(self, select_name: str) -> bool: + """Return if specified option 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 + def _select_start_option(self, option_str: str, value_str: str) -> None: + """ + Add option to the list of remote start overrides and set its value. + - option may be a friendly or internal name + - value may be a friendly or internal name + """ + option = self._convert_option_name(option_str) + value = self._convert_option_value(option.internal, value_str) + + # _course_overrides_lists, _course_overrides and permitted_options use internal names + permitted_options = self._course_overrides_lists.get(option.internal) + if value and permitted_options and (value.internal in permitted_options): + self._course_overrides[option.internal] = value.internal else: - raise ServiceValidationError(f"{option_selected} is invalid and will be ignored. {option_friendly_name} must be one of {permitted_options}.") + raise ServiceValidationError(f"The value '{value_str}' is invalid and has been ignored. The value for the option '{option.friendly}' must be one of: {self._get_friendly_values_list(option.internal, permitted_options)} or {permitted_options}.") @property def select_temp_enabled(self) -> bool: """Return if select temp is enabled.""" - return self._select_enabled('temp') + return self._select_option_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) + self._select_start_option('temp', temp_name) @property def select_rinse_enabled(self) -> bool: """Return if select rinse is enabled.""" - return self._select_enabled('rinse') + return self._select_option_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) + self._select_start_option('rinse', rinse_name) @property def select_spin_enabled(self) -> bool: """Return if select spin is enabled.""" - return self._select_enabled('spin') + return self._select_option_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) + self._select_start_option('spin', spin_name) async def power_off(self): """Power off the device.""" @@ -834,24 +933,26 @@ async def wake_up(self): async def remote_start(self, course_name: str | None = None, overrides_json: str | None = None) -> None: """Remote start the device.""" if not self.remote_start_enabled: - raise ServiceValidationError("Cannot remote start. Please wake machine or enable remote start at the machine.") + raise ServiceValidationError("Machine is off, asleep or running, or remote start is not enabled.") if course_name and self._initial_bit_start: await self.select_start_course(course_name) if overrides_json: - if not course_name: raise ServiceValidationError("Course name required if overrides are specified.") + if not course_name: raise ServiceValidationError("Programme (course) name required if overrides are specified.") try: overrides = json.loads(overrides_json) except: raise ServiceValidationError("'overrides' contains invalid JSON.") + allowed_overrides = self._course_overrides_lists.keys() - if not overrides: raise ServiceValidationError(f"'overrides' must contain one key from {str(list(allowed_overrides))}.") - for key in overrides: - if key in allowed_overrides: - value = overrides.get(key) - self._select_start_option(key, key, value) + if not overrides: raise ServiceValidationError(f"'overrides' must contain one option from {self._get_friendly_names_list(list(allowed_overrides))} or {list(allowed_overrides)}.") + for option_str in overrides: + option = self._convert_option_name(option_str) + if option and (option.internal in allowed_overrides): + value_str = overrides.get(option_str) + self._select_start_option(option.internal, value_str) else: - raise ServiceValidationError(f"Course '{course_name}' does not allow the setting '{key}' to be overridden. Permitted overrides are: {str(list(allowed_overrides))}.") - + raise ServiceValidationError(f"The programme '{course_name}' does not allow the option '{option_str}' to be overridden. Permitted options are: {self._get_friendly_names_list(list(allowed_overrides))} or {list(allowed_overrides)}.") + keys = self._get_cmd_keys(CMD_REMOTE_START) await self.set(keys[0], keys[1], key=keys[2]) self._remote_start_pressed = True From 52fc0f299e1e8c57e6636327d49577b560501e9f Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Wed, 20 Mar 2024 14:38:45 +0000 Subject: [PATCH 09/12] Moved _course_overrides_list into _course_overrides. --- .../wideq/devices/washerDryer.py | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index ad137214..0b8dc661 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -128,14 +128,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 - # For the selected course, the options that can be overridden and the override value. - # Uses internal names. {'option internal name': value internal name, ...} - # Initialised by select_start_course and _select_start_option. read in selected_*, _select_start_option and _prepare_course_info + # For the selected course, the options that can be overridden their permitted values + # and current value. Uses internal names. + # {'option internal name': { 'currently': value internal name, + # 'permitted: [value internal name, ...]}, ...} + # Initialised by select_start_course and _select_start_option. read in selected_*, + # _select_option_enabled, _select_start_option, _prepare_course_info and remote_start. self._course_overrides: dict | None = {} - # For the selected course, the permitted values for each option that can be overridden. - # Uses internal names. {'option internal name': [value internal name, ...], ...} - # Initialised by select_start_course, _select_start_option. read in _select_option_enabled, _select_start_option and remote_start - self._course_overrides_lists: dict | None = {} # For some options and their values, map the internal name to and from a friendly name. # The friendly name is extracted from the machine info language pack # {'option internal name': {'friendly_name': option friendly name, @@ -270,7 +269,9 @@ def temps_list(self) -> list: @property def selected_temp(self) -> str: """Return current selected water temperature.""" - value = self._convert_option_value('temp', self._course_overrides.get('temp')) + value = self._convert_option_value( + 'temp', + self._course_overrides.get('temp',{}).get('currently')) return value.friendly @cached_property @@ -282,7 +283,9 @@ def rinses_list(self) -> list: @property def selected_rinse(self) -> str: """Return current selected rinse option.""" - value = self._convert_option_value('rinse', self._course_overrides.get('rinse')) + value = self._convert_option_value( + 'rinse', + self._course_overrides.get('rinse',{}).get('currently')) return value.friendly @cached_property @@ -294,7 +297,9 @@ def spins_list(self) -> list: @property def selected_spin(self) -> str: """Return current selected spin speed.""" - value = self._convert_option_value('spin', self._course_overrides.get('spin')) + value = self._convert_option_value( + 'spin', + self._course_overrides.get('spin',{}).get('currently')) return value.friendly @property @@ -548,7 +553,7 @@ def _prepare_course_info( ret_data[ckey] = cdata # If an override is defeined then apply it - if override_value := self._course_overrides.get(ckey): + if override_value := self._course_overrides.get(ckey,{}).get('currently'): ret_data[ckey] = override_value _LOGGER.debug("_prepare_course_info, course data override: %s: %s", ckey, ret_data[ckey]) @@ -850,22 +855,19 @@ async def select_start_course(self, course_name: str) -> None: raise ValueError("Course info not available") 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 + self._course_overrides[value] = {'currently': default, 'permitted': selectable} def _select_option_enabled(self, select_name: str) -> bool: """Return if specified option select is enabled.""" - enabled = self.select_course_enabled and self._selected_course and self._course_overrides_lists.get(select_name) + enabled = self.select_course_enabled and self._selected_course and self._course_overrides.get(select_name) if (not enabled) and (select_name in self._course_overrides): del self._course_overrides[select_name] return enabled @@ -880,9 +882,9 @@ def _select_start_option(self, option_str: str, value_str: str) -> None: value = self._convert_option_value(option.internal, value_str) # _course_overrides_lists, _course_overrides and permitted_options use internal names - permitted_options = self._course_overrides_lists.get(option.internal) + permitted_options = self._course_overrides.get(option.internal,{}).get('permitted') if value and permitted_options and (value.internal in permitted_options): - self._course_overrides[option.internal] = value.internal + self._course_overrides[option.internal]['currently'] = value.internal else: raise ServiceValidationError(f"The value '{value_str}' is invalid and has been ignored. The value for the option '{option.friendly}' must be one of: {self._get_friendly_values_list(option.internal, permitted_options)} or {permitted_options}.") @@ -943,7 +945,7 @@ async def remote_start(self, course_name: str | None = None, overrides_json: str try: overrides = json.loads(overrides_json) except: raise ServiceValidationError("'overrides' contains invalid JSON.") - allowed_overrides = self._course_overrides_lists.keys() + allowed_overrides = self._course_overrides.keys() if not overrides: raise ServiceValidationError(f"'overrides' must contain one option from {self._get_friendly_names_list(list(allowed_overrides))} or {list(allowed_overrides)}.") for option_str in overrides: option = self._convert_option_name(option_str) From 150d2739e47b4d3a5e69bf1b7d6a837668efb84b Mon Sep 17 00:00:00 2001 From: John Lancaster Date: Fri, 11 Oct 2024 15:52:57 +0100 Subject: [PATCH 10/12] Added localization support for error messages --- .../smartthinq_sensors/services.yaml | 8 +-- .../smartthinq_sensors/translations/en.json | 23 +++++++ .../wideq/devices/washerDryer.py | 67 ++++++++++++++++--- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/custom_components/smartthinq_sensors/services.yaml b/custom_components/smartthinq_sensors/services.yaml index 31deafd8..19985e47 100644 --- a/custom_components/smartthinq_sensors/services.yaml +++ b/custom_components/smartthinq_sensors/services.yaml @@ -7,14 +7,14 @@ remote_start: domain: sensor fields: course: - name: course - description: Course (if not set will use current) + name: Programme + description: The wash programme (course), if not set will use current. required: false selector: text: overrides: - name: overrides - description: Overrides (JSON dictionary containing the options to override and their new value) + name: Option overrides + description: A JSON dictionary containing the options to override and their new value as name:value pairs. required: false selector: text: diff --git a/custom_components/smartthinq_sensors/translations/en.json b/custom_components/smartthinq_sensors/translations/en.json index 2923bf65..2b58989c 100644 --- a/custom_components/smartthinq_sensors/translations/en.json +++ b/custom_components/smartthinq_sensors/translations/en.json @@ -48,5 +48,28 @@ } }, "title": "SmartThinQ LGE Sensors" + }, + "exceptions": { + "remote_start_disabled": { + "message": "Machine is off, asleep or running, or remote start is not enabled." + }, + "course_name_required": { + "message": "Programme required if option overrides are specified." + }, + "invalid_json": { + "message": "Option overrides contains invalid JSON. {error}" + }, + "option_missing": { + "message": "Option overrides must contain at least one option from {friendly_list} or {internal_list}." + }, + "option_cannot_be_overridden": { + "message": "The programme {course} does not allow the option {option} to be overridden. Permitted options are: {friendly_list} or {internal_list}." + }, + "invalid_programme": { + "message": "The programme {name} is invalid. Permitted programmes are: {permitted}." + }, + "invalid_option_value": { + "message": "The value {value} is invalid and has been ignored. The value for the option {option} must be one of: {friendly_list} or {internal_list}." + } } } diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index 0b8dc661..a65843a2 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -16,6 +16,7 @@ from ..core_exceptions import InvalidDeviceStatus from ..device import Device, DeviceStatus from ..device_info import DeviceInfo, DeviceType +from ...const import DOMAIN STATE_WM_POWER_OFF = "STATE_POWER_OFF" STATE_WM_INITIAL = "STATE_INITIAL" @@ -135,6 +136,7 @@ def __init__( # Initialised by select_start_course and _select_start_option. read in selected_*, # _select_option_enabled, _select_start_option, _prepare_course_info and remote_start. self._course_overrides: dict | None = {} + # For some options and their values, map the internal name to and from a friendly name. # The friendly name is extracted from the machine info language pack # {'option internal name': {'friendly_name': option friendly name, @@ -844,7 +846,14 @@ async def select_start_course(self, course_name: str) -> None: self._selected_course = None return if course_name not in self.course_list: - raise ServiceValidationError(f"The programme (course name) '{course_name}' is invalid. Permitted names are: {list(self.course_list)}.") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_programme", + translation_placeholders={ + "name": "'"+str(course_name)+"'", + "permitted": str(list(self.course_list)) + } + ) self._selected_course = course_name # For the selected course save the permitted values for setting that can be overridden @@ -862,9 +871,9 @@ async def select_start_course(self, course_name: str) -> None: 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] = {'currently': default, 'permitted': selectable} - + _LOGGER.debug("select_start_course(%s), set overrides for %s to %s", course_name, value, self._course_overrides[value]) + def _select_option_enabled(self, select_name: str) -> bool: """Return if specified option select is enabled.""" enabled = self.select_course_enabled and self._selected_course and self._course_overrides.get(select_name) @@ -886,7 +895,16 @@ def _select_start_option(self, option_str: str, value_str: str) -> None: if value and permitted_options and (value.internal in permitted_options): self._course_overrides[option.internal]['currently'] = value.internal else: - raise ServiceValidationError(f"The value '{value_str}' is invalid and has been ignored. The value for the option '{option.friendly}' must be one of: {self._get_friendly_values_list(option.internal, permitted_options)} or {permitted_options}.") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_option_value", + translation_placeholders={ + "value": "'"+str(value_str)+"'", + "option": str(option.friendly), + "friendly_list": str(self._get_friendly_values_list(option.internal, permitted_options)), + "internal_list": str(list(permitted_options)) + } + ) @property def select_temp_enabled(self) -> bool: @@ -935,25 +953,56 @@ async def wake_up(self): async def remote_start(self, course_name: str | None = None, overrides_json: str | None = None) -> None: """Remote start the device.""" if not self.remote_start_enabled: - raise ServiceValidationError("Machine is off, asleep or running, or remote start is not enabled.") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="remote_start_disabled" + ) if course_name and self._initial_bit_start: await self.select_start_course(course_name) if overrides_json: - if not course_name: raise ServiceValidationError("Programme (course) name required if overrides are specified.") + if not course_name: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="course_name_required" + ) + try: overrides = json.loads(overrides_json) - except: raise ServiceValidationError("'overrides' contains invalid JSON.") + except json.JSONDecodeError as error: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_json", + translation_placeholders={"error": str(error)} + ) from error allowed_overrides = self._course_overrides.keys() - if not overrides: raise ServiceValidationError(f"'overrides' must contain one option from {self._get_friendly_names_list(list(allowed_overrides))} or {list(allowed_overrides)}.") + if not overrides: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="option_missing", + translation_placeholders={ + "friendly_list": str(self._get_friendly_names_list(list(allowed_overrides))), + "internal_list": str(list(allowed_overrides)) + } + ) + for option_str in overrides: option = self._convert_option_name(option_str) if option and (option.internal in allowed_overrides): value_str = overrides.get(option_str) self._select_start_option(option.internal, value_str) else: - raise ServiceValidationError(f"The programme '{course_name}' does not allow the option '{option_str}' to be overridden. Permitted options are: {self._get_friendly_names_list(list(allowed_overrides))} or {list(allowed_overrides)}.") + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="option_cannot_be_overridden", + translation_placeholders={ + "course": str(course_name), + "option": "'"+str(option_str)+"'", + "friendly_list": str(self._get_friendly_names_list(list(allowed_overrides))), + "internal_list": str(list(allowed_overrides)) + } + ) keys = self._get_cmd_keys(CMD_REMOTE_START) await self.set(keys[0], keys[1], key=keys[2]) From 329d756d323153bb742b2cf83a9f645fc8c64b14 Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Wed, 30 Oct 2024 11:25:25 +0000 Subject: [PATCH 11/12] added debug --- .../smartthinq_sensors/wideq/devices/washerDryer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index a65843a2..ccc460e1 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -151,6 +151,8 @@ def __init__( def _build_friendly_enum_value(self, option: str): """Add to friendly names dictionary the enum friendly values.""" + ##jl + _LOGGER.debug('self.model_info._data: %s',self.model_info._data) if not self.model_info.is_enum_type(option): return @@ -255,6 +257,8 @@ def subkey_device(self) -> Device | None: def course_list(self) -> list: """Return a list of available course.""" course_infos = self._get_course_infos() + ##JL + _LOGGER.debug('course_list: %s', course_infos) return [_CURRENT_COURSE, *course_infos.keys()] @property @@ -266,6 +270,8 @@ def selected_course(self) -> str: def temps_list(self) -> list: """Return a list of available water temperatures for the selected course.""" self._build_friendly_enum_value('temp') + ##JL + _LOGGER.debug('_option_friendly_names: %s', self._option_friendly_names) return list(self._option_friendly_names['temp']['enum_values'].values()) @property From 6d8c281505741abc18ffc2021f634bba195cb9a4 Mon Sep 17 00:00:00 2001 From: lancasterJ Date: Thu, 31 Oct 2024 17:04:46 +0000 Subject: [PATCH 12/12] Removed temp, spinf and rinse selctors from GUI because of race condition with the setup model_info --- .../smartthinq_sensors/select.py | 57 ++++--- .../wideq/devices/washerDryer.py | 146 +++++++++--------- 2 files changed, 103 insertions(+), 100 deletions(-) diff --git a/custom_components/smartthinq_sensors/select.py b/custom_components/smartthinq_sensors/select.py index 4cdf41f0..3002673e 100644 --- a/custom_components/smartthinq_sensors/select.py +++ b/custom_components/smartthinq_sensors/select.py @@ -42,41 +42,40 @@ class ThinQSelectEntityDescription( WASH_DEV_SELECT: tuple[ThinQSelectEntityDescription, ...] = ( ThinQSelectEntityDescription( key="course_selection", - # Changed the name so that course controls are grouped together on the UI. name="Course selection", - name="Set Course", + name="Course selection", 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, - ), + # 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, + # ), ) MICROWAVE_SELECT: tuple[ThinQSelectEntityDescription, ...] = ( ThinQSelectEntityDescription( diff --git a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py index ccc460e1..be888401 100644 --- a/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py +++ b/custom_components/smartthinq_sensors/wideq/devices/washerDryer.py @@ -129,7 +129,7 @@ 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 - # For the selected course, the options that can be overridden their permitted values + # For the selected course, the options that can be overridden, their permitted values # and current value. Uses internal names. # {'option internal name': { 'currently': value internal name, # 'permitted: [value internal name, ...]}, ...} @@ -150,9 +150,12 @@ def __init__( self._initial_bit_start: bool = False def _build_friendly_enum_value(self, option: str): - """Add to friendly names dictionary the enum friendly values.""" + """Add to friendly names dictionary the enum friendly values for 'option'.""" ##jl - _LOGGER.debug('self.model_info._data: %s',self.model_info._data) + ##_LOGGER.debug('self.model_info._data: %s',self.model_info._data) + if self._option_friendly_names.get(option) != None : + return + if not self.model_info.is_enum_type(option): return @@ -169,6 +172,8 @@ def _build_friendly_enum_value(self, option: str): self.model_info._data_root(option).get('label', option)) friendly_enum['enum_values'] = friendly_enum_values self._option_friendly_names[option] = friendly_enum + ##jl + _LOGGER.debug('self._option_friendly_names: %s',self._option_friendly_names) def _convert_option_name(self, option_str: str) -> FriendlyName | None: """Convert an option name to its internal and friendly forms.""" @@ -257,8 +262,6 @@ def subkey_device(self) -> Device | None: def course_list(self) -> list: """Return a list of available course.""" course_infos = self._get_course_infos() - ##JL - _LOGGER.debug('course_list: %s', course_infos) return [_CURRENT_COURSE, *course_infos.keys()] @property @@ -266,49 +269,49 @@ def selected_course(self) -> str: """Return current selected course.""" return self._selected_course or _CURRENT_COURSE - @cached_property - def temps_list(self) -> list: - """Return a list of available water temperatures for the selected course.""" - self._build_friendly_enum_value('temp') - ##JL - _LOGGER.debug('_option_friendly_names: %s', self._option_friendly_names) - return list(self._option_friendly_names['temp']['enum_values'].values()) + # @cached_property + # def temps_list(self) -> list: + # """Return a list of available water temperatures for the selected course.""" + # self._build_friendly_enum_value('temp') + #JL + # _LOGGER.debug('_option_friendly_names: %s', self._option_friendly_names) + # return list(self._option_friendly_names['temp']['enum_values'].values()) - @property - def selected_temp(self) -> str: - """Return current selected water temperature.""" - value = self._convert_option_value( - 'temp', - self._course_overrides.get('temp',{}).get('currently')) - return value.friendly - - @cached_property - def rinses_list(self) -> list: - """Return a list of available rinse options for the selected course.""" - self._build_friendly_enum_value('rinse') - return list(self._option_friendly_names['rinse']['enum_values'].values()) - - @property - def selected_rinse(self) -> str: - """Return current selected rinse option.""" - value = self._convert_option_value( - 'rinse', - self._course_overrides.get('rinse',{}).get('currently')) - return value.friendly - - @cached_property - def spins_list(self) -> list: - """Return a list of available spin speeds for the selected course.""" - self._build_friendly_enum_value('spin') - return list(self._option_friendly_names['spin']['enum_values'].values()) - - @property - def selected_spin(self) -> str: - """Return current selected spin speed.""" - value = self._convert_option_value( - 'spin', - self._course_overrides.get('spin',{}).get('currently')) - return value.friendly + # @property + # def selected_temp(self) -> str: + # """Return current selected water temperature.""" + # value = self._convert_option_value( + # 'temp', + # self._course_overrides.get('temp',{}).get('currently')) + # return value.friendly + + # @cached_property + # def rinses_list(self) -> list: + # """Return a list of available rinse options for the selected course.""" + # self._build_friendly_enum_value('rinse') + # return list(self._option_friendly_names['rinse']['enum_values'].values()) + + # @property + # def selected_rinse(self) -> str: + # """Return current selected rinse option.""" + # value = self._convert_option_value( + # 'rinse', + # self._course_overrides.get('rinse',{}).get('currently')) + # return value.friendly + + # @cached_property + # def spins_list(self) -> list: + # """Return a list of available spin speeds for the selected course.""" + # self._build_friendly_enum_value('spin') + # return list(self._option_friendly_names['spin']['enum_values'].values()) + + # @property + # def selected_spin(self) -> str: + # """Return current selected spin speed.""" + # value = self._convert_option_value( + # 'spin', + # self._course_overrides.get('spin',{}).get('currently')) + # return value.friendly @property def run_state(self) -> str: @@ -877,6 +880,7 @@ async def select_start_course(self, course_name: str) -> None: if selectable is None: continue + self._build_friendly_enum_value(value) self._course_overrides[value] = {'currently': default, 'permitted': selectable} _LOGGER.debug("select_start_course(%s), set overrides for %s to %s", course_name, value, self._course_overrides[value]) @@ -912,32 +916,32 @@ def _select_start_option(self, option_str: str, value_str: str) -> None: } ) - @property - def select_temp_enabled(self) -> bool: - """Return if select temp is enabled.""" - return self._select_option_enabled('temp') + # @property + # def select_temp_enabled(self) -> bool: + # """Return if select temp is enabled.""" + # return self._select_option_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', temp_name) - - @property - def select_rinse_enabled(self) -> bool: - """Return if select rinse is enabled.""" - return self._select_option_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_name) - - @property - def select_spin_enabled(self) -> bool: - """Return if select spin is enabled.""" - return self._select_option_enabled('spin') + # async def select_start_temp(self, temp_name: str) -> None: + # """Select a secific water temperature for remote start.""" + # self._select_start_option('temp', temp_name) + + # @property + # def select_rinse_enabled(self) -> bool: + # """Return if select rinse is enabled.""" + # return self._select_option_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_name) + + # @property + # def select_spin_enabled(self) -> bool: + # """Return if select spin is enabled.""" + # return self._select_option_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_name) + # async def select_start_spin(self, spin_name: str) -> None: + # """Select a secific spin for remote start.""" + # self._select_start_option('spin', spin_name) async def power_off(self): """Power off the device."""