Skip to content
Open
178 changes: 168 additions & 10 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 @@ -143,6 +144,55 @@

WASHER_OPTIONS = ["pause", "run", "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]:
"""Return the power attributes."""
Expand All @@ -153,6 +203,14 @@
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.split("_")[-1].lower() if "_" in value_str else value_str.lower()

Check warning on line 211 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check pylint

C0207: Use value_str.rsplit('_', maxsplit=1)[-1] instead (use-maxsplit-arg)


Comment on lines +216 to +225
@dataclass(frozen=True, kw_only=True)
class SmartThingsSensorEntityDescription(SensorEntityDescription):
"""Describe a SmartThings sensor entity."""
Expand Down Expand Up @@ -1278,6 +1336,74 @@
)
]
},
"samsungce.washerCycle": {

Check failure on line 1339 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 70 has incompatible type "str": "dict[Attribute, list[SmartThingsSensorEntityDescription]]"; expected "Capability": "dict[Attribute, list[SmartThingsSensorEntityDescription]]" [dict-item]
"washerCycle": [

Check failure on line 1340 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 0 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="washerCycle",
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,
)
]
},
"samsungce.dryerCycle": {

Check failure on line 1352 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 71 has incompatible type "str": "dict[Attribute, list[SmartThingsSensorEntityDescription]]"; expected "Capability": "dict[Attribute, list[SmartThingsSensorEntityDescription]]" [dict-item]
"dryerCycle": [

Check failure on line 1353 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 0 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="dryerCycle",
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,
)
]
},
"samsungce.washerOperatingState": {

Check failure on line 1365 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 72 has incompatible type "str": "dict[Attribute, list[SmartThingsSensorEntityDescription]]"; expected "Capability": "dict[Attribute, list[SmartThingsSensorEntityDescription]]" [dict-item]
"progress": [

Check failure on line 1366 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 0 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="progress",
translation_key="washer_progress",
icon="mdi:washing-machine",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
)
],
"remainingTime": [

Check failure on line 1375 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 1 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="remainingTime",
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.
]
},
"samsungce.dryerOperatingState": {

Check failure on line 1386 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 73 has incompatible type "str": "dict[Attribute, list[SmartThingsSensorEntityDescription]]"; expected "Capability": "dict[Attribute, list[SmartThingsSensorEntityDescription]]" [dict-item]
"progress": [

Check failure on line 1387 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 0 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="progress",
translation_key="dryer_progress",
icon="mdi:tumble-dryer",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
)
],
"remainingTime": [

Check failure on line 1396 in homeassistant/components/smartthings/sensor.py

View workflow job for this annotation

GitHub Actions / Check mypy

Dict entry 1 has incompatible type "str": "list[SmartThingsSensorEntityDescription]"; expected "Attribute": "list[SmartThingsSensorEntityDescription]" [dict-item]
SmartThingsSensorEntityDescription(
key="remainingTime",
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 +1587,45 @@
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:
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
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_str = option.get("cycle")
else:
opt_str = option
if opt_str is not None:
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())

# 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

# 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

return None
64 changes: 64 additions & 0 deletions homeassistant/components/smartthings/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,70 @@
"stop": "[%key:common::state::stopped%]"
}
},
"washer_cycle": {
"name": "Washer cycle",
"state": {
"1c": "Eco 40-60",
"2b": "AI Wash",
"1b": "Cotton",
"1e": "15' Quick Wash",
"1d": "Super Speed",
"96": "Less Microfiber",
"8f": "Intense Cold",
"25": "Synthetics",
"26": "Delicates",
"33": "Towels",
"24": "Bedding",
"32": "Shirts",
"20": "Hygiene Steam",
"22": "Wool",
"23": "Outdoor",
"2f": "Activewear",
"21": "Colors",
"66": "Denim",
"2e": "Baby Care",
"2d": "Silent Wash",
"30": "Cloudy day",
Comment thread
gielk marked this conversation as resolved.
Outdated
"29": "Drum Clean+",
"27": "Rinse+Spin",
"28": "Drain/Spin"
}
},
"dryer_cycle": {
"name": "Dryer cycle",
"state": {
"51": "Eco Cotton",
"53": "AI Dry+",
"23": "Quick Dry 35'",
"17": "Super Speed",
"18": "Synthetics",
"19": "Delicates",
"1d": "Towels",
"1b": "Bedding",
"1c": "Shirts",
"21": "Hygiene Care",
"1a": "Wool",
"1e": "Outdoor",
"20": "Iron Dry",
"27": "Time Dry",
"25": "Warm Air",
"24": "Cool Air",
"4e": "Self Dry",
"4c": "Air Refresh"
}
},
"washer_progress": {
"name": "Washing progress"
},
"dryer_progress": {
"name": "Drying progress"
},
"washer_remaining_time": {
"name": "Washer remaining time"
},
"dryer_remaining_time": {
"name": "Dryer remaining time"
},
"washer_mode": {
"name": "Washer mode"
},
Expand Down
Loading