Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 186 additions & 13 deletions homeassistant/components/smartthings/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
)
from homeassistant.core import HomeAssistant
Expand Down Expand Up @@ -141,7 +142,64 @@
"UVPaused": "uv_paused",
}

WASHER_OPTIONS = ["pause", "run", "stop"]
DISHWASHER_MACHINE_STATE_OPTIONS = ["pause", "run", "stop"]
WASHER_MACHINE_STATE_OPTIONS = [
"pause",
"paused",
"ready",
"run",
"running",
"stop",
]


WASHER_CYCLES = [
"1c",
"2b",
"1b",
"1e",
"1d",
"96",
"8f",
"25",
"26",
"33",
"24",
"32",
"20",
"22",
"23",
"2f",
"21",
"66",
"2e",
"2d",
"30",
"29",
"27",
"28",
]
Comment thread
gielk marked this conversation as resolved.

DRYER_CYCLES = [
"51",
"53",
"23",
"17",
"18",
"19",
"1d",
"1b",
"1c",
"21",
"1a",
"1e",
"20",
"27",
"25",
"24",
"4e",
"4c",
]
Comment thread
gielk marked this conversation as resolved.


def power_attributes(status: dict[str, Any]) -> dict[str, Any]:
Expand All @@ -153,6 +211,18 @@ def power_attributes(status: dict[str, Any]) -> dict[str, Any]:
return state


def _normalize_cycle_value(value: Any) -> str | None:
"""Normalize washer/dryer cycle names."""
if not value:
return None
value_str = str(value)
return (
value_str.rsplit("_", maxsplit=1)[-1].lower()
if "_" in value_str
Comment on lines +220 to +221
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

else value_str.lower()
)


Comment on lines +216 to +225
@dataclass(frozen=True, kw_only=True)
class SmartThingsSensorEntityDescription(SensorEntityDescription):
"""Describe a SmartThings sensor entity."""
Expand Down Expand Up @@ -365,7 +435,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="dishwasher_machine_state",
options=WASHER_OPTIONS,
options=DISHWASHER_MACHINE_STATE_OPTIONS,
device_class=SensorDeviceClass.ENUM,
)
],
Expand Down Expand Up @@ -413,7 +483,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="dryer_machine_state",
options=WASHER_OPTIONS,
options=WASHER_MACHINE_STATE_OPTIONS,
device_class=SensorDeviceClass.ENUM,
)
],
Expand Down Expand Up @@ -1153,7 +1223,7 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
SmartThingsSensorEntityDescription(
key=Attribute.MACHINE_STATE,
translation_key="washer_machine_state",
options=WASHER_OPTIONS,
options=WASHER_MACHINE_STATE_OPTIONS,
device_class=SensorDeviceClass.ENUM,
component_fn=lambda component: component == "sub",
component_translation_key={
Expand Down Expand Up @@ -1278,6 +1348,74 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
)
]
},
Capability.SAMSUNG_CE_WASHER_CYCLE: {
Attribute.WASHER_CYCLE: [
SmartThingsSensorEntityDescription(
key=Attribute.WASHER_CYCLE,
translation_key="washer_cycle",
icon="mdi:washing-machine",
options=WASHER_CYCLES,
options_attribute=Attribute.SUPPORTED_CYCLES,
device_class=SensorDeviceClass.ENUM,
Comment on lines +1357 to +1359
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

value_fn=_normalize_cycle_value,
)
]
},
Capability.SAMSUNG_CE_DRYER_CYCLE: {
Attribute.DRYER_CYCLE: [
SmartThingsSensorEntityDescription(
key=Attribute.DRYER_CYCLE,
translation_key="dryer_cycle",
icon="mdi:tumble-dryer",
options=DRYER_CYCLES,
options_attribute=Attribute.SUPPORTED_CYCLES,
device_class=SensorDeviceClass.ENUM,
value_fn=_normalize_cycle_value,
)
]
},
Capability.SAMSUNG_CE_WASHER_OPERATING_STATE: {
Attribute.PROGRESS: [
SmartThingsSensorEntityDescription(
key=Attribute.PROGRESS,
translation_key="washer_progress",
icon="mdi:washing-machine",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
)
],
Attribute.REMAINING_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.REMAINING_TIME,
translation_key="washer_remaining_time",
icon="mdi:timer-sand",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
)
Comment thread
gielk marked this conversation as resolved.
Comment thread
gielk marked this conversation as resolved.
],
},
Capability.SAMSUNG_CE_DRYER_OPERATING_STATE: {
Attribute.PROGRESS: [
SmartThingsSensorEntityDescription(
key=Attribute.PROGRESS,
translation_key="dryer_progress",
icon="mdi:tumble-dryer",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
)
],
Attribute.REMAINING_TIME: [
SmartThingsSensorEntityDescription(
key=Attribute.REMAINING_TIME,
translation_key="dryer_remaining_time",
icon="mdi:timer-sand",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MINUTES,
)
Comment thread
gielk marked this conversation as resolved.
],
},
}


Expand Down Expand Up @@ -1461,13 +1599,48 @@ def extra_state_attributes(self) -> Mapping[str, Any] | None:
def options(self) -> list[str] | None:
"""Return the options for this sensor."""
if self.entity_description.options_attribute:
if (
options := self.get_attribute_value(
self.capability, self.entity_description.options_attribute
)
) is None:
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())
Comment on lines +1602 to +1622
# Fall back to static options in description if attribute is missing/None
elif (static_options := super().options) is not None:
options_list = [str(opt).lower() for opt in static_options]
else:
return []
if options_map := self.entity_description.options_map:
return [options_map[option] for option in options]
return [option.lower() for option in options]
return super().options
elif (static_options := super().options) is not None:
options_list = [str(opt).lower() for opt in static_options]
else:
return None

# Deduplicate while preserving order
seen: set[str] = set()
unique_options: list[str] = []
for opt in options_list:
if opt not in seen:
seen.add(opt)
unique_options.append(opt)

if (current_value := self.native_value) is not None:
current_value_str = str(current_value).lower()
if current_value_str not in seen:
unique_options.append(current_value_str)

return unique_options
73 changes: 73 additions & 0 deletions homeassistant/components/smartthings/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,29 @@
"tank": "Tank"
}
},
"dryer_cycle": {
"name": "Dryer cycle",
"state": {
"1a": "Wool",
"1b": "Bedding",
"1c": "Shirts",
"1d": "Towels",
"1e": "Outdoor",
"4c": "Air Refresh",
"4e": "Self Dry",
"17": "Super Speed",
"18": "Synthetics",
"19": "Delicates",
"20": "Iron Dry",
"21": "Hygiene Care",
"23": "Quick Dry 35'",
"24": "Cool Air",
"25": "Warm Air",
"27": "Time Dry",
"51": "Eco Cotton",
"53": "AI Dry+"
}
},
"dryer_job_state": {
"name": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::name%]",
"state": {
Expand All @@ -529,13 +552,22 @@
"name": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::name%]",
"state": {
"pause": "[%key:common::state::paused%]",
"paused": "[%key:common::state::paused%]",
"ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]",
"run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"running": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"stop": "[%key:common::state::stopped%]"
}
},
"dryer_mode": {
"name": "Dryer mode"
},
"dryer_progress": {
"name": "Drying progress"
},
"dryer_remaining_time": {
"name": "Dryer remaining time"
},
"energy_difference": {
"name": "Energy difference"
},
Expand Down Expand Up @@ -918,6 +950,35 @@
"uv_index": {
"name": "UV index"
},
"washer_cycle": {
"name": "Washer cycle",
"state": {
"1b": "Cotton",
"1c": "Eco 40-60",
"1d": "Super Speed",
"1e": "15' Quick Wash",
"2b": "AI Wash",
"2d": "Silent Wash",
"2e": "Baby Care",
"2f": "Activewear",
"8f": "Intense Cold",
"20": "Hygiene Steam",
"21": "Colors",
"22": "Wool",
"23": "Outdoor",
"24": "Bedding",
"25": "Synthetics",
"26": "Delicates",
"27": "Rinse+Spin",
"28": "Drain/Spin",
"29": "Drum Clean+",
"30": "Cloudy day",
"32": "Shirts",
"33": "Towels",
"66": "Denim",
"96": "Less Microfiber"
}
},
"washer_job_state": {
"name": "[%key:component::smartthings::entity::sensor::dishwasher_job_state::name%]",
"state": {
Expand All @@ -943,13 +1004,22 @@
"name": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::name%]",
"state": {
"pause": "[%key:common::state::paused%]",
"paused": "[%key:common::state::paused%]",
"ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]",
"run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"running": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"stop": "[%key:common::state::stopped%]"
}
},
"washer_mode": {
"name": "Washer mode"
},
"washer_progress": {
"name": "Washing progress"
},
"washer_remaining_time": {
"name": "Washer remaining time"
},
"washer_sub_completion_time": {
"name": "Upper washer completion time"
},
Expand Down Expand Up @@ -978,7 +1048,10 @@
"name": "Upper washer machine state",
"state": {
"pause": "[%key:common::state::paused%]",
"paused": "[%key:common::state::paused%]",
"ready": "[%key:component::smartthings::entity::sensor::oven_machine_state::state::ready%]",
"run": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"running": "[%key:component::smartthings::entity::sensor::dishwasher_machine_state::state::run%]",
"stop": "[%key:common::state::stopped%]"
}
},
Expand Down
Loading
Loading