diff --git a/homeassistant/components/lg_thinq/__init__.py b/homeassistant/components/lg_thinq/__init__.py index 47282b6cc22..25e0d4afb8b 100644 --- a/homeassistant/components/lg_thinq/__init__.py +++ b/homeassistant/components/lg_thinq/__init__.py @@ -42,6 +42,7 @@ PLATFORMS = [ Platform.CLIMATE, Platform.EVENT, Platform.FAN, + Platform.HUMIDIFIER, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/lg_thinq/humidifier.py b/homeassistant/components/lg_thinq/humidifier.py new file mode 100644 index 00000000000..37c14c055b8 --- /dev/null +++ b/homeassistant/components/lg_thinq/humidifier.py @@ -0,0 +1,195 @@ +"""Support for humidifier entities.""" + +from __future__ import annotations + +from dataclasses import dataclass +import logging +from typing import Any + +from thinqconnect import DeviceType +from thinqconnect.devices.const import Property as ThinQProperty +from thinqconnect.integration import ActiveMode + +from homeassistant.components.humidifier import ( + HumidifierAction, + HumidifierDeviceClass, + HumidifierEntity, + HumidifierEntityDescription, + HumidifierEntityFeature, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import ThinqConfigEntry +from .coordinator import DeviceDataUpdateCoordinator +from .entity import ThinQEntity + + +@dataclass(frozen=True, kw_only=True) +class ThinQHumidifierEntityDescription(HumidifierEntityDescription): + """Describes ThinQ humidifier entity.""" + + current_humidity_key: str + operation_key: str + mode_key: str = ThinQProperty.CURRENT_JOB_MODE + + +DEVICE_TYPE_HUM_MAP: dict[DeviceType, ThinQHumidifierEntityDescription] = { + DeviceType.DEHUMIDIFIER: ThinQHumidifierEntityDescription( + key=ThinQProperty.TARGET_HUMIDITY, + name=None, + device_class=HumidifierDeviceClass.DEHUMIDIFIER, + translation_key="dehumidifier", + current_humidity_key=ThinQProperty.CURRENT_HUMIDITY, + operation_key=ThinQProperty.DEHUMIDIFIER_OPERATION_MODE, + ), + DeviceType.HUMIDIFIER: ThinQHumidifierEntityDescription( + key=ThinQProperty.TARGET_HUMIDITY, + name=None, + device_class=HumidifierDeviceClass.HUMIDIFIER, + translation_key="humidifier", + current_humidity_key=ThinQProperty.HUMIDITY, + operation_key=ThinQProperty.HUMIDIFIER_OPERATION_MODE, + ), +} + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ThinqConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up an entry for humidifier platform.""" + entities: list[ThinQHumidifierEntity] = [] + for coordinator in entry.runtime_data.coordinators.values(): + if ( + description := DEVICE_TYPE_HUM_MAP.get(coordinator.api.device.device_type) + ) is not None: + entities.extend( + ThinQHumidifierEntity(coordinator, description, property_id) + for property_id in coordinator.api.get_active_idx( + description.key, ActiveMode.READ_WRITE + ) + ) + + if entities: + async_add_entities(entities) + + +class ThinQHumidifierEntity(ThinQEntity, HumidifierEntity): + """Represent a ThinQ humidifier entity.""" + + entity_description: ThinQHumidifierEntityDescription + _attr_supported_features = HumidifierEntityFeature.MODES + + def __init__( + self, + coordinator: DeviceDataUpdateCoordinator, + entity_description: ThinQHumidifierEntityDescription, + property_id: str, + ) -> None: + """Initialize a humidifier entity.""" + super().__init__(coordinator, entity_description, property_id) + self._attr_available_modes = self.coordinator.data[ + self.entity_description.mode_key + ].options + + if self.data.max is not None: + self._attr_max_humidity = self.data.max + if self.data.min is not None: + self._attr_min_humidity = self.data.min + self._attr_target_humidity_step = ( + self.data.step if self.data.step is not None else 1 + ) + + def _update_status(self) -> None: + """Update status itself.""" + super()._update_status() + + self._attr_target_humidity = self.data.value + self._attr_current_humidity = self.coordinator.data[ + self.entity_description.current_humidity_key + ].value + self._attr_is_on = self.coordinator.data[ + self.entity_description.operation_key + ].is_on + self._attr_mode = self.coordinator.data[self.entity_description.mode_key].value + if self.is_on: + self._attr_action = ( + HumidifierAction.DRYING + if self.entity_description.device_class + == HumidifierDeviceClass.DEHUMIDIFIER + else HumidifierAction.HUMIDIFYING + ) + else: + self._attr_action = HumidifierAction.OFF + + _LOGGER.debug( + "[%s:%s] update status: c:%s, t:%s, mode:%s, action:%s, is_on:%s", + self.coordinator.device_name, + self.property_id, + self.current_humidity, + self.target_humidity, + self.mode, + self.action, + self.is_on, + ) + + async def async_set_mode(self, mode: str) -> None: + """Set new target preset mode.""" + _LOGGER.debug( + "[%s:%s] async_set_mode: %s", + self.coordinator.device_name, + self.entity_description.mode_key, + mode, + ) + await self.async_call_api( + self.coordinator.api.post(self.entity_description.mode_key, mode) + ) + + async def async_set_humidity(self, humidity: int) -> None: + """Set new target humidity.""" + _target_humidity = round(humidity / (self.target_humidity_step or 1)) * ( + self.target_humidity_step or 1 + ) + _LOGGER.debug( + "[%s:%s] async_set_humidity: %s, target_humidity: %s, step: %s", + self.coordinator.device_name, + self.property_id, + humidity, + _target_humidity, + self.target_humidity_step, + ) + if _target_humidity == self.target_humidity: + return + await self.async_call_api( + self.coordinator.api.post(self.property_id, _target_humidity) + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the entity on.""" + if self.is_on: + return + _LOGGER.debug( + "[%s:%s] async_turn_on", + self.coordinator.device_name, + self.entity_description.operation_key, + ) + await self.async_call_api( + self.coordinator.api.async_turn_on(self.entity_description.operation_key) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + if not self.is_on: + return + _LOGGER.debug( + "[%s:%s] async_turn_off", + self.coordinator.device_name, + self.entity_description.operation_key, + ) + await self.async_call_api( + self.coordinator.api.async_turn_off(self.entity_description.operation_key) + ) diff --git a/homeassistant/components/lg_thinq/strings.json b/homeassistant/components/lg_thinq/strings.json index 78e9f85da72..9d2d7df2863 100644 --- a/homeassistant/components/lg_thinq/strings.json +++ b/homeassistant/components/lg_thinq/strings.json @@ -199,6 +199,33 @@ } } }, + "humidifier": { + "dehumidifier": { + "state_attributes": { + "mode": { + "state": { + "air_clean": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::air_clean%]", + "clothes_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::clothes_dry%]", + "intensive_dry": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::intensive_dry%]", + "quiet_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::quiet_humidity%]", + "rapid_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::rapid_humidity%]", + "smart_humidity": "[%key:component::lg_thinq::entity::sensor::current_job_mode::state::smart_humidity%]" + } + } + } + }, + "humidifier": { + "state_attributes": { + "mode": { + "state": { + "air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::air_clean%]", + "humidify": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify%]", + "humidify_and_air_clean": "[%key:component::lg_thinq::entity::select::current_job_mode::state::humidify_and_air_clean%]" + } + } + } + } + }, "number": { "fan_speed": { "name": "Fan" diff --git a/tests/components/lg_thinq/conftest.py b/tests/components/lg_thinq/conftest.py index a0626ddb603..1a5bafe77d0 100644 --- a/tests/components/lg_thinq/conftest.py +++ b/tests/components/lg_thinq/conftest.py @@ -116,6 +116,7 @@ def mock_thinq_mqtt_client() -> Generator[None]: params=[ "air_conditioner", "washer", + "dehumidifier", ] ) def device_fixture(request: pytest.FixtureRequest) -> Generator[str]: diff --git a/tests/components/lg_thinq/fixtures/dehumidifier/device.json b/tests/components/lg_thinq/fixtures/dehumidifier/device.json new file mode 100644 index 00000000000..b8744e6e44a --- /dev/null +++ b/tests/components/lg_thinq/fixtures/dehumidifier/device.json @@ -0,0 +1,9 @@ +{ + "deviceId": "TQS-25A89504-EF8E-4C4E-99F3-3956ED3BD397", + "deviceInfo": { + "deviceType": "DEVICE_DEHUMIDIFIER", + "modelName": "DHUM_056905_KR", + "alias": "Test dehumidifier", + "reportable": true + } +} diff --git a/tests/components/lg_thinq/fixtures/dehumidifier/energy_profile.json b/tests/components/lg_thinq/fixtures/dehumidifier/energy_profile.json new file mode 100644 index 00000000000..b2792613644 --- /dev/null +++ b/tests/components/lg_thinq/fixtures/dehumidifier/energy_profile.json @@ -0,0 +1,6 @@ +{ + "resultCode": "0000", + "result": { + "property": ["energyUsage"] + } +} diff --git a/tests/components/lg_thinq/fixtures/dehumidifier/profile.json b/tests/components/lg_thinq/fixtures/dehumidifier/profile.json new file mode 100644 index 00000000000..6955759e43e --- /dev/null +++ b/tests/components/lg_thinq/fixtures/dehumidifier/profile.json @@ -0,0 +1,78 @@ +{ + "notification": { + "push": ["WATER_IS_FULL"] + }, + "property": { + "airFlow": { + "windStrengthLevel": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["LOW", "HIGH"], + "w": ["LOW", "HIGH"] + } + } + }, + "operation": { + "dehumidifierOperationMode": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": ["POWER_ON", "POWER_OFF"], + "w": ["POWER_ON", "POWER_OFF"] + } + } + }, + "dehumidifierJobMode": { + "currentJobMode": { + "mode": ["r", "w"], + "type": "enum", + "value": { + "r": [ + "SMART_HUMIDITY", + "INTENSIVE_DRY", + "RAPID_HUMIDITY", + "QUIET_HUMIDITY", + "CLOTHES_DRY" + ], + "w": [ + "SMART_HUMIDITY", + "INTENSIVE_DRY", + "RAPID_HUMIDITY", + "QUIET_HUMIDITY", + "CLOTHES_DRY" + ] + } + } + }, + "humidity": { + "currentHumidity": { + "mode": ["r"], + "type": "range", + "value": { + "r": { + "max": 100, + "min": 0, + "step": 1 + } + } + }, + "targetHumidity": { + "mode": ["r", "w"], + "type": "range", + "value": { + "r": { + "max": 70, + "min": 30, + "step": 5 + }, + "w": { + "max": 70, + "min": 30, + "step": 5 + } + } + } + } + } +} diff --git a/tests/components/lg_thinq/fixtures/dehumidifier/status.json b/tests/components/lg_thinq/fixtures/dehumidifier/status.json new file mode 100644 index 00000000000..c8cbd04fabd --- /dev/null +++ b/tests/components/lg_thinq/fixtures/dehumidifier/status.json @@ -0,0 +1,15 @@ +{ + "airFlow": { + "windStrengthLevel": "HIGH" + }, + "dehumidifierJobMode": { + "currentJobMode": "SMART_HUMIDITY" + }, + "humidity": { + "currentHumidity": 80, + "targetHumidity": 55 + }, + "operation": { + "dehumidifierOperationMode": "POWER_ON" + } +} diff --git a/tests/components/lg_thinq/snapshots/test_humidifier.ambr b/tests/components/lg_thinq/snapshots/test_humidifier.ambr new file mode 100644 index 00000000000..8fce4087ca1 --- /dev/null +++ b/tests/components/lg_thinq/snapshots/test_humidifier.ambr @@ -0,0 +1,76 @@ +# serializer version: 1 +# name: test_humidifier_entities[dehumidifier][humidifier.test_dehumidifier-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'available_modes': list([ + 'smart_humidity', + 'intensive_dry', + 'rapid_humidity', + 'quiet_humidity', + 'clothes_dry', + ]), + 'max_humidity': 70, + 'min_humidity': 30, + 'target_humidity_step': 5, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'humidifier', + 'entity_category': None, + 'entity_id': 'humidifier.test_dehumidifier', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': None, + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': , + 'translation_key': 'dehumidifier', + 'unique_id': 'TQS-25A89504-EF8E-4C4E-99F3-3956ED3BD397_target_humidity', + 'unit_of_measurement': None, + }) +# --- +# name: test_humidifier_entities[dehumidifier][humidifier.test_dehumidifier-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'action': , + 'available_modes': list([ + 'smart_humidity', + 'intensive_dry', + 'rapid_humidity', + 'quiet_humidity', + 'clothes_dry', + ]), + 'current_humidity': 80, + 'device_class': 'dehumidifier', + 'friendly_name': 'Test dehumidifier', + 'humidity': 55, + 'max_humidity': 70, + 'min_humidity': 30, + 'mode': 'smart_humidity', + 'supported_features': , + 'target_humidity_step': 5, + }), + 'context': , + 'entity_id': 'humidifier.test_dehumidifier', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- \ No newline at end of file diff --git a/tests/components/lg_thinq/snapshots/test_switch.ambr b/tests/components/lg_thinq/snapshots/test_switch.ambr index e427916630b..89aab01d510 100644 --- a/tests/components/lg_thinq/snapshots/test_switch.ambr +++ b/tests/components/lg_thinq/snapshots/test_switch.ambr @@ -194,4 +194,53 @@ 'last_updated': , 'state': 'off', }) +# --- +# name: test_switch_entities[dehumidifier][switch.test_dehumidifier_power-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'switch', + 'entity_category': None, + 'entity_id': 'switch.test_dehumidifier_power', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Power', + 'platform': 'lg_thinq', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'operation_power', + 'unique_id': 'TQS-25A89504-EF8E-4C4E-99F3-3956ED3BD397_dehumidifier_operation_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_switch_entities[dehumidifier][switch.test_dehumidifier_power-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'switch', + 'friendly_name': 'Test dehumidifier Power', + }), + 'context': , + 'entity_id': 'switch.test_dehumidifier_power', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) # --- \ No newline at end of file diff --git a/tests/components/lg_thinq/test_humidifier.py b/tests/components/lg_thinq/test_humidifier.py new file mode 100644 index 00000000000..fa0f92975bc --- /dev/null +++ b/tests/components/lg_thinq/test_humidifier.py @@ -0,0 +1,31 @@ +"""Tests for the LG ThinQ humidifier platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("device_fixture", ["dehumidifier"]) +async def test_humidifier_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + devices: AsyncMock, + mock_thinq_api: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.lg_thinq.PLATFORMS", [Platform.HUMIDIFIER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) diff --git a/tests/components/lg_thinq/test_number.py b/tests/components/lg_thinq/test_number.py index b36685a8aa4..a740cb500c5 100644 --- a/tests/components/lg_thinq/test_number.py +++ b/tests/components/lg_thinq/test_number.py @@ -15,6 +15,7 @@ from tests.common import MockConfigEntry, snapshot_platform @pytest.mark.usefixtures("entity_registry_enabled_by_default") +@pytest.mark.parametrize("device_fixture", ["air_conditioner", "washer"]) async def test_number_entities( hass: HomeAssistant, snapshot: SnapshotAssertion,