From 34dbd1fb108ce29510160165c8a3c805f205ac9d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 8 May 2025 21:03:41 +0200 Subject: [PATCH] Add hob support to SmartThings (#144493) * Add hob support to SmartThings * Add hob support to SmartThings * Add hob support to SmartThings * Fix * Update homeassistant/components/smartthings/icons.json --- .../components/smartthings/__init__.py | 5 + .../components/smartthings/icons.json | 18 + .../components/smartthings/sensor.py | 157 +++++-- .../components/smartthings/strings.json | 16 + .../device_status/da_ks_cooktop_31001.json | 10 +- .../smartthings/snapshots/test_sensor.ambr | 424 ++++++++++++++++++ 6 files changed, 577 insertions(+), 53 deletions(-) diff --git a/homeassistant/components/smartthings/__init__.py b/homeassistant/components/smartthings/__init__.py index cec71f91750..fe1b965db30 100644 --- a/homeassistant/components/smartthings/__init__.py +++ b/homeassistant/components/smartthings/__init__.py @@ -493,6 +493,11 @@ def process_status(status: dict[str, ComponentStatus]) -> dict[str, ComponentSta ) if disabled_components is not None: for component in disabled_components: + # Burner components are named burner-06 + # but disabledComponents contain burner-6 + if "burner" in component: + burner_id = int(component.split("-")[-1]) + component = f"burner-0{burner_id}" if component in status: del status[component] for component_status in status.values(): diff --git a/homeassistant/components/smartthings/icons.json b/homeassistant/components/smartthings/icons.json index b6e33e1a142..51978590e2e 100644 --- a/homeassistant/components/smartthings/icons.json +++ b/homeassistant/components/smartthings/icons.json @@ -57,6 +57,24 @@ "paused": "mdi:pause", "finished": "mdi:food-turkey" } + }, + "manual_level": { + "default": "mdi:radiator", + "state": { + "0": "mdi:radiator-off" + } + }, + "heating_mode": { + "state": { + "off": "mdi:power", + "manual": "mdi:cog", + "boost": "mdi:flash", + "keep_warm": "mdi:fire", + "quick_preheat": "mdi:heat-wave", + "defrost": "mdi:car-defrost-rear", + "melt": "mdi:snowflake-melt", + "simmer": "mdi:fire" + } } }, "switch": { diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index 3c32df271a9..fac503399a9 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -45,6 +45,17 @@ THERMOSTAT_CAPABILITIES = { Capability.THERMOSTAT_MODE, } +COOKTOP_HEATING_MODES = { + "off": "off", + "manual": "manual", + "boost": "boost", + "keepWarm": "keep_warm", + "quickPreheat": "quick_preheat", + "defrost": "defrost", + "melt": "melt", + "simmer": "simmer", +} + JOB_STATE_MAP = { "airWash": "air_wash", "airwash": "air_wash", @@ -133,6 +144,9 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): extra_state_attributes_fn: Callable[[Any], dict[str, Any]] | None = None capability_ignore_list: list[set[Capability]] | None = None options_attribute: Attribute | None = None + options_map: dict[str, str] | None = None + translation_placeholders_fn: Callable[[str], dict[str, str]] | None = None + component_fn: Callable[[str], bool] | None = None exists_fn: Callable[[Status], bool] | None = None use_temperature_unit: bool = False deprecated: Callable[[ComponentStatus], str | None] | None = None @@ -265,6 +279,31 @@ CAPABILITY_TO_SENSORS: dict[ ) ] }, + Capability.SAMSUNG_CE_COOKTOP_HEATING_POWER: { + Attribute.MANUAL_LEVEL: [ + SmartThingsSensorEntityDescription( + key=Attribute.MANUAL_LEVEL, + translation_key="manual_level", + translation_placeholders_fn=lambda component: { + "burner_id": component.split("-0")[-1] + }, + component_fn=lambda component: component.startswith("burner-0"), + ) + ], + Attribute.HEATING_MODE: [ + SmartThingsSensorEntityDescription( + key=Attribute.HEATING_MODE, + translation_key="heating_mode", + options_attribute=Attribute.SUPPORTED_HEATING_MODES, + options_map=COOKTOP_HEATING_MODES, + device_class=SensorDeviceClass.ENUM, + translation_placeholders_fn=lambda component: { + "burner_id": component.split("-0")[-1] + }, + component_fn=lambda component: component.startswith("burner-0"), + ) + ], + }, Capability.CUSTOM_COOKTOP_OPERATING_STATE: { Attribute.COOKTOP_OPERATING_STATE: [ SmartThingsSensorEntityDescription( @@ -1038,59 +1077,72 @@ async def async_setup_entry( for device in entry_data.devices.values(): # pylint: disable=too-many-nested-blocks for capability, attributes in CAPABILITY_TO_SENSORS.items(): - if capability in device.status[MAIN]: - for attribute, descriptions in attributes.items(): - for description in descriptions: - if ( - not description.capability_ignore_list - or not any( - all( - capability in device.status[MAIN] - for capability in capability_list - ) - for capability_list in description.capability_ignore_list - ) - ) and ( - not description.exists_fn - or description.exists_fn( - device.status[MAIN][capability][attribute] - ) - ): + for component, capabilities in device.status.items(): + if capability in capabilities: + for attribute, descriptions in attributes.items(): + for description in descriptions: if ( - description.deprecated - and ( - reason := description.deprecated( - device.status[MAIN] + ( + not description.capability_ignore_list + or not any( + all( + capability in device.status[MAIN] + for capability in capability_list + ) + for capability_list in description.capability_ignore_list + ) + ) + and ( + not description.exists_fn + or description.exists_fn( + device.status[MAIN][capability][attribute] + ) + ) + and ( + component == MAIN + or ( + description.component_fn is not None + and description.component_fn(component) ) ) - is not None ): - if deprecate_entity( - hass, - entity_registry, - SENSOR_DOMAIN, - f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}", - f"deprecated_{reason}", - ): - entities.append( - SmartThingsSensor( - entry_data.client, - device, - description, - capability, - attribute, + if ( + description.deprecated + and ( + reason := description.deprecated( + device.status[MAIN] ) ) - continue - entities.append( - SmartThingsSensor( - entry_data.client, - device, - description, - capability, - attribute, + is not None + ): + if deprecate_entity( + hass, + entity_registry, + SENSOR_DOMAIN, + f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{description.key}", + f"deprecated_{reason}", + ): + entities.append( + SmartThingsSensor( + entry_data.client, + device, + description, + MAIN, + capability, + attribute, + ) + ) + continue + entities.append( + SmartThingsSensor( + entry_data.client, + device, + description, + component, + capability, + attribute, + ) ) - ) async_add_entities(entities) @@ -1105,6 +1157,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): client: SmartThings, device: FullDevice, entity_description: SmartThingsSensorEntityDescription, + component: str, capability: Capability, attribute: Attribute, ) -> None: @@ -1112,16 +1165,22 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): capabilities_to_subscribe = {capability} if entity_description.use_temperature_unit: capabilities_to_subscribe.add(Capability.TEMPERATURE_MEASUREMENT) - super().__init__(client, device, capabilities_to_subscribe) - self._attr_unique_id = f"{device.device.device_id}_{MAIN}_{capability}_{attribute}_{entity_description.key}" + super().__init__(client, device, capabilities_to_subscribe, component=component) + self._attr_unique_id = f"{device.device.device_id}_{component}_{capability}_{attribute}_{entity_description.key}" self._attribute = attribute self.capability = capability self.entity_description = entity_description + if self.entity_description.translation_placeholders_fn: + self._attr_translation_placeholders = ( + self.entity_description.translation_placeholders_fn(component) + ) @property 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 options_map := self.entity_description.options_map: + return options_map.get(res) return self.entity_description.value_fn(res) @property @@ -1158,5 +1217,7 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity): ) ) 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 diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 828948910c2..9cec158b5a9 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -181,6 +181,22 @@ "finished": "[%key:component::smartthings::entity::sensor::oven_job_state::state::finished%]" } }, + "manual_level": { + "name": "Burner {burner_id} level" + }, + "heating_mode": { + "name": "Burner {burner_id} heating mode", + "state": { + "off": "[%key:common::state::off%]", + "manual": "[%key:common::state::manual%]", + "boost": "Boost", + "keep_warm": "Keep warm", + "quick_preheat": "Quick preheat", + "defrost": "Defrost", + "melt": "Melt", + "simmer": "Simmer" + } + }, "dishwasher_machine_state": { "name": "Machine state", "state": { diff --git a/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json b/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json index 5ca8f56fbbf..ab836de52ad 100644 --- a/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json +++ b/tests/components/smartthings/fixtures/device_status/da_ks_cooktop_31001.json @@ -9,11 +9,11 @@ }, "samsungce.cooktopHeatingPower": { "manualLevel": { - "value": 0, + "value": 5, "timestamp": "2025-03-26T05:57:23.203Z" }, "heatingMode": { - "value": "manual", + "value": "boost", "timestamp": "2025-03-25T18:18:28.550Z" }, "manualLevelMin": { @@ -95,7 +95,7 @@ "main": { "custom.disabledComponents": { "disabledComponents": { - "value": ["burner-6"], + "value": ["burner-05", "burner-6"], "timestamp": "2025-03-25T18:18:28.464Z" } }, @@ -467,11 +467,11 @@ }, "samsungce.cooktopHeatingPower": { "manualLevel": { - "value": 0, + "value": 2, "timestamp": "2025-03-26T07:27:58.652Z" }, "heatingMode": { - "value": "manual", + "value": "keepWarm", "timestamp": "2025-03-25T18:18:28.550Z" }, "manualLevelMin": { diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 8c631ed6983..df943079fe2 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -2578,6 +2578,430 @@ 'state': '27', }) # --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_1_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 1 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 1 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_1_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_1_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 1 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-01_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_1_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 1 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_1_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_2_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 2 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 2 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_2_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'boost', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_2_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 2 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-02_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_2_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 2 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_2_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '5', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_3_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 3 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 3 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_3_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'keep_warm', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_3_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 3 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-03_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_3_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 3 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_3_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_heating_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_4_heating_mode', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Burner 4 heating mode', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'heating_mode', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_heatingMode_heatingMode', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_heating_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Induction Hob Burner 4 heating mode', + 'options': list([ + 'manual', + 'boost', + 'keep_warm', + ]), + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_4_heating_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'manual', + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_level-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.induction_hob_burner_4_level', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Burner 4 level', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'manual_level', + 'unique_id': '808dbd84-f357-47e2-a0cd-3b66fa22d584_burner-04_samsungce.cooktopHeatingPower_manualLevel_manualLevel', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_burner_4_level-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Induction Hob Burner 4 level', + }), + 'context': , + 'entity_id': 'sensor.induction_hob_burner_4_level', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0', + }) +# --- # name: test_all_entities[da_ks_cooktop_31001][sensor.induction_hob_operating_state-entry] EntityRegistryEntrySnapshot({ 'aliases': set({