Add Samsung washer and dryer cycle and state sensors to SmartThings#171684
Add Samsung washer and dryer cycle and state sensors to SmartThings#171684gielk wants to merge 7 commits into
Conversation
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
|
Hey there @joostlek, mind taking a look at this pull request as it has been labeled with an integration ( Code owner commandsCode owners of
|
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds additional Samsung CE washer/dryer telemetry to the SmartThings integration by surfacing cycle selection and operating-state progress/remaining-time as sensor entities with localized enum state labels.
Changes:
- Add new translation keys and enum state labels for washer/dryer cycles.
- Add SmartThings sensor entity descriptions for washer/dryer cycle, progress (%), and remaining time (minutes).
- Normalize raw cycle values to match enum option keys.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| homeassistant/components/smartthings/strings.json | Adds translation keys and enum state labels for washer/dryer cycle sensors and operating-state sensors. |
| homeassistant/components/smartthings/sensor.py | Defines enum options and new sensor entity descriptions; adds normalization logic for washer/dryer cycle values. |
| def native_value(self) -> str | float | datetime | int | None: | ||
| """Return the state of the sensor.""" | ||
| res = self.get_attribute_value(self.capability, self._attribute) | ||
| if self.capability in ("samsungce.washerCycle", "samsungce.dryerCycle"): |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| def native_value(self) -> str | float | datetime | int | None: | ||
| """Return the state of the sensor.""" | ||
| res = self.get_attribute_value(self.capability, self._attribute) | ||
| if self.capability in ("samsungce.washerCycle", "samsungce.dryerCycle"): | ||
| if not res: | ||
| return None | ||
| return res.split("_")[-1].lower() if "_" in res else res.lower() | ||
| if options_map := self.entity_description.options_map: | ||
| return options_map.get(res) | ||
| value = self.entity_description.value_fn(res) |
| from .entity import SmartThingsEntity | ||
| from .util import deprecate_entity | ||
|
|
||
|
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72b81a1021
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| options=WASHER_CYCLES, | ||
| device_class=SensorDeviceClass.ENUM, |
There was a problem hiding this comment.
Use device-reported cycle options for enum sensors
These new enum sensors hardcode options to fixed code lists, but native_value can still emit any cycle code reported by SmartThings after normalization. In Home Assistant, enum sensors reject states not present in options (SensorEntity._stringify_state checks value not in options), so any model/firmware-specific or newly added cycle code will make the sensor state invalid instead of updating. This is especially likely for Samsung laundry devices where supported cycles vary by model; options should be derived from the device’s supported cycle attributes (or the state should be guarded) rather than a static list.
Useful? React with 👍 / 👎.
…from supportedCycles
| # Guard against rejection: append current state if not present | ||
| if (current_value := self.native_value) is not None: | ||
| current_value_str = str(current_value) | ||
| if current_value_str not in options_list: | ||
| options_list.append(current_value_str) |
| # Fall back to static options in description if attribute is missing/None | ||
| if (static_options := super().options) is not None: | ||
| options_list = list(static_options) | ||
| if (current_value := self.native_value) is not None: | ||
| current_value_str = str(current_value) | ||
| if current_value_str not in options_list: | ||
| options_list.append(current_value_str) | ||
| return options_list | ||
| return [] | ||
|
|
||
| if (static_options := super().options) is not None: | ||
| options_list = list(static_options) | ||
| if (current_value := self.native_value) is not None: | ||
| current_value_str = str(current_value) | ||
| if current_value_str not in options_list: | ||
| options_list.append(current_value_str) | ||
| return options_list |
joostlek
left a comment
There was a problem hiding this comment.
The CI is failing. I would love to see if we can do something similar like someone has done for the dishwasher
What has someone done for the dishwasher? Where can I find it? |
| # Guard against rejection: append current state if not present | ||
| if (current_value := self.native_value) is not None: | ||
| current_value_str = str(current_value) | ||
| if current_value_str not in options_list: | ||
| options_list.append(current_value_str) | ||
| return options_list |
| opt_str = str(opt_str) | ||
| if options_map := self.entity_description.options_map: | ||
| opt_str = options_map.get(opt_str, opt_str) | ||
| options_list.append(opt_str.lower()) |
| translation_key="washer_machine_state", | ||
| options=WASHER_OPTIONS, | ||
| device_class=SensorDeviceClass.ENUM, | ||
| component_fn=lambda component: component == "sub", | ||
| component_translation_key={ | ||
| "sub": "washer_sub_machine_state", | ||
| }, |
| @@ -143,6 +144,54 @@ | |||
|
|
|||
| WASHER_OPTIONS = ["pause", "run", "stop"] | |||
| assert _normalize_cycle_value("washer_delicate") == "delicate" | ||
| assert _normalize_cycle_value("delicate") == "delicate" | ||
|
|
||
|
|
| if not value: | ||
| return None | ||
| value_str = str(value) | ||
| return ( | ||
| value_str.rsplit("_", maxsplit=1)[-1].lower() | ||
| if "_" in value_str | ||
| else value_str.lower() | ||
| ) | ||
|
|
||
|
|
| options_val = self.get_attribute_value( | ||
| self.capability, self.entity_description.options_attribute | ||
| ) | ||
| if options_val is not None: | ||
| options_list = [] | ||
| for option in options_val: | ||
| if isinstance(option, dict): | ||
| opt_val = option.get("cycle") | ||
| else: | ||
| opt_val = option | ||
| if opt_val is not None: | ||
| if options_map := self.entity_description.options_map: | ||
| opt_val = options_map.get(opt_val, opt_val) | ||
| else: | ||
| opt_val = self.entity_description.value_fn(opt_val) | ||
| if self.entity_description.presentation_fn: | ||
| opt_val = self.entity_description.presentation_fn( | ||
| self.device.device.presentation_id, opt_val | ||
| ) | ||
| if opt_val is not None: | ||
| options_list.append(str(opt_val).lower()) |
| elif (static_options := super().options) is not None: | ||
| options_list = [str(opt).lower() for opt in static_options] | ||
| else: | ||
| return [] |
| "state": { | ||
| "pause": "[%key:common::state::paused%]", | ||
| "paused": "[%key:common::state::paused%]", | ||
| "ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]", |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e73a0bda56
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| value_str.rsplit("_", maxsplit=1)[-1].lower() | ||
| if "_" in value_str |
There was a problem hiding this comment.
Preserve full cycle identifier when normalizing values
The normalization logic drops everything before the final underscore, so cycle IDs like NORMAL_CYCLE and DELICATE_CYCLE both become cycle. In environments where SmartThings reports underscore-delimited names instead of trailing hex codes, this collapses distinct washer/dryer cycles into one enum state, making the sensor inaccurate and breaking automations that depend on the specific cycle.
Useful? React with 👍 / 👎.
Proposed change
This PR adds cycle and progress/remaining time sensors to the SmartThings integration for Samsung washing machines and dryers.
Specifically, it registers the following new sensor entities:
samsungce.washerCycle(usingSensorDeviceClass.ENUMwith cycle code options, translated viastrings.json)samsungce.dryerCycle(usingSensorDeviceClass.ENUMwith cycle code options, translated viastrings.json)samsungce.washerOperatingState(progressin % andremainingTimein minutes)samsungce.dryerOperatingState(progressin % andremainingTimein minutes)State values from the cycle capabilities are normalized to lowercase codes (e.g.
1b,1c,51) and translated via Home Assistant's standard translation framework instrings.json.Type of change
Additional information
strings.jsonfor English. Localizations in other languages will be imported via Crowdin.Checklist